Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Advanced positioning and sizing of a react radio button group on top of an SVG

Edit: here is a link to my app that is currently displaying the problem I am referring to.

I did my best to make the post concise and thorough, but apologies in advance if it is a bit long. I think the length is helpful though.

I have what feels like a challenging task, involving the positioning of a React radio button group on top of an SVG that shows a graph. I have created the following code snippet, which includes two components (a graph and a container component), to help highlight the problem I am having.

In place of an actual graph (scatter plot, bar plot, w/e), I instead made just a rectangular SVG with 3 colorful rects. On the SVG component, I have also added 6 black boxes on the right hand side, which I use as a radio button group built directly onto the SVG using D3.

For a variety of state reasons (mainly because I would like my container component to hold the state of the graph, since the container component will have other parts that require the button values), I am working on building a set of React radio buttons, to replace the D3 SVG radio buttons, but am struggling with the positioning of the buttons. Given the way I've made the SVG (using Viewbox), the React radio buttons built directly onto the SVG rescale as the width of the browser window changes. This is important not because viewers of my website will be actively changing the browser window size (they wont be), but rather because I dont know how wide the users browser is, and this resizing of the buttons with the resizing of the SVG means the buttons will always look good.

However, the React radio buttons do not scale. As you can see here:

<div style={{"width":"85%", "margin":"0 auto", "marginTop":"75px", "padding":"0", "position":"relative"}}>
  <div style={{"width":"450px", "position":"absolute", "bottom":"32px", "left":"20px", "zIndex":"1"}}>
      <h3>X-Axis Stat</h3>
      {xStatButtons}
  </div>

  <div style={{"position":"absolute", "top":"5px", "left":"8px", "zIndex":"1"}}>
      <h3>Y-Axis Stat</h3>
      {yStatButtons}
  </div>
</div>

I use position = absolute|relative, along with top|bottom|left styles in order to position the buttons on top of the SVG. I also change the z-index on the react radio button divs so that they are on top of the SVG.

Before reviewing the code, please run the code snippet and open to full screen, and increase and decrease the width of your browser window.

class GraphComponent extends React.Component {

	constructor(props) { 
		super(props);
    
		this.chartProps = {
			chartWidth: 400, // Dont Change These, For Viewbox 
			chartHeight: 200 // Dont Change These, For Viewbox 
		};
	}

  // Lifecycle Components
	componentDidMount() {
    
		const { chartHeight, chartWidth, svgID } = this.chartProps;
		d3.select('#d3graph')
			.attr('width', '100%')
			.attr('height', '100%')
			.attr('viewBox', "0 0 " + chartWidth + " " + chartHeight)
			.attr('preserveAspectRatio', "xMaxYMax");
    
    const fakeButtons = d3.select('g.fakeButtons')
    for(var i = 0; i < 6; i++) {
      fakeButtons
        .append('rect')
        .attr('x', 370)
        .attr('y', 15 + i*25)
        .attr('width', 25)
        .attr('height', 20)
        .attr('fill', 'black')
        .attr('opacity', 1)
        .attr('stroke', 'black')
        .attr('stroke-width', 2) 
    }
    
    d3.select('g.myRect')
      .append('rect')
        .attr('x', 0)
        .attr('y', 0)
        .attr('width', chartWidth)
        .attr('height', chartHeight)
        .attr('fill', 'red')
        .attr('opacity', 0.8)
        .attr('stroke', 'black')
        .attr('stroke-width', 5)
    
    d3.select('g.myRect')
      .append('rect')
        .attr('x', 35)
        .attr('y', 35)
        .attr('width', chartWidth - 70)
        .attr('height', chartHeight - 70)
        .attr('fill', 'blue')
        .attr('opacity', 0.8)
        .attr('stroke', 'black')
        .attr('stroke-width', 2)
    
    d3.select('g.myRect')
      .append('rect')
        .attr('x', 70)
        .attr('y', 70)
        .attr('width', chartWidth - 140)
        .attr('height', chartHeight - 140)
        .attr('fill', 'green')
        .attr('opacity', 0.8)
        .attr('stroke', 'black')
        .attr('stroke-width', 2)
	}
  
	render() {
 		return (
			<div ref="scatter">
				<svg id="d3graph">
          <g className="myRect" />
          <g className="fakeButtons" />
				</svg>
			</div>
		)
	}
}

class GraphContainer extends React.Component {

	constructor(props) { 
		super(props);
    this.state = {
      statNameX: "AtBats",
      statNameY: "Saves"
    }
	}

