Raspberry Pi Pico Tips and Tricks

Tuesday 8 January 2013

Using multiple axes for a d3.js graph


The following post is a portion of the D3 Tips and Tricks document which it free to download. To use this post in context, consider it with the others in the blog or just download the pdf  and / or the examples from the downloads page:-)
--------------------------------------------------------

Alrighty... Let's imagine that we want to show our wonderful graph with two lines on it much like we already have, but that the data that the lines is made from is significantly different in magnitude from the original data (in the example below, the data for the second line has been reduced by approximately a factor of 10 from our original data).

date       close   open
1-May-12   58.13   3.41
30-Apr-12  53.98   4.55
27-Apr-12  67.00   6.78
26-Apr-12  89.70   7.85
25-Apr-12  99.00   8.92
24-Apr-12  130.28  9.92
23-Apr-12  166.70  10.13
20-Apr-12  234.98  12.23
19-Apr-12  345.44  13.45
18-Apr-12  443.34  16.04
17-Apr-12  543.70  18.03
16-Apr-12  580.13  21.02
13-Apr-12  605.23  22.34
12-Apr-12  622.77  20.15
11-Apr-12  626.20  21.26
10-Apr-12  628.44  31.04
9-Apr-12   636.23  35.04
5-Apr-12   633.68  41.02
4-Apr-12   624.31  43.05
3-Apr-12   629.32  46.03
2-Apr-12   618.63  51.03
30-Mar-12  599.55  53.42
29-Mar-12  609.86  57.82
28-Mar-12  617.62  59.01
27-Mar-12  614.48  56.03
26-Mar-12  606.98  58.01

Now this isn't a problem in itself. D3 will still make a reasonable graph of the data, but because of the difference in range, the detail of the second line will be lost.
So what I'm proposing is that we have a second y axis on the right hand side of the graph that relates to the red line.

The adoption I've done here is based on the great examples put forward by Ben Christensen here: http://benjchristensen.com/2012/05/02/line-graphs-using-d3-js/.

Now... You'll want to concentrate a bit here since there are quite a few different bits to change and adapt, but don't despair, they're all quite logical and make sense.

First things first, there won't be space on the right hand side of or graph to show the extra axis, so we should make our right hand margin a little larger.
var margin = {top: 30, right: 40, bottom: 30, left: 50},
I went for 40 and it seems to fit pretty well.

Then (and here's where the main point of difference for this graph comes in) you want to amend the code to separate out the two scales for the two lines in the graph. This is actually a lot easier than it sounds, since it consists mainly of finding anywhere that mentions 'y' and replacing it with 'y0' and then adding in a reciprocal piece of code for 'y1'.

The idea here is that we will be creating two references for the y axis. One for each column of data. Then when we draw the lines the scales will automatically scale the data correctly (and separately) to our canvas and we will draw two different y axes with the different scales. Believe it or not, it's sounds a lot harder than it is.

Let's get started.

Firstly, change the variable declaration for 'y' to 'y0' and add in 'y1'.
var x = d3.time.scale().range([0, width]);
var y0 = d3.scale.linear().range([height, 0]);
var y1 = d3.scale.linear().range([height, 0]);
Then change our yAxis declaration to be specific for 'y0' and specifically 'left'. And add in a declaration for the right hand axis;
var yAxisLeft = d3.svg.axis().scale(y0)     //  <==  Add in 'Left' and 'y0'
    .orient("left").ticks(5);

var yAxisRight = d3.svg.axis().scale(y1)  // This is the new declaration for the 'Right', 'y1'
    .orient("right").ticks(5);           // and includes orientation of the axis to the right.
Note the orientation change for the right hand axis.

Now change our valueline declarations so that they refer to the 'y0' and 'y1' scales.
var valueline = d3.svg.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y0(d.close); });    // <== y0
var valueline2 = d3.svg.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y1(d.open); }); // <== y1
There are a few different ways for the scaling to work, but we'll stick with the fancy max method we used in the dual line example (although technically it's not required).
y0.domain([0, d3.max(data, function(d) { return Math.max(d.close); })]); 
y1.domain([0, d3.max(data, function(d) { return Math.max(d.open); })]); 
Again, here's the 'y0' and 'y1' changed and added and the maximums for 'd.close' and 'd.open' are separated out).

