Band scales for bar and column charts
A basic feature of bar (horizontal) and column (vertical) charts is that the width of the "bars" is based on how many bars there are and how much space is available. After all, this looks pretty silly, doesn't it?
This is where band scales come in. A band scale’s primary value prop is it will calculate the “band width” for you. In order for it to do this, you have to tell it all of the values that need to be displayed. It will then calculate what each bar's width should be, based on the number of items to display and the available space. In our examples above, the values are simply letters and numbers but in a real world scenario you'd likely pluck the values off of your data items. We'll look at how to do that in just a moment.
There's a name for this kind of thing
A band scale is a type of ordinal scale. According to the D3 docs, ordinal scales are categorized by a discrete, rather than continuous, domain and range.
Clear as mud? Let's break it down.
The simplest type of scale is a linear scale, which you've seen before. A linear scale does a direct, "linear" mapping of values from one context to another. For example, if you were converting percentage values (that range from 0 to 100) to normalized values between 0 and 1, you could use a linear scale like this.
d3
A linear scale is a type of continuous scale, which just means there are an infinite amount of possible values within the context. While our linear scale's domain specifies bounds of 0 and 100, absolutely any value between those bounds can be translated. 7.0000003
is different than 7.0000004
, for example.
An ordinal scale, on the other hand, uses a finite, or discrete, set of values for both the input domain and the output range.
Creating a band scale
Now that we know roughly how a band scale works, lets look at how to create one. For the rest of the examples we'll be assuming this is our data.
const data = name: 'Alice' math: 37 science: 62 language: 54 name: 'Billy' math: 43 science: 34 language: 85 name: 'Cindy' math: 86 science: 48 language: 71 name: 'David' math: 44 science: 21 language: 65 name: 'Emily' math: 59 science: 73 language: 29
So we're dealing with an array of objects, and the name
property is what we want our X axis to display. Axes are created from scales, so we need to create our band scale first, and then we can create the axis.
const xScale = d3
The most important part here is .domain(data.map(d => d.name))
. We're using a simple Array.map to pluck the name property off of our objects, which would result in an array of ['Alice', 'Billy', 'Cindy', 'David', 'Emily']
. Just like we said we needed, this is all of the values that need to be displayed on our band scale.
The range is specified using the width
, since we want the scale to fill the available space. We also specified a padding
value which just controls the space between bars. 0
is no space and 0.5
makes the bars and spaces the same size. Everything else is proportional.
Using the band scale
With the scale created we can go ahead and use it, which looks like using any other scale in D3.
svg
This is a pretty standard set of commands for an X axis. We append a new group to the SVG so the axis has its own context, move the group to the bottom of the chart using a height
property, and then call d3.axisBottom
, passing in our scale. This would look the same if our xScale
was a linear scale, or any other non-band scale.
Lastly, we have to use the scale to position and size our bars. Since our example is a column chart it's the X position and width of the bars that we control with the band scale. If you were building a horizontal bar chart you'd use the band scale to set the Y position and bar height.
So we're setting our rect's attributes by using the scale in two different ways.
.attr('x', d => xScale(d.name))
sets the bar's position by passing the datum's name
property to the band scale. Note this is the same property we used to construct the band scale's domain. This is important, because the scale is essentially going to look up the name in its internal list of recognizable values. When it receives "Billy", for example, it will find that it's the second value in the domain and return the second value from the list of positions it calculated.
.attr('width', d => xScale.bandwidth())
calls the bandwidth
method on our scale to get the size it has determined each bar should be. Note that this is a single, static value based on how the band scale was constructed. Each bar will have the same width, and we don't have to pass the datum into anything to get the value. In fact, you could rewrite the line as .attr('width', xScale.bandwidth)
and it would still work.
You did it!
That's pretty much all there is to band scales. As always, you can play with the full example here.
Happy hacking!