Simple Bar Chart in D3.js v.5

This is actually happening! I’ve put myself together (the key to more time is less Netflix, people) and wrote up a couple of examples in D3.js version 5 (yes, version 5!) that should get people started in the transition over to the tricky number 5. The guide assumes that you have some basics in D3 (you have an idea about SVG, DOM, HTML, and CSS), or better yet that you come from an earlier version. In this chapter we’ll create a simple bar chart. The objectives of the day are: data upload from a csv, data format setup, and drawing the data. As basic as this! Next time we will tackle scales and grids.

Make sure to check out my library for more fun examples!

The simplest of all bar charts

Document Setup

We need something to plot our drawing onto: let’s create an html document and call it simple_graphs.html. There is a hint in the name: beware our visualisation will be very simplistic and its main goal is to explain the D3 logic, not to enchant the viewer.

Paste this into simple_graphs.html:

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Simple 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>

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

//-----------------------DATA PREPARATION------------------------//

//---------------------------BAR CHART---------------------------//


</script>
</body>

This snippet is a simple webpage setup. In the head, we declare the document as html, give it a name, and point to the D3.js version 5 library. Document’s body starts at line 8 and as of now only includes two objects: a div element of class svg-container – that will hold our svg, and the svg appended to that div. Alternatively, you can declare an svg (like <svg width=“960” height=“600”>) and call it later. Attaching an svg to a div gives you more control over its placing, so I usually do it this way (even if rather unnecessary in our example).

The svg section holds the padding and margin information and sets the scaling of the element, so that the svg takes all available space. If you’d like to know more, I wrote about the viewBox property (and its friends) in my post Creating fun shapes in D3.js.

Data Preparation

Reading data onto the document will be our first milestone. The data set I created for this example is only two columns and 8 rows long: it holds some mysterious information about a group of cats. The cats.csv file looks like this:

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

Create the file cats.csv in the same directory as the html and css documents.

We read the data by using the built-in d3.csv() module. Paste the following under the Data Preparation section of the html document, reload the webpage, and observe the console:

var dataset = d3.csv("data.csv");
console.log(dataset);

Congratulations! The data is now accessible to the D3 magic. The console prints the array’s information and its values:

Console promise

We are promised an array of 7 cats

You will notice that the values were recognised as strings. That’s correct for the cat names, but the values need to be read as numbers. Let’s make the number conversion our next task. To do so, we need to access the data in the dataset object. Paste the following in the Data Preparation section:

var dataset = d3.csv("data.csv");
dataset.then(function(data) {
    data.map(function(d) {
            d.val = +d.val;
            return d;});
});
console.log(dataset);

In the above example we call the dataset object and run a function on every element of it. Note the syntax of our new function: specifically, consider the role of the promise.then() method. The method can be abstracted to [promise].then(onFulfilled[, onRejected]): this is to say that then() always acts on a promise, and if such is fulfilled (e.g. there is an array of data to call), it proceeds to call a function on it. Eh? It means that our dataset array is only accessed in this step: JavaScript does not need to load it before it’s actually used. It makes the code lighter and the browser’s happier. The function also allows for specifying a response if a promise is not resolved. Take a look at my intro to Promise syntax in D3.js for some more syntax examples.

There are more fun facts about the then() method, and one of them is that then() returns a promise as well. Yes, it did it again! I promise we will come back to this promise when printing the data onto our svg.

Let’s come back to our block of code. On the successful resolution of the dataset promise we will run a function that takes one argument – that is, data. The function will load the data set in and we will access it with a super useful method called map(). The method calls a function on every element of the provided array (data). We have to be careful here as our data set contains more than one data format. We can specify how we want each column to be treated by directly accessing the column names. The values (val) will be converted to numeric.

Save the file, reload the page, and verify the console log: Console cats

Each cat has a numeric value assigned

The data got converted like a charm! Note that again we are returning the dataset promise – and because we have called a function on it, it got amended! The initial file is no longer accessible!

