D3 is a very robust library that will help you create a great spectre of visualizations, if you don’t know where to start you should take a look to the gallery. Stackoverflow has a D3 tag and in the last couple months I was able to answer three questions regarding Grouped Bar charts. In this post I will try to explain as best as possible how to create a Grouped Bar chart from the ground up.

Data visualization is one of the main jobs I have at my workplace, if you’ve never worked with data visualizations you should try it, it’s kinda “soothing”. In the beginning when we needed simple charts we opted to use libraries such as Highcharts and C3.js, as we developed more complex UIs we ended up trying out D3.js.

If you need a quick introduction to d3 I find this tutorial very helpful and easy to grasp.

To make things a little more visual, code snippets will be followed by the result of the given code via jsFiddle. The full code will be using webpack as asset building tool, you can find the full source code here.

Understanding our dataset

Movie ratings are perfect for grouped bar charts, each movie has a different rating from a wide pool of critics. Comparing the ratings should be simple, we will be using a simple dataset with the following object structure:

Dataset Object Structure
1
2
3
4
5
6
7
8
{
"key": "Sausage Party",
"values": [
{ "key": "all", "value": 0.82 },
{ "key": "top", "value": 0.86 },
{ "key": "audience", "value": 0.66 }
]
}

We have an array of movie objects, each object has an array of values which contains the three ratings taken from Rotten Tomatoes. Each of the ratings will be inside the [0, 100] range, 0 being the lowest possible rating and 100 being the highest possible one. The all rating corresponds to the whole group of critics, top just takes into account the top critics and audience is the rating that the audience grants to the movie. The chart will group the ratings by movie and display them in a simple way so the information can be understood really quick. In the end we will have a chart like the one below.

Sausage Party Suicide Squad Star Trek Beyond The Nice Guys Ghostbusters Don't Think Twice 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 All Critics Top Critics Audience

That being said, lets begin, first we will need a simple HTML layout so we can start developing our project:

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<title>
Grouped Bar Charts with d3.js (Tutorial)
</title>
<link rel="stylesheet" type="text/css" href="main.css"></link>
</head>
<body>
<div id="grouped-bar" class="grouped-bar">
</div>
<script src="main.js" type="text/javascript" language="JavaScript"></script>
</body>
</html>

Creating our chart

We will be using ES6 and the new modular d3 which lets us import just the necessary modules from the library. At this moment we just need to render a plain svg into the DOM, importing the select library will allow us to transform the DOM. To understand a little bit more lets dig into the selection library API docs, specially into the “Selecting Elements“ paragraph.

Selection methods accept W3C selector strings such as .fancy to select elements with the class fancy, or div to select DIV elements. Selection methods come in two forms: select and selectAll: the former selects only the first matching element, while the latter selects all matching elements in document order.

In the following snippet we are creating just the basic stuff we will need for our chart:

  1. Importing select library from the d3 package.
  2. Define the SVG dimensions.
  3. Use the select library to transform the DOM by selecting our node and appending a new SVG one with certain modifications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. Importing select library from the d3 package.
import { select } from 'd3'
// 2. Define the SVG dimensions.
const margin = {
top: 25,
right: 25,
bottom: 25,
left: 25
}
const height = 310;
const width = 650;
// 3. Use the select library to transform the DOM by selecting our node and
// appending a new SVG one with certain modifications
const svg = select('#grouped-bar') // Using the id of the div in our html template
.append('svg')
.attr('class', 'grouped-bars-svg')
.attr('height', height)
.attr('width', width)
.style('background', '#fff');

The jsFiddle shows us how we can transform the DOM with the select library API. We are using some methods provided by the select library, append and attr. The first method will take as argument a string and will append the tag into the selected node, in the jsFiddle you will be able to check that our #grouped-bar element has a new node, and that node is a SVG. If you check even further in the SVG tag you will find that some attributes were defined, those attributes were the ones we defined in our transformation by using the attr method.

