Bar graphs are one of the foundations of the graphing world. We'll walk through how to take data, transform it using scaleBand
and scaleLinear
to create a dynamic bar chart to fit into any width/height.
To get setup we import some data, specify a width/height to work with and render our svg
so we have a place to render our bars.
import React, { Component } from "react"; import { data } from "./data"; const width = 500; const height = 300; class App extends Component { render() { return ( <div> <svg width={width} height={height} /> </div> ); } }
These are our JavaScript data selectors. When given a data point from the array of data they will select the appropriate key.
So if your data had a value like price
instead of y
, you'd have d.price
being returned for our ySelector
.
const xSelector = (d) => d.x; const ySelector = (d) => d.y;
Our data structure example
[ { x: 0, y: 1, // Just so it shows up :) }, { x: 1, y: 50, }, { x: 2, y: 100, }, { x: 3, y: 150, }, ]; //...
We need to start by importing our 2 scales from d3-scale
.
import { scaleBand, scaleLinear } from "d3-scale";
scaleBand
is used to create an output range is continuous and numeric.
Basically the concept of scaleBand
is take an output area (some width), take our data and amount of data points, split it into bands.
A band in our case can be considered each bar. This is why we need to know how many pieces of data we have. In our example data we have 10
bars. With the current width, as well as known data points it will figure out how wide each bar is.
Not only will it divide our content into each specific bar by width but we can apply padding. This will allow us to create an even spacing of each band aka each bar.
Read more about scaleBand
in the D3 documentation d3-scale#band-scales.
const xScale = scaleBand() .range([0, width]) .domain(data.map((d) => xSelector(d))) .padding(0.3);
So here we have our range of rendering which will be constrained to [0, width]
, so from [0, 500]
. Then we will pass in our domain.
Our domain will be an array of our data points so it can calculate how many bars we need to place into 500
pixels of space. Finally we toss some padding on. This will set our innerPadding
and outerPadding
to the same value.
innerPadding
being between each band aka each bar. The outerPadding
being on the left and right sides after all bands are rendered. The padding must be inbetween 0 and 1. We set it to .3
so that it's roughly a 1/3 of the width of a bar as spacing. If we set it to .5
the padding and the bars would all be equal widths.
Our scaleLinear
function will map our domain of y
value data which sits between 0
and 500
as the max value. Which will map it to the height
of our svg we're rendering into.
const yScale = scaleLinear().range([height, 0]).domain([0, 500]);
The reason why the height
is mapped to the 0
of our data is that the top left corner of an SVG is 0,0
. When graphing 0
is generally on the bottom. So in order to map our 0
value to render at the bottom we map it to the height of our svg.
We need to map over each of our data pieces and we'll calculate the data needed to render using our scales that we created. Each bar will eventually be rendered separately.
{ data.map((d, i) => { const yValue = yScale(ySelector(d)); const barHeight = height - yValue; }); }
Our yScale(ySelector(d))
is calculating the y
value. You may think that we are rendering from the bottom up because that's how you're viewing it but what we're doing is actually rendering from the top down. Remember everything originates from 0,0
in the corner.
So for a value of 0
we've mapped it to the height of 300
. So in our case a value that's 0 would be rendered at y=300
. Then based upon the value of which we are rendering at we need to render it down to the bottom of the svg which is at 300
.
In the middle of our values it will be at y=150
. So we'll start our rendering in the middle, and render a bar that is 150px
as well. Then for our value that is at the very top our y
is going to be 0
so it will start it's rendering at the top, and thus our bar height will be 300px
.
To render a bar we just use the rect
.
First off we use width={xScale.bandwidth()}
to get the bar width which is automatically calculated for us based upon the range
and domain
that we set. This will make sure that our bars fit in the space we've set.
Then we have our height={barHeight}
, it's calculated from the y
point minus the height
. As mentioned before the y
value is going to be the top of the bar, and then we're rendering down towards the bottom of the SVG.
The x={xScale(xSelector(d))}
will take our value, select the x
off of it and then pass it through to our xScale
which is our scaleBand
. This will take into account the number of bars we have, as well as the padding
we've set and give us our x
value to render.
The y
will just be our yValue
that we've calculated with our scaleLinear
.
return ( <div> <svg width={width} height={height}> {data.map((d, i) => { const yValue = yScale(ySelector(d)); const barHeight = height - yValue; return ( <rect key={i} width={xScale.bandwidth()} height={barHeight} x={xScale(xSelector(d))} y={yValue} stroke="#ff6347" strokeWidth={3} fill="#f5f5f5" /> ); })} </svg> </div> );
Now if we wanted to render our data in an upside down fashion all we need to do is make one small tweak. Rather than render at our yValue
we just render at 0
. The barHeight
will be correctly calculated from our scales, but we're just changing the point at which it starts rendering.
So to accomplish this pop in y={0}
.
<rect key={i} width={xScale.bandwidth()} height={barHeight} x={xScale(xSelector(d))} y={0} stroke="#ff6347" strokeWidth={3} fill="#f5f5f5" />
There we have it! We took some data and using 2 different scale methods were able to turn it into a bar graph that can be rendered into dynamic width/height containers.