Advanced Bar Chart in D3.js v.5

Or should I say more advanced than the construction from the previous post. This part of the tutorial will cover scales and axes. Let the fun begin!

The goal of today

Document Preparation

I will base my chart on the same data set as in the previous tutorial, a simple 2-column file containing some unspecified numeric information about a group of cats. Let’s kick things off by creating bar_chart.html and data.csv files. The html document will hold the DOM structure, and the comma separated text file is the data set. The html file is almost an exact copy of our end result from last week – with the exception of added placeholders for the scales and axes. Paste this to the html file, save, and reload the page:

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Bar Chart</title>
<script type="text/javascript" src="https://d3js.org/d3.v5.min.js"></script>
<style></style>
</head>
<body>
<div id="container" class="svg-container"></div>
<script>

//--------------------------PREPARATION--------------------------//
//------------------------SVG PREPARATION------------------------//
var width = 960;
var height = 500;
var margin = 5;
var padding = 5;
var adj = 20;
// we are appending SVG first
var svg = d3.select("div#container").append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
//.attr("viewBox", "-20 -20 1600 1600")
.attr("viewBox", "-" + adj + " -"+ adj + " " + (width + adj) + " " + (height + adj*2))
.style("padding", padding)
.style("margin", margin)
.classed("svg-content", true);

//-----------------------SCALES PREPARATION----------------------//

//------------------------DATA PREPARATION-----------------------//
var dataset = d3.csv("datas.csv");
dataset.then(function(data) {
    data.map(function(d) {
            d.val = +d.val;
            return d;});
});
console.log(dataset);
//----------------------------DRAWING----------------------------//
//-----------------------------AXES------------------------------//

//-----------------------------BARS------------------------------//
dataset.then(function(data) {  
    svg.selectAll("div")
    .data(data)
    .enter()
    .append("rect")
    .attr("class", "bar")
    .attr("x", function (d, i) {
        for (i>0; i < data.length; i++) {
            return i * 21;
        }
    })
    .attr("y", function (d) {
        return height - (d.val*10);
    })
    .attr("width", 20)
    .attr("height", function (d) {
        return d.val*10;
    });
});
</script>
</body>

Paste the following rows to data.csv:

cat,val
Phillip,10
Rita,12
Tom,20
Oscar,19
Lulu,8
Keko,14
Lena,9

If the copy-paste went according to the plan, you should see a modest bar chart appear in your browser:

A very modest bar chart

Scales Preparation

The big reason behind creating scales is so that the chart fits the browser’s screen without manually hacking the data values (like multiplying them so they stretch across the screen). D3.js comes with a couple of built-in scales that you can choose from. There are 3 basic steps to keep in mind when designing the scales:

  • What type of data are we dealing with? Is it categorical data or is it continuous? Are these points in time?
  • What is the data input? If the data is categorical: what is the domain array? If it’s continuous: what are the minimum and maximum values?
  • What is the data output, i.e. what should the input values be translated to?

Let’s review those questions against our data set. The first column of data.csv holds the cat names. Names, just like any sort of labels or categories, are classified as nominal data. There is no quantitative value that this data type provides. There are a number of good scale choices for categorical data: ordinal scale and band scale being on top of the list. Band scale is typically used for bar charts in D3.js because it translates the categories into numeric (continuous) outputs and have built-in magical width adjustment capabilities. In the case of our cats, the input domain are the cat names (arranged in the order of rows) and the output domain should be translated to anywhere from the left corner of the svg to the svg‘s width.

The value column in the csv file is constituted by numbers which in turn can be represented on a continuous scale. There are good D3.js choices for continuous sets as well: the most common for a bar chart being the linear scale. The input domain are all the values present in the csv file, from the lowest (8) to the highest (20). They will be translated on the vertical plane of the svg (0, height).

With those parameters in mind, let’s construct the scales. Add the following lines to the document under the Scales Preparation section:

var xScale = d3.scaleBand()
    .rangeRound([0, width])
    .paddingInner(0.05);
var yScale = d3.scaleLinear()
    .rangeRound([0, height]);

The xScale will take care of our cats, assign each a band, and place it in identical intervals on the x axis. I’ve also set the padding property so that the bands never touch each other. The yScale will translate the numeric values on the y axis.

Let’s now specify the data domains: this part should act on the dataset promise. The xScale will take the whole cats array as its domain. For the linear yScale we have to provide the minimum and maximum values. Add the following lines to the Data Preparation section, right after the data types are set up.

dataset.then(function(data) {  
    xScale.domain(data.map(function(d) {return d.cat}))
    yScale.domain([0, d3.max(data, function(d)
                    {return d.val; })]);
});

After this preparation, let’s scale our input data. To achieve this we simply call the appropriate scale when the data is plotted. Amend the Bars section to include the following code:

//--------------------------BARS--------------------------//   
 dataset.then(function (data) {
    svg.selectAll("div")
    .data(data)
    .enter().append("rect")
    .attr("class", "bar")
    .attr("x", function(d){
        return xScale(d.cat);
    })
    .attr("y", function (d) {
        return yScale(d.val);
    })
    .attr("width", xScale.bandwidth())
    .attr("height", function (d) {
        return height - yScale(d.val);
    });
});