Now that we have a SVG in which we can render our information, lets add a g element into our SVG node. This element will contain all the elements that we will build in our chart. We will also need a way to color our bars in the near future so lets also add an object that will contain the mappings of our colors to each given rating. Finally, lets make a request to obtain the dataset that we will be using in this example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. Create an object with the color mappings rating_key => color
const colors = {
all: '#CFDFA9',
top: '#F2DA8E',
audience: '#EA9485'
}
// 2. Append a g element into our SVG
const main = svg.append('g')
.attr('class', 'grouped-bars-main-group')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Append a rect so we can see how the margin will work
main.append('rect')
.attr('width', width - margin.left - margin.right)
.attr('height', height - margin.top - margin.bottom)
.attr('fill', 'none')
.attr('stroke', '#222');
// 3. Make a request to obtain our dataset
fetch('https://....')
.then(res => { return res.json() }) // Parse response
.then(renderGraph) // Call our chart function
.catch(err => { console.log('Error...', err) });
// 4. Create a function to render our chart
function renderGraph(data) {
// TODO
}

Right now we have a SVG with a g element which has rect as child node. We transformed our g tag a little, we gave it a class name .grouped-bars-main-group and made a simple transformation. The g tag was translated in the x position by the left margin variable and in the y position by the top margin variable. This will make every new node appended into this element to start at the 0 , 0 position of the group element, and since the group element was translated all our child nodes will be restricted by the margin we provided. The rect inside the group element is a clear example of this, we just appended the element into our group element and since it will start rendering from the initial position of the parent group it will be respecting the SVG margin we declared.

Creating scales

Ok, we have an SVG and a group element, so what should we do next?. We need to declare scales in order to render our dataset in a proper way. D3 provides functionality to create certain types of scales, we will need 3 scales to accomplish our chart. Below is a simple sketch of the scales we should be needing.

alt text

The first scale we will check is the scaleBand, this scale is part of the ordinal scales that d3 offers. From the d3 docs we can find this definition:

Unlike continuous scales, ordinal scales have a discrete domain and range. For example, an ordinal scale might map a set of named categories to a set of colors, or determine the horizontal positions of columns in a column chart

https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales

This definition fits right into our use case, we need to map a set of categories to the correct position in our x axis. The scaleBand has a more specific definition:

Band scales are like ordinal scales except the output range is continuous and numeric. Discrete output values are automatically computed by the scale by dividing the continuous range into uniform bands.

https://github.com/d3/d3-scale/blob/master/README.md#band-scales

The second scale we will review is the linear scale:

Constructs a new continuous scale with the unit domain [0, 1], the unit range [0, 1], the default interpolator and clamping disabled. Linear scales are a good default choice for continuous quantitative data because they preserve proportional differences. Each range value y can be expressed as a function of the domain value x: y = mx + b.

https://github.com/d3/d3-scale/blob/master/README.md#linear-scales

This type of scale will be useful to map our rating’s values, we want to get a rating and output a value within the range we provide. Now lets start constructing our scales.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. Add scale imports
import { select, scaleLinear, scaleBand } from 'd3'
// ...
// 2. Create scales with range and domain
// xDomain will be defined by all the movie names in our dataset
const xDomain = data.map(movie => { return movie.key });
// xScale's domain will be our previously configured xDomain
const xScale = scaleBand().domain(xDomain);
// xScale's range will be the width of our chart (minus the margin constraints)
xScale.range([0, width - margin.left - margin.right]);
// yScale will be a linear scale, with a domain [0, 1]
// This domain is defined like this since ratings go from 0 to 1
const yScale = scaleLinear().domain([0, 100]);
// yScale's range will be the height of our chart (minus the margin constraints)
yScale.range([height - margin.top - margin.bottom, 0]);
// ratingsScale's domain is just the array of ratings for a movie
const ratingsScale = scaleBand().domain(['all', 'top', 'audience']);
// ratingsScale's range will be the bandwidth of our xScale
ratingsScale.range([0, xScale.bandwidth()]);
// ratingsScale's options
ratingsScale.round(true).paddingInner(0.15).paddingOuter(0.95);

We created a xScale of type scaleBand, the purpose of using this type of scale is to correctly map our movie names to the corresponding x position. The domain of the xScale will be all our movie names, the range will be defined by the width of our SVG. Remember the definition given by the d3 API docs? well, our xScale will help us compute the uniform bands we need to segment our x axis. So, lets see what we can do with the xScale:

1
2
3
4
5
6
7
8
9
console.log(xScale('Sausage Party')); // 0
console.log(xScale('Suicide Squad')); // 91.66666666666667
console.log(xScale('Star Trek Beyond')); // 183.33333333333334
// ...
console.log(xScale('Don\'t Think Twice')); // 458.33333333333337
// ...
console.log(xScale('A movie not defined in our domain')); // undefined
// ...
console.log(xScale.bandwidth()); // 91.66666666666667

