Sunday, 30 December 2012

Adding axis labels to a d3.js graph

The following post is a portion of the D3 Tips and Tricks document which is free to download from the main page.
---------------------------------------------------------


What's the first thing you get told at school when drawing a graph?

“Always label your axes!”

So, time to add a couple of labels!

First things first (because they're done slightly differently), the x axis. If we begin by describing what we want to achieve, it may make the process of implementing a solution a little more logical

What we want to do is to add a simple piece of text under the x axis and in the centre of the total span. Wow, that does sound easy.

And it is, but there are different ways of accomplishing it, and I think I should take an opportunity to demonstrate them. Especially since one of those ways is a BAD idea.

Lets start with the bad idea first :-).

This is the code we're going to add to the simple line graph script;
svg.append("text")      // text label for the x axis
        .attr("x", 265 )
        .attr("y",  240 )
        .style("text-anchor", "middle")
        .text("Date");
We will put it in between the blocks of script that add the x axis and the y axis.
svg.append("g")         // Add the X Axis
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    //  PUT THE NEW CODE HERE!

    svg.append("g")         // Add the Y Axis
        .attr("class", "y axis")
        .call(yAxis);
Before we describe what's happening, let's take a look at the result;
Well, it certainly did what it was asked to do. There's a 'Date' label as advertised! (Yes, I know it's not pretty.) Let's describe the code and then work out why there's a better way to do it.
svg.append("text")      // text label for the x axis
        .attr("x", 265 )
        .attr("y", 240 )
        .style("text-anchor", "middle")
        .text("Date");
The first line appends a "text" element to our canvas. There is a lot more to learn about "text" elements here; http://www.w3.org/TR/SVG/text.html#TextElement.

The next two lines ( .attr("x", 265 ) and .attr("y", 240 ) ) set the attributes for the x and y coordinates to position the text on the canvas.

The second last line (.style("text-anchor", "middle")) ensures that the text 'style' is such that the text is centre aligned and therefore remains nicely centred on the x,y coordinates that we send it to.

The final line (.text("Date");) adds the actual text that we are going to place.

That seems really simple and effective and it is. However, the bad part about it is that we have hard coded the location for the date into the code. This means if we change any of the physical aspects of the graph, we will end up having to re-calculate and edit our code. And we don't want to do that.

Here's an example. If I decide that I would prefer to increase the height of the graph by editing the line here;
height = 270 - margin.top - margin.bottom;
and making the height 350 pixels;
height = 350 - margin.top - margin.bottom;
The result is as follows;
EVERYTHING about the graph has adjusted itself, except our nasty, hard coded 'Date' label. This is far from ideal and can be easily fixed by using the variables that we set up ever so carefully earlier.

So, instead of;
.attr("x", 265 )
        .attr("y", 240 )
lets let our variables do the walking and use;
.attr("x", width / 2 )
        .attr("y",  height + margin.bottom)
So with this code we tell the script that the 'Date' label will always be halfway across the width of the graph (no matter how wide it is) and at the bottom of the graph with respect to it's height and the bottom margin (remember it uses a coordinates system that increases from the top down).

The end result of using variables is that if I go to an extreme of changing the height and width of my graph to;
width = 400 - margin.left - margin.right,
    height = 200 - margin.top - margin.bottom;
We still get an acceptable result;
Well, for the label position at least :-).

So the changes to using variables is just a useful lesson that variables rock and mean that you don't have to worry about your graph staying in relative shape while you change the dimensions. The astute readers amongst you will have learned this lesson very early on in your programming careers, but it's never a bad idea to make sure that users that are unfamiliar with the concept have an indicator of why it's a good idea.
Now the third method that I mentioned at the start of our x axis odyssey. This is not mentioned because its any better or worse way to implement your script (The reason that I say this is because I'm not sure if it's better or worse.) but because it's sufficiently different to make it look confusing if you didn't think of it in the first place.

So, we'll take our marvelous coordinates code;
.attr("x", width / 2 )
        .attr("y",  height + margin.bottom)
And replace it with a single (longer) line;
.attr("transform", "translate(" + (width / 2) + " ," + (height + margin.bottom) + ")")
This uses the "transform" attribute to move (translate) the point to place the 'Date' label to exactly the same spot that we've been using for the other two examples (using variables of course).
Things to note about this piece of code;

The "translate” function is done in a 'translate(x,y)' style but it is put on the page in such a way that the verbatim pieces that get passed back are in speech marks and the variables are in the clear (in a manner of speaking). That's why the comma is in speech marks.

Additionally, the variables are contained within plus signs. I make the assumption that this is a designator for 'areas where there is variable action going on'. The end result is that if you try to do some maths in that area with a plus sign, it does not appear to work (or at least it didn't for me). That's why I put the variable for ( + (height + margin.bottom) + ) in parenthesis (then I thought I should make the + (width / 2) + part look the same, but actually you can get away without them there).

So, that's the x axis label. Time to do the y axis. The code we're going to use looks like this;
svg.append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 – margin.left)
        .attr("x",0 - (height / 2))
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .text("Value");
For the sake of neatness we will put the piece of code in a nice logical spot and this would be following the block of code that added the y axis (but before the closing curly bracket)
svg.append("g")         // Add the Y Axis
        .attr("class", "y axis")
        .call(yAxis);

    // PUT THE NEW CODE HERE!

});
And the result looks like this;
There we go, a label for the y axis that is nicely centred and (gasp!) rotated by 90 degrees! Woah, does the leetness never end! (No. No it does not.)

