Thursday, 4 July 2013

Introduction to Bullet Charts 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 .
---------------------------------------------------------- 

Introduction to bullet chart structure

One of the first D3.js examples I ever came across (back when Protovis was the thing to use) was one with bullet charts (or bullet graphs).

It struck me straight away as an elegant way to represent data by providing direct information and context.


The Bullet Graph Design specification was laid down by Stephen Frew as part of his work with Perceptual Edge.

Using his specification we can break down the components of the chart as follows.

Quantitative scale:
A scale that is an analogue of the scale on the x axis of a two dimensional xy graph.
Performance measure:
The primary data being displayed. In this case the frequency of operation of a CPU.
Comparative marker:
A reference symbol designating a measurement such as the previous day's high value (or similar).
Qualitative ranges:
These represent ranges such as low medium and high or bad, satisfactory and good. Ideally there would be no fewer than two and no more than 5 of these (for the purposes of readability).
Understanding the specification for the chart is useful, because it's also reflected in the way that the data for the chart is structured.
For instance, If we take the current example, the data can be presented (in JSON) as follows;
 [
  {
    "title":"CPU 1 Load",
    "subtitle":"GHz",
    "ranges":[1500,2250,3000],
    "measures":[2200],
    "markers":[2500]
  }
]


Here we an see all the components for the chart laid out and it's these values that we will load into our D3 script to display.

D3.js code for bullet charts

We'll move through the explanation of the code in a similar process to the other examples in the book. Where there are areas that we have covered before, I will gloss over some details on the understanding that you will have already seen them explained in an earlier section (most likely the basic line graph example).

Here is the full code;

<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  margin: auto;
  padding-top: 40px;
  position: relative;
  width: 800px;
}

button {
  position: absolute;
  right: 40px;
  top: 10px;
}