Our xScale correctly maps our movie names! You can see how calling our scale with Sausage Party returns 0, Suicide Squad returns 91.6 and so on. This is great, the scale will correctly return the x position in which our movie should render its information. When we try to map a value that doesn’t exist in our xScale we will receive undefined as the return value. Finally, if we want to know the width of the uniform bands of our scale we can simple call the bandwidth in our xScale.

The second scale we created is the yScale, this one will map our rating’s values (domain [0, 100]) to the range we provided (SVG height) and if we give it any input within the specified domain we should be able to get the y value as the output.

1
2
3
4
console.log(yScale(83)); // 44.20000000000002
console.log(yScale(0)); // 260
console.log(yScale(-1)); // 262.6
console.log(yScale(110)); // -26

The last scale (ratingsScale) is the one that will allow us to map our rating names to the corresponding x inside our movie’s elements. This scale will take as domain the types of ratings we have for each of our movies and for range the width of the uniform band of our xScale.

Axis Rendering

Axis make our chart information more readable, they help us understand the relationship between the data we are representing. We will need two axis in our chart, the x (Movies) and the y (Movie Rating Score). D3 has utilities that will reduce the hassle of creating all the logic behind the axis, by calling any of the axis provided by d3 (axisTop, axisRight, axisBottom, axisLeft) we will be able to render human-readable reference marks for our scales.

The movies axis will be our xAxis, using the axisBottom with our xScale as argument we will construct a new bottom-oriented axis with the movie names drawn below the horizontal domain path.

The rating’s axis will be our yAxis, using the axisLeft with our yScale as argument we will construct a new left-oriented axis with the rating’s values drawn to the left of the domain path.

Grid lines are an easy way to grasp really quick the values shown in charts, it’s a great visual aid. We will create a simple set of grid lines for our rating’s values. We will repeat the process of creating an axisLeft with the yScale as argument, but this time we will add the tickSize into the definition of this axis. Adding the tickSize will grant us the ability of modifying the size of the ticks rendered, so lets set the size to the width of the chart.

The axis and scales are correctly defined, the question is, how are we going to use them to render our information?. Remember the main variable we defined?, that variable has the reference to the g element with the margin we had defined previously, in this node is where we will append all our new nodes.

SVG elements do not support a z-index or similar property, everything is rendered in order one element above the other. We want our grid lines to be behind our bars and axis, so we have two options, append our elements in the order we want (the method we will use) or make use of the insert utility d3 provides. D3 also offers the call function which takes a selection as input and hands that selection off to any function, we will make use of this utility to create all our axis by giving the functions our newly appended g elements as inputs and as a functions ours axis functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Create an axis for our movies axis
const xAxis = axisBottom(xScale);
// Create an axis for our rating's values
const yAxis = axisLeft(yScale);
// Ugly way to create gridlines
const yGrid = axisLeft(yScale).tickSize(-width + margin.left + margin.right);
// Render gridlines
main.append('g')
.attr('class', 'grouped-bar-chart grid y-grid')
.attr('transform', `translate(0, 0)`)
.call(yGrid)
.selectAll('text') // Remove all the text elements from this g
.remove();
// Render xAxis - movie names
main.append('g')
.attr('class', 'grouped-bar-chart axis x-axis')
// Translate our x-axis to the bottom of our chart
.attr('transform', `translate(0, ${height - margin.top - margin.bottom})`)
.call(xAxis);
// Render yAxis - rating's values
main.append('g')
.attr('class', 'grouped-bar-chart axis y-axis')
.attr('transform', `translate(0, 0)`)
.call(yAxis);
Label Rendering