	render() {
    
    const { statNameX, statNameY } = this.state;
    const xStats = [
      { value: "GamesPlayed", label: "G" },
      { value: "AtBats", label: "AB" },
      { value: "Runs", label: "R" },
      { value: "Hits", label: "H" },
      { value: "SecondBaseHits", label: "2B" },
      { value: "ThirdBaseHits", label: "3B" },
      { value: "Homeruns", label: "HR" },
      { value: "RunsBattedIn", label: "RBI" }];
    const yStats = [
      { value: "Wins", label: "W" },
      { value: "Losses", label: "L" },
      { value: "EarnedRunAvg", label: "ERA" },
      { value: "GamesPlayed", label: "G" },
      { value: "GamesStarted", label: "GS" },
      { value: "Saves", label: "SV" },
      { value: "RunsAllowed", label: "R"}]; 
    
    const xStatButtons = 
          <form>
            <div className="qualify-radio-group scatter-group">
              {xStats.map((d, i) => {
                return (
                  <label key={'xstat-' + i}>
                    <input
                      type={"radio"}
                      value={xStats[i].value}
                    />
                    <span>{xStats[i].label}</span>
                  </label>
                )
              })}
            </div>
          </form>;
    
    const yStatButtons = 
          <form>
            <div className="qualify-radio-group scatter-group">
              {yStats.map((d, i) => {
                return (
                  <label  className="go-vert" key={'ystat-' + i}>
                    <input
                      type={"radio"}
                      value={yStats[i].value}
                    />
                    <span>{yStats[i].label}</span>
                  </label>
                )
              })}
            </div>
          </form>;
    
 		return (
			<div style={{"width":"85%", "margin":"0 auto", "marginTop":"75px", "padding":"0", "position":"relative"}}>
        
        <div style={{"width":"450px", "position":"absolute", "bottom":"32px", "left":"20px", "zIndex":"1"}}>
          <h3>X-Axis Stat</h3>
          {xStatButtons}
        </div>

        <div style={{"position":"absolute", "top":"5px", "left":"8px", "zIndex":"1"}}>
          <h3>Y-Axis Stat</h3>
          {yStatButtons}
        </div>
        
        <GraphComponent />
      </div>
		)
	}
}



ReactDOM.render(
  <GraphContainer />,
  document.getElementById('root')
);
.scatter-group input[type=radio] {
	visibility:hidden;
	width:0px;
	height:0px;
	overflow:hidden;
}

.scatter-group input[type=radio] + span {
	cursor: pointer;
	display: inline-block;
	vertical-align: top;
	line-height: 25px;
	padding: 2px 6px;
	border-radius: 2px;
	color: #333;
	background: #EEE;

	border-radius: 5px;
	border: 2px solid #333;

	margin-right: 2px;
}

.scatter-group input[type=radio]:not(:checked) + span {
	cursor: pointer;
	background-color: #EEE;
	color: #333;
}

.scatter-group input[type=radio]:not(:checked) + span:hover{
	cursor: pointer;
	background: #888;
}

.scatter-group input[type=radio]:checked + span{
	cursor: pointer;
	background-color: #333;
	color: #EEE;
}