So, how do we get to this incredible result?

The first thing we do is the same as for the x axis and append a test element to our canvas (svg.append("text")).

Then things get interesting.
.attr("transform", "rotate(-90)")
Because that line rotates everything by -90 degrees. While it's obvious that the text label 'Value' has been rotated by -90 degrees (from the picture), the following lines of code show that we also rotated our reference point (which can be a little confusing).
.attr("y", 0 – margin.left)
        .attr("x",0 - (height / 2))
Let's get graphical to illustrate how this works;
Here's our starting position, with x,y in the 0,0 coordinate of the graph drawing area surrounded by the margins.

When we apply a -90 degrees transform we get the equivalent of this;
Here the 0,0 coordinate has been shifted by -90 degrees and the x,y designations are flipped so that we now need to tell the script that we're moving a 'y' coordinate when we would have otherwise been moving 'x'.

Hence, when the script runs...  
.attr("y", 0 – margin.left)
… we can see that this is moving the x position to the left from the new 0 coordinate by the margin.left value.
 
Likewise when the script runs...
.attr("x",0 - (height / 2))
… this is actually moving the y position from the new 0 coordinate halfway up the height of the graph area.

Now, I will be the first to admit that this does seem a little confusing, but here's the good part. You really don't need to understand it completely. Simply do what I did when I saw the code. Play with is a bit till you get the result you were looking for. If that means putting in some hard coded numbers and incrementing them to see which way is the new 'up'. Good! Once you work it out, then work out how to get the right variable expression in there and you're set.

In the worst case scenario, simply use the code blocks as shown here and leave well enough alone :-)
Right, we're not quite done yet. The following line has the effect of shifting the text slightly to the right.
.attr("dy", "1em")

Firstly the reason we do this is that our previous translation of coordinates means that when we place our text label it sits exactly on the line of 0 – margin.left. But in this case that takes the text to the other side of the line, so it actually sits just outside the boundary of the overall canvas.

The "dy" attribute is another coordinate adjustment move, but this time a relative adjustment and the "1em" is a unit of measure that equals exactly one unit of the currently specified text point size (http://en.wikipedia.org/wiki/Em_(typography)). So what ends up happening is that the 'Value' label gets shifted to the right by exactly the height of the text, which neatly places it exactly on the edge of the canvas.

The two final lines of this part of the script are the same as for the x axis and they make sure reference point is aligned to the centre of the text (.style("text-anchor", "middle")) and then it prints the text (.text("Value");). There, that wasn't too painful.

Many thanks the the eagle eyed reader that spotted that the code for this example wasn't available for download. Although this isn't the perfect resolution, here is the code in full;
<!DOCTYPE html>
<meta charset="utf-8">
<style>

body { font: 12px Arial;}

path { 
    stroke: steelblue;
    stroke-width: 2;
    fill: none;
}

.axis path,
.axis line {
    fill: none;
    stroke: grey;
    stroke-width: 1;
    shape-rendering: crispEdges;
}

</style>
<body>
<script type="text/javascript" src="d3/d3.v3.js"></script>

<script>

var margin = {top: 30, right: 20, bottom: 30, left: 50},
    width = 600 - margin.left - margin.right,
    height = 270 - margin.top - margin.bottom;

var parseDate = d3.time.format("%d-%b-%y").parse;

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

var xAxis = d3.svg.axis().scale(x)
    .orient("bottom").ticks(5);

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

var valueline = d3.svg.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });
    
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 + ")");

// Get the data
d3.tsv("data/data.tsv", function(error, data) {
    data.forEach(function(d) {
        d.date = parseDate(d.date);
        d.close = +d.close;
    });

    // Scale the range of the data
    x.domain(d3.extent(data, function(d) { return d.date; }));
    y.domain([0, d3.max(data, function(d) { return d.close; })]);

    svg.append("path")      // Add the valueline path.
        .attr("class", "line")
        .attr("d", valueline(data));

    svg.append("g")         // Add the X Axis
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    // Add the text label for the x axis
    svg.append("text")
        .attr("transform", "translate(" + (width / 2) + " ," + (height + margin.bottom) + ")")
        .style("text-anchor", "middle")
        .text("Date");

    svg.append("g")         // Add the Y Axis
        .attr("class", "y axis")
        .call(yAxis);

    // Add the text label for the Y axis
    svg.append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 - margin.left)
        .attr("x",0 - (height / 2))
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .text("Value");

});

</script>
</body>



-------------------------------------------------------------
The above description (and heaps of other stuff aimed at helping those with limited understanding, but plenty of desire to play with D3) is in the D3 Tips and Tricks document that can be accessed from the main page of d3noob.org.  

2 comments:

  1. Sorry but without example this is worthless (

    ReplyDelete
    Replies
    1. Wow! This was one of my first set of posts back in the days when the book was being distributed via either dropbox or github and I think it might have measured 60 pages! I was also distributing examples via the d3noob downloads page (and still am for legacy's sake). When I read your comment I thought "Surely I have that code in one of the examples". But you're right. It's not there :-(. My bad entirely. If it's any help, I will be redoing the initial part of the book and changing the examples slightly (to use csv for instance) but more importantly to make sure I have all the example code in a single place (via Leanpub when you download the book). This will take me a while, so in the mean time, please accept my apologies and I will add in a separate section at the end of the post above that includes the full code.

      Delete