Wednesday, 12 February 2014

Making a bar chart in d3.js

The following post is a portion of the D3 Tips and Tricks book which is free to download. To use this post in context, consider it with the others in the blog or just download the the book as a pdf / epub or mobi .
----------------------------------------------------------

d3.js Bar Charts

A bar chart is a visual representation using either horizontal or vertical bars to show comparisons between discrete categories. There are a number of variations of bar charts including stacked, grouped, horizontal and vertical.
There is a wealth of examples of bar charts on the web, but I would recommend a visit to the D3.js gallery maintained by Christophe Viau as a starting point to get some ideas.
We will work through a simple vertical bar chart that uses a value on the y axis and date values on the x axis.
The end result will look like this;
Bar chart

The data

The data for this example will be sourced from an external csv file named bar-data.csv. It consists of a column of dates in year-month format and it’s contents are as follows;
date,value
2013-01,53
2013-02,165
2013-03,269
2013-04,344
2013-05,376
2013-06,410
2013-07,421
2013-08,405
2013-09,376
2013-10,359
2013-11,392
2013-12,433
2014-01,455
2014-02,478

The code

The full code listing for the example we are going to work through is as follows;
<!DOCTYPE html>
<meta charset="utf-8">

<head>
 <style>

 .axis {
   font: 10px sans-serif;
 }

 .axis path,
 .axis line {
   fill: none;
   stroke: #000;
   shape-rendering: crispEdges;
 }

 </style>
</head>

<body>
 
<script src="http://d3js.org/d3.v3.min.js"></script>

<script>

var margin = {top: 20, right: 20, bottom: 70, left: 40},
    width = 600 - margin.left - margin.right,
    height = 300 - margin.top - margin.bottom;

// Parse the date / time
var parseDate = d3.time.format("%Y-%m").parse;

var x = d3.scale.ordinal().rangeRoundBands([0, width], .05);

var y = d3.scale.linear().range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .tickFormat(d3.time.format("%Y-%m"));

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .ticks(10);

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", 
          "translate(" + margin.left + "," + margin.top + ")");

d3.csv("bar-data.csv", function(error, data) {

    data.forEach(function(d) {
        d.date = parseDate(d.date);
        d.value = +d.value;
    });
 
  x.domain(data.map(function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.value; })]);

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis)
    .selectAll("text")
      .style("text-anchor", "end")
      .attr("dx", "-.8em")
      .attr("dy", "-.55em")
      .attr("transform", "rotate(-90)" );

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Value ($)");

  svg.selectAll("bar")
      .data(data)
    .enter().append("rect")
      .style("fill", "steelblue")
      .attr("x", function(d) { return x(d.date); })
      .attr("width", x.rangeBand())
      .attr("y", function(d) { return y(d.value); })
      .attr("height", function(d) { return height - y(d.value); });

});

</script>

</body>
The full code for this example can be found on github, in the appendices of this book or in the code samples bundled with this book (bar.html and bar-data.csv). A working example can be found on bl.ocks.org.

The bar chart explained

In the course of describing the operation of the file I will gloss over the aspects of the structure of an HTML file which have already been described at the start of the book. Likewise, aspects of the JavaScript functions that have already been covered will only be briefly explained.
The start of the file deals with setting up the document’s head and body, loading the d3.js script and setting up the css in the <style> section.
The css section sets styling for the axes. It sizes the font to be used and make sure the lines are formatted appropriately.
 .axis {
   font: 10px sans-serif;
 }

 .axis path,
 .axis line {
   fill: none;
   stroke: #000;
   shape-rendering: crispEdges;
 }
Then our JavaScript section starts and the first thing that happens is that we set the size of the area that we’re going to use for the chart and the margins;
var margin = {top: 20, right: 20, bottom: 70, left: 40},
    width = 600 - margin.left - margin.right,
    height = 300 - margin.top - margin.bottom;
The next section of our code includes some of the functions that will be called from the main body of the code.
We have a familiar parseDate function with a slight twist. Since our source data for the date is made up of only the year and month, these are the only two portions of the date that need to be recognised;
var parseDate = d3.time.format("%Y-%m").parse;
The next section declares the function to determine positioning in the x domain.
var x = d3.scale.ordinal().rangeRoundBands([0, width], .05);
The ordinal scale is used to describe a range of discrete values. In our case they are a set of monthly values. The rangeRoundBands operator provides the magic that arranges our bars in a graceful way across the x axis. In our example we use it to set the range that our bars will cover (in this case from 0 to the width of the graph) and the amount of padding between the bars (in this case we have selected .05 which equates to approximately (depending on the number of pixels available) 5% of the bar width.
The function to set the scaling in the y domain is the same as most of our other graph examples;
var y = d3.scale.linear().range([height, 0]);
The declarations for our two axes are relatively simple, with the only exception being to force the format of the labels for the x axis into a ‘year-month’ format.
var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .tickFormat(d3.time.format("%Y-%m"));

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .ticks(10);
The next block of code selects the body on the web page and appends an svg object to it of the size that we have set up with our widthheight and margin’s.
var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", 
          "translate(" + margin.left + "," + margin.top + ")");
It also adds a g element that provides a reference point for adding our axes.
Then we begin the main body of our JavaScript. We load our csv file and then loop through it making sure that the dates and numerical values are recognised correctly;
d3.csv("bar-data.csv", function(error, data) {

    data.forEach(function(d) {
        d.date = parseDate(d.date);
        d.value = +d.value;
    });
We then then work through our x and y data and ensure that it is scaled to the domains we are working in;
  x.domain(data.map(function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.value; })]);
