Friday, 5 July 2013

Adapting and changing bullet chart components

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

Adapting and changing bullet chart components

This section explores some of the simple changes that can be made to bullet charts that may not necessarily be obvious.

Understand your data

The first point to note is that understanding the data loaded from the JSON file is a key to knowing what your chart is going to do.

We'll start by looking at our data in a way that hopefully makes the most sense.

You may be faced with data for a bullet chart that's in a format as follows;

 [
{"title":"CPU Load","subtitle":"GHz","ranges":[1500,2250,3000],"measures":[2200],"markers":[2500]},
{"title":"Memory Used","subtitle":"MBytes","ranges":[256,512,1024],"measures":[768],"markers":[900]}
]


 This is perfectly valid data, but we'll find it slightly easier to understand if we show it like this...

 [
  {
    "title":"CPU Load",
    "subtitle":"GHz",
    "ranges":[1500,2250,3000],
    "measures":[2200],
    "markers":[2500]
  },
  {
    "title":"Memory Used",
    "subtitle":"MBytes",
    "ranges":[256,512,1024],
    "measures":[768],
    "markers":[900]
  }
]


 The data is exactly the same (in terms of content) but I find it a lot easier to comprehend what's going on with the second example.

I have a section in the book called 'Understanding JavaScript Object Notation (JSON)' in the 'Assorted Tips and Tricks' chapter. I have found life a lot easier once I started to understand how data was structured in JSON, and if you take a bit of time to understand it, I think you'll find life easier too.

Add as many individual charts as you want.

The example data in the file is an array of two groups. Each group represents the information required to generate one bullet chart. Therefore the example data above will create the following charts;



You don't need to make any changes to your code in order to add more individual charts. You just need to add more data groups to your JSON file. The following example uses exactly the same code, but with several extra groups of data.



Add more ranges and measures

Returning to our single chart example, you can see from the JSON data that there are three specified ranges and one measure.

 [
  {
    "title":"CPU 1 Load",
    "subtitle":"GHz",
    "ranges":[1500,2250,3000],
    "measures":[2200],
    "markers":[2500]
  }
]


 The same was true for the CSS in the JavaScript code. Three ranges and one measure

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

 By matching the CSS for the `.bullet` style with the data you can add more or less of both. For example here's example data, CSS and chart with five ranges and two measures.

 [
  {
    "title":"CPU 1 Load",
    "subtitle":"GHz",
    "ranges":[500,1000,1500,2250,3000],
    "measures":[1250, 2200],
    "markers":[2650]
  }
]

.bullet { font: 10px sans-serif; }
.bullet .marker { stroke: lightgreen; stroke-width: 5px; }
.bullet .tick line { stroke: #666; stroke-width: .5px; }
.bullet .range.s0 { fill: navy; }
.bullet .range.s1 { fill: mediumblue; }
.bullet .range.s2 { fill: dodgerblue; }
.bullet .range.s3 { fill: aqua; }
.bullet .range.s4 { fill: lightblue; }
.bullet .measure.s0 { fill: red; }
.bullet .measure.s1 { fill: pink; }
.bullet .title { font-size: 14px; font-weight: bold; }
.bullet .subtitle { fill: #999; }


First of all. Yes, I know the colours are gaudy. Hopefully they stand out. Don't abuse your own graphs in this hideous way.

More importantly though, you can now get a better idea of how to align the ranges and measures values in the JSON file with the `.range` and `.measure` styles in the CSS.


The diagram shows that the `.range` and `.measure` bars are numbered from the right. (for example the 'navy' colour showing the range up to 3000 GHz is designated `.range.s0`. At first this convention of numbering from the right confused me. I imagined that the smallest range should be `.range.s0` and this should be on the left. Then I realised that the numbering related to the layer of the range. So this would make `.range.s0` go from 0 to 3000. Then the second layer would be `.range.s1` which would go on top of `.range.s0` from 0 to 2250, thereby covering most of `.range.s0` except for the part that exceeded `.range.s1`. Which is exactly what we see with successively higher layers having higher numbers. The same is true for the `.measure` numbers and layers.


Updating a bullet chart automatically

Displaying static data is a good start for a bullet chart, but if you have data that's changing dynamically, you need to be able to re-load the information and display it automatically.

To adapt our code to this purpose we will first remove the parts that added the button.

Remove this portion from the CSS section;

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

Then remove this line that added the button in the html section;

<button>Update</button>

 All we need to do now is change the section that called the original JSON file from;

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

... to ...

d3.json("data/bulletdata2.json", function(error, data) {
So that we're dealing with a different JSON file (there's no need to go messing around with our original data).

Change the section that used to call the function to randomise the data from the button click from...

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

... to ...

  setInterval(function() {
        updateData();
    }, 1000);

 This new piece of code simply sets up a repeating function that calls another function (`updateData`) every 1000ms.

The final change is to replace the original functions that randomised the data...

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

 ... with our new function that updates the data ...

function updateData() {
    d3.json("data/bulletdata2.json", function(error, data) {
        d3.select("body").selectAll("svg")
            .datum(function (d, i) {
                d.ranges = data[i].ranges;
                d.measures = data[i].measures;
                d.markers = data[i].markers;
                return d;
            })
            .call(chart.duration(1000));
    });
}

This new function (`updateData`) reads in our JSON file again, selects all the svg elements then updates all the `.ranges`, `.measures` and `.markers` data with whatever was in the file. Then it calls the `chart` function that updates the bullet charts.

All the code components for this script can be downloaded from GitHub. A live version can be viewed on bl.ocks.org (although it won't update since the data file can't be updated online).

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

5 comments:

  1. Thanks for the book. It really helped. :) Thanks a lot :)

    ReplyDelete
  2. How do I make this work from static data - like var data = ["a" : "bb" } , NOT a file?

    ReplyDelete
    Replies
    1. Essentially you can replace the 'd3.csv' part with a statement that declares the data. The only real trap is to make sure that you reconnise that the 'd3.csv' line has an enclosing set of brackets at the end of the script. Check out the following two examples for the two ways of doing it with the same visualization http://bl.ocks.org/d3noob/b3ff6ae1c120eea654b5 and http://bl.ocks.org/d3noob/13a36f70a4f060b97e41
      I know it's not the same graphic, but I'm sure you'll get the idea :-). Good luck.

      Delete
  3. Many thanks for that usefull component, how to manage axis label overlapping?

    ReplyDelete
    Replies
    1. Hi. I haven't seen that before. You will probably have the best chance of success by asking the question on Stack Overflow. Good luck.

      Delete