bar charts are a common form of data visualisation and are probably familiar to most people. for those starting in D3, a bar chart is likely one of the first thing you will want to try doing. this tutorial doesn't go into the weeds about how D3 works, so some of the concepts may be confusing to those who are not familiar with this library. however, all the code is explained, to the best of my abilities, so it is hoped that this will be a good starting point for those wishing to learn D3, or else just a handy place to copy and paste code from for those looking for quick and dirty solutions. at the end of this article will be a list of what I think are useful resources for those wishing to dig deeper into D3
create or retrieve data
the first thing to do is to create some data. for our purposes, we will just hardcode it. the only requirement is that the data consist of an array of items. the reason for this is that each item represents a different bar, or category, in the chart. the structure of the item is less important because we can write functions to access it. obviously, it is best when the structure of the item is simple and easy to understand
const data = [{
category: 'football',
value: 9
},
{
category: 'tennis',
value: 7
},
{
category: 'athletics',
value: 5
},
{
category: 'gymnastics',
value: 3
},
{
category: 'chess',
value: 2
},
];
define main dimensions
it’s good to use variables to store values. it makes code more readable by giving names to values, and also allows these variables to be reused across the code and changed in one place. the SVG container will have a height, a width, and margins. the margins are necessary to make space for axes. in d3, it is convention to define the margins in an object.
const width = 500;
const height = 400;
const margin = {
top: 30,
right: 20,
bottom: 40,
left: 30,
};
create SVG container
this step will be repeated for all charts. we select the body element and append an svg element to it, configure it with a width and a height using the variables defined in the previous step, and we also give it a class name, allowing us to target it from within a stylesheet.
const width = 500;
const height = 400;
d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'chart-container')
scales
we need a scale for both the categories and the values. for the categories, we use scaleBand
as we are handling discrete values. the domain is the array of categories from our data, e.g., [‘football’, ‘tennis’, ‘athletics’, ‘gymnastics’, ‘chess’]
. the range is equivalent to the x coordinates of the two ends of the horizontal axis. in this case, the axis extends to the left and right margins. we add padding to give space between and around the bars
const categoriesScale = d3.scaleBand()
.domain(data.map(d => d.category))
.range([margin.left, width - margin.right])
.paddingInner(0.5)
.paddingOuter(0.2)
for the values scale, we use scaleLinear
, since we are handling continuous values. the domain goes from 0 to the maximum value within the data. similar to before, the range is equivalent to the y coordinates of the two ends of the vertical axis - but with a twist: the start of the range is the y coordinate corresponding the bottom margin of the chart and the end of the range corresponds to the top margin. the reason we do this is because the SVG coordinate system has the 0 y coordinate at the top of the svg container, whilst the coordinate system in our chart has the 0 y coordinate at the bottom. note the use of variables to contain the min and max domain values
const minValue = 0;
const maxValue = d3.max(data.map(d => d.value));
const valuesScale = d3.scaleLinear()
.domain([minValue, maxValue])
.range([height - margin.bottom, margin.top])
draw bars
the scales defined in the previous step are used to calculate the dimensions of the bars. the following code will likely be confusing for those not already familiar with D3. the key concept to understand is that, in D3, data is bound to HTML or SVG elements within the DOM. this allows this data to be conveniently accessed in code for manipulating the elements that represent it. in our case, the bars in our chart are represented using rect SVG elements, so each rect element has the data for the corresponding category bound to it.
what this code does is:
- append a
g
element to thesvg
element - for each item in the data array:
- add a rect element to the group
- bind the item to the rect element
- set the x and y coordinates of the rect element
- set the width and height of the rect element
- set the fill color for the rect element
svg.append('g')
.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', d => categoriesScale(d.category))
.attr('width', categoriesScale.bandwidth)
.attr('height', d => height - margin.bottom - valuesScale(d.value))
.attr('y', d => valuesScale(d.value))
.attr('fill', d => colorScale(d => d.category))
for the x coordinate, the callback passed as the second argument to .attr()
receives as an argument the item of data that it is working with. e.g. {category: ‘football’, value: 9}
. this allows us to extract the category and use this to obtain the correct xCoordinate from the categoriesScale. we do something similar for the y coordinate using valuesScale. the width of each bar is the same and can be obtained from the bandwidth property of categoriesScale.
the setting of the height and fill attributes require special attention
it might be expected that calling valuesScale(d.value) would produce the height in pixels of the bar. however, it’s not as simple as this because of how we previously configured the range of the scale. instead, the height of the bar is actually the height of the container minus the value returned by the scale, minus the bottom margin.
we use another type of scale to calculate colors for the bars: an ordinal scale. this is somewhat similar to a Map where the keys are the category names and the values are different colors (supplied by d3.schemeCategory10).
const colorScale = d3.scaleOrdinal()
.domain(data.map(d => d.category))
.range(d3.schemeCategory10)
Axes
the last thing we need to do is add in the x and y axes.
first, we create them using d3 functions, passing in the previously created scales:
const xAxis = d3.axisBottom(categoriesScale)
const yAxis = d3.axisLeft(valuesScale)
then we attach them to the DOM whilst applying translations to push them into place
svg.append('g')
.attr('transform', `translate(0, ${height - margin.bottom})`)
.call(xAxis);
svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(yAxis)
the x axis is positioned at the top of the container and has to be pushed down to the level of the bottom margin, whilst the y axis is positioned at the far left of the container and needs to be shifted rightwards to the position of the left margin