Following that we append our x axis;
  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis)
    .selectAll("text")
      .style("text-anchor", "end")
      .attr("dx", "-.8em")
      .attr("dy", "-.55em")
      .attr("transform", "rotate(-90)" );
This is placed in the correct position .attr("transform", "translate(0," + height + ")") and the text is positioned (using dx and dy) and rotated (.attr("transform", "rotate(-90)" );) so that it is aligned vertically.
Then we append our y axis in a similar way and append a label (.text("Value ($)"););
  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Value ($)");
Lastly we add the bars to our chart;
  svg.selectAll("bar")
      .data(data)
    .enter().append("rect")
      .style("fill", "steelblue")
      .attr("x", function(d) { return x(d.date); })
      .attr("width", x.rangeBand())
      .attr("y", function(d) { return y(d.value); })
      .attr("height", function(d) { return height - y(d.value); });
This block of code creates the bars (selectAll("bar")) and associates each of them with a data set (.data(data)).
We then append a rectangle (.append("rect")) with values for x/y position and height/width as configured in our earlier code.
The end result is our pretty looking bar chart;
Bar chart


The description above (and heaps of other stuff) is in the D3 Tips and Tricks book that can be downloaded for free (or donate if you really want to :-)).

17 comments:

  1. Hi, I made myself crazy trying to work through this example. I couldn't get it to work. I was using our shared copy of D3, version 3.0.4. It would break in the second to last section just before the bars are added. I pointed to http://d3js.org/d3.v3.js, version 3.4.4 and it worked. I point this out to perhaps save someone hours beating your head against a wall.

    ReplyDelete
    Replies
    1. Thanks for sharing. Much appreciated and well done.

      Delete
  2. When the first bar is tall enough it runs over the rotated "Value ($)" label. How can this label be moved to the left side of the y-axis to prevent this issue? Thanks.

    ReplyDelete
    Replies
    1. You can change the position of the label by adding in a .attr("x", "-10") line or something similar to the block that prints that label. Have a play with the values in the code to see what you can make it do. The x axis label block has both x and y movement you might also play with to see how it changes. Have fun.

      Delete
  3. Hi there, how would you get two of these charts using different csv files as data to display on one html page?

    ReplyDelete
  4. Hi there, is there any way that you could have two of these charts to display on one html page as I am having difficulty in doing so.

    ReplyDelete
    Replies
    1. I reccomend using bootstrap, but there are a couple of different ways. Read here for an outdated guide to using bootstrap with d3.js https://leanpub.com/D3-Tips-and-Tricks/read#leanpub-auto-using-bootstrap-with-d3js using with version 3 is pretty much the same, but they just call the spans something different

      Delete
  5. Thanks for this tutorial.

    I don't have data for each day, is it possible to plot dates without data to have a linear scale on the time?

    ReplyDelete
    Replies
    1. That's a good question. I found muself asking a similar question a while back. check out the resolution here; https://leanpub.com/D3-Tips-and-Tricks/read#leanpub-auto-padding-for-zero-values

      Delete
  6. is it possible to have a grouped bar chart with different colors for each part of a group and a line showing a target value?
    the data would look something like this:

    Date, value1, value2, value3
    1/1/2001, 55,33,91
    1/8/2001, 45,44,92
    1/15/2001, 35,55,33
    Target, 80

    The target could be 80,80,80 if it needs to be, I would like it to be a single line across the bars

    ReplyDelete
    Replies
    1. Apologies for the really late reply. I think you can achieve what you're looking for here with a standard grouped bar chart (https://bl.ocks.org/mbostock/3887051) and by appending a line over the top. Sorry, I've never actually made a grouped bar chart with d3, so I don't have my own example to share.

      Delete
  7. How can I add images over the bars?

    ReplyDelete
    Replies
    1. That's a good question. Firstly you will want to have the appropriate image associated with the appropriate data. (in the csv file for instance). Then while appending the specific bars you will want to associate the images and append as necessary. The closest example I have is one I've done with a tree diagram (http://bl.ocks.org/d3noob/9662ab6d5ac823c0e444). You should be able to make it happen with some experimentation and study of the code for both examples. Remember, take it easy when experimenting. Start with a really simple example (say one image) and build from there.

      Delete
  8. I am using SPA, angularjs(ui-router), d3.js, MVS/ASPNET. The chart is displayed on the view using angular controller (ng-controller). When the view is changed, remainings of the chart stay in the view (SVG -black bars ). How could the chart be erased/destroyed when the view is changed ?

    ReplyDelete
    Replies
    1. Wow! Sorry for the late reply, and sorry that I can't really help answer your question. Half the acronyms you used there I'm not familiar with. It's really out of my experience I'm afraid.

      Delete
  9. i am little bit confuse about where i have to put the CSV file or CSV data for getting the data as charts

    ReplyDelete
    Replies
    1. The simplest answer for this example is to put the bar.html file in the same directory as the bar-data.csv file. In practice they can be in different directories and you can adjust the `d3.csv("bar-data.csv", function(error, data) {` line to pick up the data from somewhere different. For example if you had your bar-data.csv file in a directory called `data` in the same location as your bar.html file you could access it by changing the line to;
      d3.csv("data/bar-data.csv", function(error, data) {
      I hope that made sense.

      Delete