Using scales
Learn how to use D3's scale module to gain control over the size of our bar chart.
Transcript
One of the reasons D3 is so useful is because all of the little graphing functions it provides. Scales are a great example of this, so in this video we'll learn how to use the d3-scale
module to get our chart looking a little bit better.
Now, if we look at our chart right now, we'll notice a few things. First, the bars have a hard-coded width of 20 pixels; and the heights are 15, 42, 23 and 27, which come from the count
property of our author data.
Next, if we hover over the svg
element we can see that it's 350 pixels wide by 150 pixels tall. We didn't set this explicitly, this is just the default dimensions that the svg
element gets.
What we want to do is use scales so that our bars stretch to fill the full space of the svg
element. This is exactly what D3 scales are designed to do. Let's see how they work.
We'll start with the vertical scale to get the correct heights for our bars. We want to use a linear scale here, so let's come up to the top of our file, and we'll import the scaleLinear
named import from the d3-scale
module.
import { scaleLinear } from 'd3-scale';
Now we can come down here and actually create our scale. These scale functions actually return objects, so we invoke them like this
scaleLinear();
and then we can use method chaining to set properties on them.
In the D3 community you'll often see scales that operate along the vertical axis called Y scales — so, let's store this scale in a variable called yScale
:
let yScale = scaleLinear();
and now we can work with it.
Our yScale is really a function that takes in the count of an author and then calculates how tall the corresponding bar should be. So for it to work, it needs to know the biggest count within our dataset, and it also needs to know the total height of the SVG container element.
The way we set properties on our scale is by using the domain
and range
methods. Let's start with the domain.
The domain
describes the minimum and maximum values of our dataset. So if our author counts had values between 10 and 50, we would pass in 10 and 50 here:
.domain([ 10, 50 ])
Now in our case, we want our minimum to actually be 0 regardless of the lowest author count — and this is just a good practice for lots of common data visualizations. But we need to calculate the maximum using our actual dataset.
So let's first get an array of all our author counts by getting our authors, and mapping each one to its count:
let authorCounts = this.get('authors').map(author => author.count);
Now we have an array of pure numbers here, and we can use use the Math.max
function to find the maximum value of this array. Math.max
doesn't actually work on arrays, it takes in an argument list. So we'll use the spread operator to turn our authorCounts
array into an argument list:
.domain([ 0, Math.max(...authorCounts) ])
Ok, so our scale's domain is now set. Next, we'll set the range
.
The range of our Y scale describes how tall our chart should be when we render it; and since we know the container SVG is 150 pixels tall, we actually don't have to do any calculations here. We can just call .range
and pass in [0, 150]
.
let authorCounts = this.get('authors').map(author => author.count);
let yScale = scaleLinear()
.domain([ 0, Math.max(...authorCounts) ])
.range([ 0, 150 ]);
Now we can use our scale to actually scale up our bar's heights. We'll come down to where we're setting the height attribute, and instead of just passing in this raw author.count
, we'll first run it through our scale:
.attr('height', author => yScale(author.count))
If we save our work, now our bars fill up the svg
element!
Ok, so we have our yScale to handle the heights - and now it's time to write our xScale which we'll use to take care of the bar widths.
Now for the x scale we'll something called a band scale, and this gives us a bunch of useful calculations for figuring out the widths for our bars, as well as figuring out the padding between our bars. So we'll come up to the top here and we'll scaleBand
as another named import.
import { scaleLinear, scaleBand } from 'd3-scale';
And then we can come down and start working on our xScale.
let xScale = scaleBand();
Now our xScale also has a domain and range, so let's start with the domain. The domain of our band scale is our entire set of authors. So we'll just get our authors, and then we want to map each author to its name, since the name is what uniquely identifies each author. And now our xScale knows about the entire set of names in our dataset.
The range is similar to our yScale - it describes the width of our chart, and since we know the container is 300 pixels wide, we can just put in 0 to 300.
let xScale = scaleBand()
.domain(this.get('authors').map(author => author.name))
.range([ 0, 300 ]);
Ok so, now we have our xScale
so we can come down and remove the hard-coded width of 20, and instead we'll grab our scale, and then we'll use the bandwidth
method, which is just a useful way to automatically calculate how wide each bar should be:
.attr('width', xScale.bandwidth())
Alright, let's save our work. Alright well, our bars are definitely bigger! But right now, they're all smushed together. And that's because right now, our x
offset is just hard-coded at 25 time the index here. But this value really needs to come from our xScale.
Let's remove this and use our scale. Now our xScale is really a function that takes in an author name and outputs the appropriate offset. So down here, we can actually call xScale
, and pass in the author's name.
.attr('width', xScale.bandwidth())
.attr('x', author => xScale(author.name))
If we save this, now our bars are actually offset correctly. We can also come here and delete the index argument, since we're not using it anymore.
Ok our bar chart is filling up its container, but the bars don't have any breathing room. To fix this, we can set the paddingInner
property on X scale.
paddingInner
takes a number between 0 and 1 which tells the scale what percent of the total width it should reserve to use for white space between the bars. So let's pass in 0.12 and save it.
.paddingInner(0.12);
Awesome! Now our bars take up the whole svg
, and they have some breathing room.
Now, before we move on we've got one last item here. You might have noticed something odd about our bar chart so far — well it's upside down! The bars are all aligned along the top, and then they grow downward.
This is because the SVG coordinate plane starts in the top-left corner of the SVG element, and then all of the calculations go down and to the right from there
In order to fix this, we need to take each bar and offset it vertically to the bottom of our SVG element. So, each bar needs to move down by the total height of the SVG, minus the height of the bar – and that will give us the appropriate offset.
So, let's come down to our chart, and add one more attribute - the y
attribute. We'll pass in a function for each author, and we'll return the total height of the SVG (which is 150), minus the height of the scaled bar, which again we'll use our Y scale to calculate.
.attr('y', author => 150 - yScale(author.count))
Now that's looking more like a bar chart!
The d3-scale
module actually has even more kinds of scales and tons of configuration options, so if you're interested in learning more be sure to check out the repo page.
In the next video we'll learn how to make our bar chart responsive.