Monday, 24 October 2016

Adding Axis Labels in d3.js v4

The following post is a section of the book 'D3 Tips and Tricks v4.x'.  The entire book can be downloaded in pdf format for free from Leanpub or you can read it online here.
Since this post is a snapshot in time. I recommend that you download a copy of the book which is updated frequently to improve and expand the content.
---------------------------------------

Adding Axis Labels

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!
We’ll start with our default code for our simple graph. The full code for this can be found on github or in the code samples bundled with this book (simple-graph.html and data.csv). A live example can be found on bl.ocks.org.
Preparation: Because we’re going to be adding labels to the bottom and left of the graph we need to increase the bottom and left margins. Changes like the following should suffice;
var margin = {top: 20, right: 20, bottom: 50, left: 70},

The x axis label

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 center 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;
  // text label for the x axis
  svg.append("text")             
      .attr("x", 480 )
      .attr("y",  475 )
      .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.
  // Add the x Axis
  svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

  // PUT THE NEW CODE HERE!

  // Add the y Axis
  svg.append("g")
      .call(d3.axisLeft(y));
Before we describe what’s happening, let’s take a look at the result;
Date label on x axis
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.
  // text label for the x axis
  svg.append("text")             
      .attr("x", 480 )
      .attr("y",  475 )
      .style("text-anchor", "middle")
      .text("Date");
The first line appends a “text” element to our svg element. There is a lot more to learn about “text” elements at the home of the World Wide Web Consortium (W3C). The next two lines ( .attr("x", 480 ) and .attr("y", 475 ) ) set the attributes for the x and y coordinates to position the text on the svg.
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 decrease the height of the graph by editing the line here;
    height = 500 - margin.top - margin.bottom;
and making the height 490 pixels;
    height = 490 - margin.top - margin.bottom;
The result is as follows;
Hard coded Date label
EVERYTHING about the graph has adjusted itself, except our nasty, hard coded ‘Date’ label which has been cruelly cut off. 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", 480 )
      .attr("y",  475 )
lets let our variables do the walking and use;
      .attr("x", width / 2 )
      .attr("y",  height + margin.top + 20)
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 its height plus the top margin and 20 pixels (as a fixed offset) (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 = 560 - margin.left - margin.right,
    height = 300 - margin.top - margin.bottom;
We still finish up with an acceptable result;
Auto adjusting Date label
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 it’s 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 marvellous coordinates code;
      .attr("x", width / 2 )
      .attr("y",  height + margin.top + 20))
And replace it with a single (longer) line;
      .attr("transform",
            "translate(" + (width/2) + " ," + 
                           (height + margin.top + 20) + ")")
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).

The y axis label

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)
  // Add the y Axis
  svg.append("g")
      .call(d3.axisLeft(y));

  // PUT THE NEW CODE HERE!

});
And the result looks like this;
y axis label with rotation!
There we go, a label for the y axis that is nicely centered 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 text element to our svg element (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;
Reference point pre-rotation
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;
Reference point after rotation
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.
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 svg element.
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. 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. They make sure the 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.

The full code for this example can be found on github or in the code samples bundled with this book (axis-labels.html and data.csv). A live example can be found on bl.ocks.org.

The post above (and heaps of other stuff) is in the book 'D3 Tips and Tricks v4.x' that can be downloaded for free (or donate to encourage further development :-)).

No comments:

Post a Comment