.bullet { font: 10px sans-serif; }
.bullet .marker { stroke: #000; stroke-width: 2px; }
.bullet .tick line { stroke: #666; stroke-width: .5px; }
.bullet .range.s0 { fill: #eee; }
.bullet .range.s1 { fill: #ddd; }
.bullet .range.s2 { fill: #ccc; }
.bullet .measure.s0 { fill: steelblue; }
.bullet .title { font-size: 14px; font-weight: bold; }
.bullet .subtitle { fill: #999; }

</style>
<button>Update</button>
<script type="text/javascript" src="d3/d3.v3.js"></script>
<script src="js/bullet.js"></script>
<script>

var margin = {top: 5, right: 40, bottom: 20, left: 120},
    width = 800 - margin.left - margin.right,
    height = 50 - margin.top - margin.bottom;

var chart = d3.bullet()
    .width(width)
    .height(height);

d3.json("data/cpu1.json", function(error, data) {
  var svg = d3.select("body").selectAll("svg")
      .data(data)
    .enter().append("svg")
      .attr("class", "bullet")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
    .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .call(chart);

  var title = svg.append("g")
      .style("text-anchor", "end")
      .attr("transform", "translate(-6," + height / 2 + ")");

  title.append("text")
      .attr("class", "title")
      .text(function(d) { return d.title; });

  title.append("text")
      .attr("class", "subtitle")
      .attr("dy", "1em")
      .text(function(d) { return d.subtitle; });

  d3.selectAll("button").on("click", function() {
    svg.datum(randomize).call(chart.duration(1000));
  });
});

function randomize(d) {
  if (!d.randomizer) d.randomizer = randomizer(d);
  d.markers = d.markers.map(d.randomizer);
  d.measures = d.measures.map(d.randomizer);
  return d;
}

function randomizer(d) {
  var k = d3.max(d.ranges) * .2;
  return function(d) {
    return Math.max(0, d + k * (Math.random() - .5));
  };
}

</script>
</body>

This code is a derivative of one of Mike Bostock's blocks here. You can download it (and a data set with two bullet chart groups in it) from https://gist.github.com/d3noob/5886992. You can view an online version here.

It will become clearer in the process of going through the code below, but as a teaser, it is worth noting that while the code that we will modify is as presented above, we are employing a separate script `bullet.js` to enable the charts.

The first block of our code is the start of the file and sets up our HTML.

<!DOCTYPE html>
<meta charset="utf-8">
<style>

This leads into our style declarations.

body {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  margin: auto;
  padding-top: 40px;
  position: relative;
  width: 800px;
}

button {
  position: absolute;
  right: 40px;
  top: 10px;
}

.bullet { font: 10px sans-serif; }
.bullet .marker { stroke: #000; stroke-width: 2px; }
.bullet .tick line { stroke: #666; stroke-width: .5px; }
.bullet .range.s0 { fill: #eee; }
.bullet .range.s1 { fill: #ddd; }
.bullet .range.s2 { fill: #ccc; }
.bullet .measure.s0 { fill: steelblue; }
.bullet .title { font-size: 14px; font-weight: bold; }
.bullet .subtitle { fill: #999; }

We declare the (general) styling for the chart page in the first instance and then the button. Then we move on to the more interesting styling for the bullet charts.

The first line `.bullet { font: 10px sans-serif; }` sets the font size.

The second line sets the colour and width of the symbol marker. So if we were to change it to...

.bullet .marker { stroke: red; stroke-width: 10px; }

 ... the result is...


The next three lines set the colours for the fill of the qualitative ranges.

.bullet .range.s0 { fill: #eee; }
.bullet .range.s1 { fill: #ddd; }
.bullet .range.s2 { fill: #ccc; }

You can have more or less ranges set here, but to use them you also need the appropriate values in your data file. We will explore how to change this later.

The next line designates the colour for the value being measured.

.bullet .measure.s0 { fill: steelblue; }

 Like the qualitative ranges, we can have more of them, but in my personal opinion, it starts to get a bit confusing.

The final two lines lay out the styling for the label.

The next block of code loads the JavaScript files.

</style>
<button>Update</button>
<script type="text/javascript" src="d3/d3.v3.js"></script>
<script src="js/bullet.js"></script>
<script>

 In this case it's d3 and `bullet.js`. We need to load `bullet.js` as a separate file since it exists outside the code base of the d3.js 'kernel'.

Then we get into the JavaScript. The first thing we do is define the size of the area that we'll be working in.

var margin = {top: 5, right: 40, bottom: 20, left: 120},
    width = 800 - margin.left - margin.right,
    height = 50 - margin.top - margin.bottom;

 Then we define the chart size using the variables that we have just set up.

var chart = d3.bullet()
    .width(width)
    .height(height);

 The other important thing that occurs while setting up the chart is that we use the `d3.bullet` function call to do it. The `d3.bullet` function is the part that resides in the `bullet.js` file that we loaded earlier. The internal workings of `bullet.js` are a window into just how developers are able to craft extra code to allow additional functionality to d3.js.

Then we load our JSON data with our values that we want to display.

d3.json("data/cpu1.json", function(error, data) {

The next block of code is the most important IMHO, since this is where the chart is drawn.

  var svg = d3.select("body").selectAll("svg")
      .data(data)
    .enter().append("svg")
      .attr("class", "bullet")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
    .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .call(chart);

 However, to look at it you can be forgiven for wondering if it's doing anything at all.

We use our `.select` and `.selectAll` statements to designate where the chart will go (d3.select(`"body").selectAll("svg")`) and then load the data as `data` (`.data(data)`).

We add in a svg element (`.enter().append("svg")`) and assign the styling from our css section (`.attr("class", "bullet")`).

Then we set the size of the svg container for an individual bullet chart using `.attr("width", width + margin.left + margin.right)` and `.attr("height", height + margin.top + margin.bottom)`.

We then group all the elements that make up each individual bullet chart with `.append("g")` before placing the group in the right place with `.attr("transform", "translate(" + margin.left + "," + margin.top + ")")`.

The we wave the magic wand and call the `chart` function with `.call(chart);` which will take all the information from our data file ( like the `ranges``measures` and `markers` values) and use the `bullet.js` script to create a chart.

The reason I made the comment about the process looking like magic is that the vast majority of the heavy lifting is done by the `bullet.js` file. Because it's abstracted away from the immediate code that we're writing, it looks simplistic, but like all good things, there needs to be a lot of complexity to make a process look simple.

We then add the titles.

  var title = svg.append("g")
      .style("text-anchor", "end")
      .attr("transform", "translate(-6," + height / 2 + ")");

  title.append("text")
      .attr("class", "title")
      .text(function(d) { return d.title; });

  title.append("text")
      .attr("class", "subtitle")
      .attr("dy", "1em")
      .text(function(d) { return d.subtitle; });

 We do this in stages. First we create a variable `title` which will append objects to the grouped element created above (`var title = svg.append("g")`). We apply a style (`.style("text-anchor", "end")`) and transform to the objects (`.attr("transform", "translate(-6," + height / 2 + ")");`).

Then we append the `title` and `subtitle` data (from our JSON file) to our chart with a modicum of styling and placement.

Then we add a button and functions which do the job of applying random data to our variables every time it's pressed.

  d3.selectAll("button").on("click", function() {
    svg.datum(randomize).call(chart.duration(1000));
  });
});

function randomize(d) {
  if (!d.randomizer) d.randomizer = randomizer(d);
  d.markers = d.markers.map(d.randomizer);
  d.measures = d.measures.map(d.randomizer);
  return d;
}

function randomizer(d) {
  var k = d3.max(d.ranges) * .2;
  return function(d) {
    return Math.max(0, d + k * (Math.random() - .5));
  };
}

 I'm not going to delve into the working of the randomize function, because it exists simply to demonstrate the dynamic nature of the chart and not really how the chart is drawn.

However, I will be going through a process later to ensure that we can update the data and the chart automatically which will hopefully be more orientated to practical applications.

That's it! Now we'll go through how you can use the data to change aspects of the chart and what part's of the code need to be adjusted to work with those changes.

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 :-)).

12 comments:

  1. Is this plugin supports with bootstrap. If no is there any other way to achieve it with bootstrap?

    ReplyDelete
    Replies
    1. This plugin supports d3.js. However, you can use d3.js with Bootstrap, so you can set up a page with bootstrap and then use d3.js and bullet charts inside that. there is a sequence of articles explaining how to use bootstrap and d3.js in the book here https://leanpub.com/D3-Tips-and-Tricks (it's free). Check ths info out and see how that sets you up. Good luck

      Delete
  2. Hi, I want to use a bullet graph in the context of Not-to-Exceed Targets. My problem is that a I have temperature data that can be positive or ngative values and I do not know how to manage Negative values. Is there are any implementation or tip that you can suggest to help me?

    ReplyDelete
    Replies
    1. I haven't tried that myself, but I'd make a start with https://groups.google.com/forum/#!topic/d3-js/Zk1Vb2mvPrw or perhaps http://stackoverflow.com/questions/10127402/bar-chart-with-negative-values for inspiration

      Delete
  3. Hi, I am trying to implement bullet chart as a node with in indented tree. It is not working. Any inputs please advise.

    ReplyDelete
    Replies
    1. Golly, that's an interesting project. I haven't seen anyone attempt that before (although you should do some thorough googling of course). I'm sorry, but the best advice I could provide would be to get the simplest example of a bullet chart and the simplest example of the type of tree diagram you are trying to incorporate and to further reduce them wherever possible so that when you bring them together, it is as clear as possible. Good luck

      Delete
  4. Hi can we set x axis as per our wish

    ReplyDelete
    Replies
    1. This may be problematic. The bullet.js code defines the x axis, so you would need to edit that code. Totally possible, but a little bit more hard core than most people would attempt.

      Delete
  5. Do you have anything explaining the bullet.js? It's a huge file that adds lots of functionality so I'd like to understand what's in it, sorry for being a noob :)

    ReplyDelete
    Replies
    1. Hi Jeremy, there is more information in the book (download a copy here https://leanpub.com/D3-Tips-and-Tricks or read online here https://leanpub.com/D3-Tips-and-Tricks/read#leanpub-auto-bullet-charts). However, I don't explain the bullet.js file, just some of my observations on how it can be used. I have yet to update the v4 book with the bullet chart info, but I see that there is a v4 port already in play here https://github.com/GordonSmith/d3-bullet.
      Good luck.

      Delete
  6. Hello i am having a problem trying to add JSON objects i made in the same page instead of having a separate JSON file. How do i use this object?

    ReplyDelete
    Replies
    1. Check out the 'Getting the data' section in the book. There is a possibility that it might be something simple that is foiling you. If possible use a `console.log(data);` or similar statement to see how the data is getting ingested into your code. failing that try to reduce your code (and your data) as much as possible to see if you can determine the problem.

      Delete