Labels will greatly increase the way users understand the chart, they will help to recognize what is the category of each of the ratings the movie has. Lets define a simple array which will be the data we provide to d3 to create our labels. In this snippet we find the use of the data method, which will allow us to connect data elements to the DOM, we’re going to create a selection and use .data() to bind our labels to the selection. The last instruction will help us create g elements that will be in charge of containing our label elements. A reference to the selection was created with the variable labels, this will let us append the desired elements for each element in our labels data array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 1. Define a simple array of labels
const labelsArr = [
{ key: 'all', name: 'All Critics' },
{ key: 'top', name: 'Top Critics' },
{ key: 'audience', name: 'Audience' }
];
// 2. Create the container of the labels
const labels = main.append('g')
.attr('class', 'grouped-bar-chart labels-container')
.attr('transform', `translate(${width - margin.left - margin.right}, 0)`)
.selectAll('.label-container') // Start data binding
.data(labelsArr) // bind with our labels array
.enter() // "for each entry"
.append('g') // append a g element
.attr('class', label => { // transform our node as we desire
return `label-container label-container--${dasherize(label.key)}`
});
// 3. For each .label-container append a rect
labels.append('rect')
.attr('fill', label => { return colors[label.key]; }) // use our color mappings
.attr('width', 10)
.attr('height', 10)
.attr('transform', (label, index) => {
return `translate(5, ${index * 20})` // translate each rect by index
});
// 3. For each .label-container append a text
labels.append('text')
.attr('transform', (label, index) => {
return `translate(18, ${index * 20})` // translate each rect by index
})
.attr('dy', '.75em')
.attr('font-size', '12px')
.text(label => { return label.name });
// ...
// Create a helper function to normalize our keys
function dasherize(str) {
return str.trim().replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
};
Bar Rendering

Scales, axis and labels were created in the previous snippets, we just need to render our bars with the help of all the elements mentioned before.

Lets create a g element which will contain all our bars and give it the data-container class. We will also be in need of data binding to connect our movie elements to the DOM and build a g element for each of our movies contained in our dataset. We will need to correctly transform the g elements so they translate to the correct position, this action will be managed by using the xScale and give it as argument the movie name. The chart now has g elements with the class movie-container movie-container--name-of-the-movie for each movie. The snippet will add some rects to the g elements classed movie-container, this is just to show how the xScale works.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. Create a g element to contain our bars
const dataContainer = main.append('g')
.attr('class', 'grouped-bar-chart data-container');
// 2. Create g elements with data binding to contain our rating's elements
const movies = dataContainer.selectAll('.movie-container')
.data(data)
.enter()
.append('g')
.attr('class', movie => {
return `movie-container movie-container--${dasherize(movie.key)}`
})
.attr('transform', movie => {
// Position the element with our xScale
const offset = xScale(movie.key);
return `translate(${offset}, 0)`;
});
// 3. Append reference rects
movies.append('rect')
.attr('width', xScale.bandwidth())
.attr('height', height - margin.top - margin.bottom)
.attr('fill', 'none')
.attr('stroke', '#000')
.attr('stroke-dasharray', '2,2');

If we inspect our jsFiddle result we will see the following structure, we can move on and render the rating’s values.

1
2
3
4
5
6
7
8
<g class="grouped-bar-chart data-container">
<g class="movie-container movie-container---sausage-party" transform="translate(0, 0)"></g>
<g class="movie-container movie-container---suicide-squad" transform="translate(91.66666666666667, 0)"></g>
<g class="movie-container movie-container---star-trek-beyond" transform="translate(183.33333333333334, 0)"></g>
<g class="movie-container movie-container---the-nice-guys" transform="translate(275, 0)"></g>
<g class="movie-container movie-container---ghostbusters" transform="translate(366.6666666666667, 0)"></g>
<g class="movie-container movie-container---don't-think-twice" transform="translate(458.33333333333337, 0)"></g>
</g>

Rating’s values must be rendered inside each of the movie’s containers. We will make use of our movies variable which currently holds the selection of every movie-container and use again data binding to connect the movie’s ratings values to the DOM and build a g element for each of our movie’s ratings, this elements will have the class rating-container. Inside of each of the rating-container elements we will append a rect which will represent the value of the current rating. The height of the rect will be calculated by subtracting the output we get from inputing the rating value into our yScale to the height. The width will be defined by the ratingsScale bandwidth, finally we use the ratingsScale to translate the rect in the x position and the yScale to translate it in the y position.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const ratings = movies.selectAll('.rating-container')
.data(movie => { return movie.values; })
.enter()
.append('g')
.attr('class', movie => {
return `rating-container rating-container--${dasherize(movie.key)}`
})
.append('rect')
.attr('height', rating => {
const h = height - margin.top - margin.bottom;
return h - yScale(rating.value);
})
.attr('width', ratingsScale.bandwidth())
.attr('transform', rating => {
const offset = ratingsScale(rating.key);
return `translate(${offset}, ${yScale(rating.value)})`;
})
.attr('fill', rating => { return colors[rating.key] });

We finally have our chart, I will cook up the next tutorial in how to add interaction to our chart. Hope you find this useful.