Raspberry Pi Pico Tips and Tricks

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.  

19 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
  2. How can we take into consideration the tick label length? If i have long labels vs short labels (e.g. 80.00% vs 4), how can I ensure the axis label is not stupidly far away from the tick label?

    ReplyDelete
    Replies
    1. The simplest way is to adjust the margin width. There could be some crazy auto adjust font size methods, but I just adjust the margin width to the larget value that suits the labels I have.

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

      Delete
    3. Sorry, I should have read your question more closely. What you are looking for is the ability to adjust you margin (as above) and then ust the text anchor attribute to justify it and locate it to the right spot. https://leanpub.com/D3-Tips-and-Tricks/read#leanpub-auto-text

      Delete
  3. My y axis is on the right side. I'm trying to use your y axis label code but by rotating it in the opposite direction but it keeps disappearing, do you have any suggestions?

    ReplyDelete
    Replies
    1. That's a tricky one. You will need to show your code (DO NOT POST IT HERE!) Put it up on Stack Overflow and ask a question. But first just try making your graph a lot larger and make your margins large as well. The text might be just 'off the page' so to speak. Once you find where it is you should be able tom adjudt the function that is putting it there.

      Delete
  4. This comment has been removed by a blog administrator.

    ReplyDelete
  5. Hi ! Thanks for this great article... after struggling a bit with the rotation, I found it easier to just use the SVG transform that takes the center of the rotation. In that case you simple position the y-label with x and y attributes as for the X-label and then you specify the current location (x,y) as the center of the rotation... for instance:
    .attr("transform", "rotate(-90 " + margin.left/2 + " " + height/2 + ")")

    ReplyDelete
  6. hi..i wanna ask,how can i label the x-axis with different name,which is states,,because i found several code that used time and its automatically labelled for ticks..
    there is a code labelled the x-axis with alphabet,but they used .tsv file.Is there any code that i can use to label the x-axis with text without using .tsv file?

    ReplyDelete
    Replies
    1. Hi, the type of file you use isn't really important (csv for instance can be treated the same as tsv). However I think that what you're looking for is some direction on using *ordinal* scales. Try checking out these links to see if that fits. https://github.com/mbostock/d3/wiki/Ordinal-Scales http://bl.ocks.org/mbostock/3259783 http://www.jeromecukier.net/blog/2011/08/11/d3-scales-and-color/ Good luck.

      Delete
  7. if my y axis data and the name of the axis are overlapping each other how do i set it right. A very beginner, explain in detail please.

    ReplyDelete
    Replies
    1. Unfortunately a detailed description won't help you learn some of the basics that are really at the core of d3. To get to grips with the topic a bit better you may need to spend some time working through some more examples to get a better feel for how everything goes together. However... As a quick experiment, try changing the with, height and margin values to see how they vary the graph. While this might help, it might also make the graph look a bit weird. Keep experimenting :-).

      Delete
  8. if my y axis data and the name of the axis are overlapping each other how do i set it right. A very beginner, explain in detail please.

    ReplyDelete
    Replies
    1. Sorry I replied on a different thread. Check out the answer above :-)

      Delete
  9. Hi,

    I am not able to get the generic y axis names and y axis labels to be non-overlapping.
    Also, if the y axis labels' length (a parametric value) is more than the margin.left, it cuts off the svg. Could you please help me here.

    ReplyDelete
    Replies
    1. helping directly by remote control is always tricky. What I recommend you do is to have a play with the values for the margins and the transform for the labels. Just keep playing with the values (make them larger and smaller) till you understand how the variables affect the result. Then you will be well placed to understand this and other processes much better. The heart of understanding from experimentation cannot be overstated.

      Delete