Now we are on a straight road to success. We have the data merrily transformed and ready to use. We can proceed with plotting it! You will notice this is almost no different to the earlier versions of the D3 library.

Graph Drawing: Bar Chart

As the first plot example, we will tackle a simple bar chart. It will be so ugly that I hope it will motivate you to make it nicer in the due course.

Let’s think about what a bar chart is made of. In a nutshell is just a bunch of rectangles aligned vertically or horizontally. Our data set will allow us to make exactly 4 bars. A rectangle is characterised by four properties: the starting point (x and y), i.e. the coords of the top left corner of each bar, the height, and the width. Let’s get to work – for starters, paste the following to the Bar Chart section:

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;
        }
    })
    .attr("y", function (d) {
        return height - d.val;
    })
    .attr("width", 20)
    .attr("height", function (d) {
        return d.val;
    });
});

Again we are calling the dataset promise, but this time the promise resolves with the data we modified in the previous section. On a successful fulfillment, we call a function that builds a bar chart. The function definition should be something you are used to from the previous D3 versions, but let’s summarise it:

The function first selects all empty div elements from the svg (of which there are infinite; with this we allow each rectangle to be appended to a separate div later), calls our promised (and delivered) data, and appends as many rectangles as there are data array elements. We then proceed to specifying the properties I mentioned earlier.

Attribute x is each rectangle’s starting point on the horizontal axis. Each bar needs a position on the x axis. In our example it is the categories – so our cats – that are plotted on the x axis. But they are no good! Cat names are meaningless in a context of a 2D plane: we need a numeric coordinate. Because we didn’t introduce scales here, we have to hack the plot a little bit. Instead of cat names, we will use their indices to locate the bars on the axis. See how i in function(d,i) stands for index.

Attribute y decides the starting point of each bar on the vertical axis. I envisioned my graph with bars aligned in their base line. Remember that the top left corner of an svg is set at point (0,0) and it’s left max is at (svg height,0). To plot a rectangle, we specify its top left position and draw a line downwards (that’s the height).

To achieve my design, we need to setup the correct y value and height of each rectangle. We will start plotting the rectangles at (height – val, x) point. In layman terms, if our svg’s height is y = 10 pixels and the bar’s height is y = 8 pixels, we start drawing the bar at y = 2 pixels so that the line of 8 pixels is drawn downwards from that position and finishes correctly at the svg’s height.

After those mind boggling coordinate calculations, the only properties that are unset are the rectangle’s height and width. We decide a common width for all bars, and the height is set at the data value that we want to visualise.

All is set*: save the file and observe the webpage.

*I made a sneaky mistake in specifying one of the attributes. Try to catch it before reloading the page!

Where did the rectangles go?

How disappointing! All this work to create a sad little blob.

I suggest you try fixing the code on your own before reading on.

The issue was caused by our good intentions: remember that our x attribute is set to the row indices instead of dates. That means the first bar is plotted at x = 0px, and the following bar at x = 1px. Since our bar width is set to 20 pixels, those poor rectangles sit on top of each other. Let’s separate them. And while at it, let’s adjust the size of the graph so the rectangles are prominent and strong (like if this data actually had a meaning).

Hiding rectangles

The rectangles were hiding!

Replace the previous code with the following snippet:

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;
    });
});

Note how I multiplied the indices by 21 so that the first bar is still printed at 0px, but the following rectangle starts at 121, so 21px, and the next one at 221, so 42px. I also stretched the bars by multiplying their height by 10.

Cats can now be compared

Here it is in its full ugliness!

Let me know in the comments how you got on and if you enjoyed the tutorial. Check out my next post to see how to add scales and axes to our visualisation.

Here’s the full code:

simple_graphs.html:

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Simple 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>

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

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

//---------------------------BAR CHART---------------------------//
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;
    });
});

cats.csv:

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