.go-vert {
	display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

<div id='root'>
  WORK
</div>

My question is this: How do i do the sizing and positioning correctly with the React radio buttons so that they behave (from a size and positioning standpoint) in a manner similar to if had built the radio buttons directly into the SVG (how the black boxes are resizing)?

Any help at all is appreciated with this.

like image 636
Canovice Avatar asked Nov 08 '22 06:11

Canovice


1 Answers

Your milage might vary, but I would suggest trying to use vw units to size radio buttons. 1vw is equal 1% of the viewport which can be used on font-size, padding and line-height properties of your radio button elements.

Extracting changes I made to your example:

    .scatter-group input[type=radio] + span {
        font-size: calc(6px + 0.8vw);
        line-height: 1;
        padding: 0.8vw;
    }

First I combine calc and vw to get fluid font-size that doesn't go below 6px on small viewport. line-height impacts overall height of the button so I reset it to 1 which means it will be 100% of font-size. Finally, I use vw unit for padding. You can use same calc trick as before for fine-grain control over dimensions.

Example is not perfect and will require some tweaking to get it to match SVG in real-world conditions. Button containers should also be adjusted depending on viewport, I didn't touch those since it's not is question scope.

vw and calc() are supported in all major browsers.

Reference:
- https://www.sitepoint.com/css-viewport-units-quick-start/
- https://css-tricks.com/snippets/css/fluid-typography/
- https://www.smashingmagazine.com/2016/05/fluid-typography/

class GraphComponent extends React.Component {

	constructor(props) { 
		super(props);
    
		this.chartProps = {
			chartWidth: 400, // Dont Change These, For Viewbox 
			chartHeight: 200 // Dont Change These, For Viewbox 
		};
	}

  // Lifecycle Components
	componentDidMount() {
    
		const { chartHeight, chartWidth, svgID } = this.chartProps;
		d3.select('#d3graph')
			.attr('width', '100%')
			.attr('height', '100%')
			.attr('viewBox', "0 0 " + chartWidth + " " + chartHeight)
			.attr('preserveAspectRatio', "xMaxYMax");
    
    const fakeButtons = d3.select('g.fakeButtons')
    for(var i = 0; i < 6; i++) {
      fakeButtons
        .append('rect')
        .attr('x', 370)
        .attr('y', 15 + i*25)
        .attr('width', 25)
        .attr('height', 20)
        .attr('fill', 'black')
        .attr('opacity', 1)
        .attr('stroke', 'black')
        .attr('stroke-width', 2) 
    }
    
    d3.select('g.myRect')
      .append('rect')
        .attr('x', 0)
        .attr('y', 0)
        .attr('width', chartWidth)
        .attr('height', chartHeight)
        .attr('fill', 'red')
        .attr('opacity', 0.8)
        .attr('stroke', 'black')
        .attr('stroke-width', 5)
    
    d3.select('g.myRect')
      .append('rect')
        .attr('x', 35)
        .attr('y', 35)
        .attr('width', chartWidth - 70)
        .attr('height', chartHeight - 70)
        .attr('fill', 'blue')
        .attr('opacity', 0.8)
        .attr('stroke', 'black')
        .attr('stroke-width', 2)
    
    d3.select('g.myRect')
      .append('rect')
        .attr('x', 70)
        .attr('y', 70)
        .attr('width', chartWidth - 140)
        .attr('height', chartHeight - 140)
        .attr('fill', 'green')
        .attr('opacity', 0.8)
        .attr('stroke', 'black')
        .attr('stroke-width', 2)
	}
  
	render() {
 		return (
			<div ref="scatter">
				<svg id="d3graph">
          <g className="myRect" />
          <g className="fakeButtons" />
				</svg>
			</div>
		)
	}
}

class GraphContainer extends React.Component {

	constructor(props) { 
		super(props);
    this.state = {
      statNameX: "AtBats",
      statNameY: "Saves"
    }
	}

	render() {
    
    const { statNameX, statNameY } = this.state;
    const xStats = [
      { value: "GamesPlayed", label: "G" },
      { value: "AtBats", label: "AB" },
      { value: "Runs", label: "R" },
      { value: "Hits", label: "H" },
      { value: "SecondBaseHits", label: "2B" },
      { value: "ThirdBaseHits", label: "3B" },
      { value: "Homeruns", label: "HR" },
      { value: "RunsBattedIn", label: "RBI" }];
    const yStats = [
      { value: "Wins", label: "W" },
      { value: "Losses", label: "L" },
      { value: "EarnedRunAvg", label: "ERA" },
      { value: "GamesPlayed", label: "G" },
      { value: "GamesStarted", label: "GS" },
      { value: "Saves", label: "SV" },
      { value: "RunsAllowed", label: "R"}]; 
    
    const xStatButtons = 
          <form>
            <div className="qualify-radio-group scatter-group">
              {xStats.map((d, i) => {
                return (
                  <label key={'xstat-' + i}>
                    <input
                      type={"radio"}
                      value={xStats[i].value}
                    />
                    <span>{xStats[i].label}</span>
                  </label>
                )
              })}
            </div>
          </form>;
    
    const yStatButtons = 
          <form>
            <div className="qualify-radio-group scatter-group">
              {yStats.map((d, i) => {
                return (
                  <label  className="go-vert" key={'ystat-' + i}>
                    <input
                      type={"radio"}
                      value={yStats[i].value}
                    />
                    <span>{yStats[i].label}</span>
                  </label>
                )
              })}
            </div>
          </form>;
    
 		return (
			<div style={{"width":"85%", "margin":"0 auto", "marginTop":"75px", "padding":"0", "position":"relative"}}>
        
        <div style={{"width":"450px", "position":"absolute", "bottom":"32px", "left":"20px", "zIndex":"1"}}>
          <h3>X-Axis Stat</h3>
          {xStatButtons}
        </div>

        <div style={{"position":"absolute", "top":"5px", "left":"8px", "zIndex":"1"}}>
          <h3>Y-Axis Stat</h3>
          {yStatButtons}
        </div>
        
        <GraphComponent />
      </div>
		)
	}
}



ReactDOM.render(
  <GraphContainer />,
  document.getElementById('root')
);
.scatter-group input[type=radio] {
	visibility:hidden;
	width:0px;
	height:0px;
	overflow:hidden;
}

.scatter-group input[type=radio] + span {
	cursor: pointer;
	display: inline-block;
	vertical-align: top;
	line-height: 1;
	padding: 0.8vw;
    font-size: calc(6px + 0.8vw);
	border-radius: 2px;
	color: #333;
	background: #EEE;

	border-radius: 5px;
	border: 2px solid #333;

	margin-right: 2px;
}

.scatter-group input[type=radio]:not(:checked) + span {
	cursor: pointer;
	background-color: #EEE;
	color: #333;
}

.scatter-group input[type=radio]:not(:checked) + span:hover{
	cursor: pointer;
	background: #888;
}

.scatter-group input[type=radio]:checked + span{
	cursor: pointer;
	background-color: #333;
	color: #EEE;
}

.go-vert {
	display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

<div id='root'>
  WORK
</div>
like image 77
Teo Dragovic Avatar answered Nov 11 '22 08:11

Teo Dragovic