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.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With