We call the scales exactly 4 times on each rectangle’s properties: to transform its x and y starting coordinates, to translate its height, and to assign its width with the bandwidth() method. Note how the band scale allowed us to get rid of plotting the nominal data by its index: it created the bands right from the categorical data that we fed to it.

Save the document and refresh the page!

Nice and scaled bar chart

That’s one prominent bar chart! But wait, what happened to the values? Why is bar #3 – Tom’s value – set to 0 out of a sudden?

This unfortunate graph is a natural result of our current translation logic. Remember how we convert a domain to a range: we cast the input points’ domain (0, 20) to a way wider range of (0, height). Which means that the maximum value of the domain – the value associated with Tom – will become the maximum of the range: the svg‘s height. That’s how our 20 became 500! To have the bars behave the way we intended, we need to revert the range. Change var yScale = d3.scaleLinear().rangeRound([0, height]); to var yScale = d3.scaleLinear().rangeRound([height,0]); and refresh the page:

The graph gets prettier with every step!

Now we are talking!

Let’s complete the chart by adding x and y axes to it. Axes are D3 objects. You plot them by simply appending them to the svg and, at the very least, specifying their origin and tick orientation. Paste the following to the Axes part of the html document:

dataset.then(function(data) {
    svg.append("g")
        .attr("class", "axis")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(xScale));

    svg.append("g")
        .attr("class", "axis")
        .call(d3.axisLeft(yScale));
});

The axes will be contained by their own g elements. By default the axis are plotted at the svg‘s origin: i.e. (0,0). To plot an x axis on the bottom we need to move it there. Note how I’m using a transform attribute to draw it at svg‘s height. The axis constructors such as d3.axisBottom() and d3.axisLeft() simply decide the tick orientation.

Save the document and refresh the page. Et voila, the axes appear!

Axes have joined the (bar) club

We have achieved our goals for today: a nicely scaled graph along with 2 handsome axes. Before we bid farewell, I would like to reinforce the beauty of this design by running a quick experiment.

The whole reason to add the scales was to tame the data: make sure it shows nicely on our available screen real estate. That means that if we add more data points to our set, the configuration will be able to immediately handle them and the graph will adjust accordingly.

Try it out: amend data.csv by adding another cat to our gang:

cat,val
Phillip,10
Rita,12
Tom,20
Oscar,19
Lulu,8
Keko,14
Lena,9
Felix, 29

Save the file, and refresh the screen. See how the graph adjusts itself and rejoice!

Felix has joined the gang

That’s all for today! Let me know how you liked the tutorial and be sure to point out any mistakes. For the next time I’m planning something super special: a nice package full of graphs to download. I promise you the wait will be worth it!

Code Samples

bar_chart.html:

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Bar Chart</title>
<script type="text/javascript" src="https://d3js.org/d3.v5.min.js"></script>
<style></style>
</head>
<body>
<div id="container" class="svg-container"></div>
<script>

//--------------------------PREPARATION--------------------------//
//------------------------SVG PREPARATION------------------------//
var width = 960;
var height = 500;
var margin = 5;
var padding = 5;
var adj = 20;
// we are appending SVG first
var svg = d3.select("div#container").append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
//.attr("viewBox", "-20 -20 1600 1600")
.attr("viewBox", "-" + adj + " -"+ adj + " " + (width + adj) + " " + (height + adj*2))
.style("padding", padding)
.style("margin", margin)
.classed("svg-content", true);

//-----------------------SCALES PREPARATION----------------------//
var xScale = d3.scaleBand()
    .rangeRound([0, width])
    .paddingInner(0.05);
var yScale = d3.scaleLinear()
    .rangeRound([height, 0]);

//------------------------DATA PREPARATION-----------------------//
var dataset = d3.csv("data.csv");
dataset.then(function(data) {
    data.map(function(d) {
            d.val = +d.val;
            return d;});
});

dataset.then(function(data) {  
    xScale.domain(data.map(function(d) {return d.cat}))
    yScale.domain([0, d3.max(data, function(d)
                             {return d.val; })]);
});

console.log(dataset);

//----------------------------DRAWING----------------------------//
//-----------------------------AXES------------------------------//
svg.append("g")
    .attr("class", "axis")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(xScale));

svg.append("g")
    .attr("class", "axis")
    .call(d3.axisLeft(yScale));
});

//-----------------------------BARS------------------------------//
dataset.then(function (data) { 
    svg.selectAll("div")
    .data(data)
    .enter().append("rect")
    .attr("class", "bar")
    .attr("x", function(d){
        return xScale(d.cat);
    })
    .attr("y", function (d) {
        return yScale(d.val);
    })
    .attr("width", xScale.bandwidth())
    .attr("height", function (d) {
        return height - yScale(d.val);
    });
});

</script>
</body>

data.csv:

cat,val
Phillip,10
Rita,12
Tom,20
Oscar,19
Lulu,8
Keko,14
Lena,9
Felix, 29