The final piece of the puzzle is to draw the new axis, but we also want to make a slight change to the original y axis. Since we have two lines and two axes, we need to know which belongs to which, so we can colour code the text in the axes to match the lines;
svg.append("g")
        .attr("class", "y axis")
        .style("fill", "steelblue")
        .call(yAxisLeft);   

    svg.append("g")             
        .attr("class", "y axis")    
        .attr("transform", "translate(" + width + " ,0)")   
        .style("fill", "red")       
        .call(yAxisRight);
In the above code you can see where we have added in a 'style' change for the yAxisLeft to make it 'steelblue' and a complementary change in the new section for yAxisRight to make that text red.

The yAxisRight section obviously needs to be added in, but the only significant difference is the transform / translate attribute that moves the axis to the right hand side of the graph.

And after all that, here's the result...
Now, let's not kid ourselves that it's a thing of beauty, but we should console our aesthetic concerns with the warm glow of understanding how the function works :-).

The above description (and heaps of other stuff) is in the D3 Tips and Tricks document that can be accessed from the downloads page of d3noob.org.

15 comments:

  1. nice and simple, you sir are a d3 maven.

    ReplyDelete
    Replies
    1. Very kind. But I think it fair to say that I'm only building on the awesome work of the open source community. Boy I wish I was a quarter as smart as some of the guys who contribute to d3.js. :-)

      Delete
  2. its possible with Axis label, hide axis, plotting tool-tip information like nvd3.

    ReplyDelete
    Replies
    1. sorry, I'm not quite sure what you mean, could you provide a link to an example?

      Delete
    2. http://cmaurer.github.io/angularjs-nvd3-directives/line.chart.html <--- I think he meant if it is possible to use library as such to implement the solution?

      Delete
    3. Hmm..., if the suggestion from Anonymous is correct, I'm afraid that I don't know if its possible to use a library. Sorry, I just don't know enough to provide advice.

      Delete
  3. If you want to use bar charts there is a extension from nvd3 that implement a second y-axis.

    https://github.com/mariuserik/nvd3-multibarchart-double-y-axis/issues/1

    ReplyDelete
  4. What i need to do if i have 3 y-axis with 1 x-axis.

    var x = d3.time.scale().range([0, width]);
    var y0 = d3.scale.linear().range([height, 0]);
    var y1 = d3.scale.linear().range([height, 0]);

    shal i put y2 here. ?

    ReplyDelete
    Replies
    1. Yes. There would be good. You will also need a variation on yXaisRight and yAxisLeft so that you can place the new axis somewhere. Make sure you have a good read of the blog and I recommend that you check out the book content (that is more up to date (https://leanpub.com/D3-Tips-and-Tricks) to get some better context (It's free)). Good luck

      Delete
    2. Hi what if I need more than 2 y-axis, let's say 5 y-axis with 1 x-axis. Do I need declare every single scale ? There's a way of doing this dynamically ? Thanks in advance

      Delete
    3. Hi Juan. Good question. I'm pretty sure you could do it dynamically although you could obviously do it in fixed code as well. However, I would counsel caution as (without trying mind you) I would imagine that any graph with that many Y axes would start to look extremely 'busy'. I recommend making a hard coded copy first to see what it might look like before going to the trouble of a dynamic version.

      Delete
    4. Hi again, even though the graph will look busy, I would like to know if you know another method to do it in a more dynamic way. I've been struggling with this problem for a while and I'm not quite sure how to solve it. Thanks

      Delete
    5. Sorry I don't have a method that I could recommend. The closest I have ever tried was the multi-line example with a dynamic number of lines, but I don't think that it's going to help much other than to demonstrate the dynamic settind if the 'id' attribute. http://www.d3noob.org/2014/07/d3js-multi-line-graph-with-automatic.html Sorry I can't be more help.

      Delete
  5. This comment has been removed by the author.

    ReplyDelete