Tuesday, 15 January 2013

Applying a colour gradient to a graph line in d3.js

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

I just know that you were impressed with the changing dots in a scatter plot based on the value in the previous example. But could we could one better?

How about we try to reproduce the same effect but by varying the colour of the plotted line.
This is a neat feature and a useful example of the flexibility of d3.js and svg in general.

I used the appropriate bits of code from Mike Bostock's Threshold Encoding example here http://bl.ocks.org/3970883. And I should take the opportunity to heartily recommend browsing through his collection of examples on bl.ocks.org (http://bl.ocks.org/mbostock).

Here then is a plotted line that is red below 400, green above 620 and black in between.
How cool is that?

Enough beating around the bush, how is the magic line produced?

Well starting with our simple line graph there are only two blocks of code to go in. One is CSS in the <style> area and the second is a tricky little piece of code that deals with gradients.

So, first the CSS.
.line {                         
    fill: none;                 
    stroke: url(#line-gradient);    
    stroke-width: 2px;          
}
This block can go in the <style> area towards the end.

There's the fairly standard fill of none and a stroke width of 2 pixels, but the stroke: url(#line-gradient); is something different.

In this case the stroke (which we remember is the colour of the line) us being set at a link within the page which is set by the anchor #line-gradient. As we will see shortly this is in our second block of code, so the colour is being defined in a separate portion of the script.

And now the JavaScript Gradient code;
svg.append("linearGradient")                
        .attr("id", "line-gradient")            
        .attr("gradientUnits", "userSpaceOnUse")    
        .attr("x1", 0).attr("y1", y(0))         
        .attr("x2", 0).attr("y2", y(1000))      
    .selectAll("stop")                      
        .data([                             
            {offset: "0%", color: "red"},       
            {offset: "40%", color: "red"},  
            {offset: "40%", color: "black"},        
            {offset: "62%", color: "black"},        
            {offset: "62%", color: "lawngreen"},    
            {offset: "100%", color: "lawngreen"}    
        ])                  
    .enter().append("stop")         
        .attr("offset", function(d) { return d.offset; })   
        .attr("stop-color", function(d) { return d.color; });   
There's our anchor on the second line!

But let's not get ahead of ourselves. This block should be placed after the x and y domains are set, but before the line is drawn.

Seems a bit strange doesn't it? This block is all about defining the actions of an element, but the element in this case is a gradient and the gradient acts on the line.

So, our first line adds our linear gradient. Gradients consist of continuously smooth colour transitions along a vector from one colour to another We can have a linear one or a radial one and depending on which you select, there are a few options to define. There is some great information on gradients here http://www.w3.org/TR/SVG/pservers.html (more than I ever thought existed).

The second line (.attr("id", "line-gradient")) sets our anchor for the CSS that we saw earlier.
The third fourth and fifth lines define the bounds of the area over which the gradient will act. Since the coordinates x1, y1, x2, y2 will describe an area. The values for y1 (0) and y2 (1000) are used more fore convenience to align with our data (which has a maximum value around 630 or so. For more information on the gradientUnits attribute I found this page useful https://developer.mozilla.org/en-US/docs/SVG/Attribute/gradientUnits. We'll come back to the coordinates in a moment.

The next block selects all the 'stop' elements for the gradients. These stop elements define where on the range covered by our coordinates the colours start and stop. These have to be defined as either percentages or numbers (where the numbers are really just percentages in disguise (i.e. 45% =0.45)).

The best way to consider the stop elements is in conjunction with the gradientUnits. The image following may help.
In this case our coordinated describe a vertical line from 0 to 1000. Our colours transition from red (0) to red (400) at which point they change to black (400) and this will continue until it gets to black (620). Then this changes to green (620) and from there, any value above that will be green.

Now, it might seem a little convoluted to be doubling up on the colours and values, but the reason is that the gradient functions are have a lot more to them than we're using and we'll have a look at the possibilities once the explanation of the code is done.

So after defining the stop elements, we enter and append the elements to the gradient (.enter().append("stop")) with attributes for offset and color that we defined in the stop elements area.
Now, the IS cool, but by now, I hope that you have picked that a gradient function really does mean a gradient, and not just a straight change from one colour to another.

So, let's try changing the stop element offsets to the following (and making the stroke-width slightly larger to see more clearly what's going on);
.data([                             
            {offset: "0%", color: "red"},       
            {offset: "30%", color: "red"},  
            {offset: "45%", color: "black"},        
            {offset: "55%", color: "black"},        
            {offset: "60%", color: "lawngreen"},    
            {offset: "100%", color: "lawngreen"}    
        ])
And here we go...
Ahh... A real gradient.

I have tended to find that I need to have a good think about how I set the offsets and bounds when doing this sort of thing since it can get quite complicated quite quickly :-).

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.

8 comments:

  1. When explaining the gradient stops, you say the number is a percent in disguise, and then offer the example "45% = 0.43". I originally thought the you were trying to show that they didn't have to be the same, but after checking the W3 page your referred to, it seems more likely that you just mistyped 0.43 instead of 0.45 (or 45% instead of 43%).

    Thanks for the site and the compiled book. I'm trying to figure out a way I can have my students create a D3 graph of concepts in the class and your book is helpful.

    ReplyDelete
    Replies
    1. Wow. Nice catch. That's a way to find out who's paying attention! Many thanks for picking that up. As you said, that's my typo and the 0.43 should be 0.45!
      Always good to hear that d3.js is being used in the classroom. If I can help at all, just use the contact address in the introduction section in the book to get in touch.
      Thanks again.

      Delete
    2. And I forgot to mention. I have amended it on this page and in the book which should have the updated version ready for download in about 20 minutes. (Leanpub rocks for amendments :-))

      Delete
  2. Hi, Can I use this approach to get a gradient along an arc along stroke (not the RADIAL gradient one)? I am trying https://gist.github.com/mbostock/4163057
    but till now have not been able to do it along an arc or circle!

    ReplyDelete
    Replies
    1. Hi, Sorry for the delay in replying. This is a really good question. At first I don't think I understood it, but I believe that I now understand what you're asking. We can use an example like the one Mike has at the link you pasted, but that is only useful if you have a 'path'. I believe the question you're asking is how do we do the same thing for a circle or an arc?
      Let me start off by apologising that I don't have an answer. This could be tricky or it could be easy. An arc or a circle is a construct that is essentially a path where d3.js is making the function of drawing a slightly more complex object easier. So in theory, there should be a way to dig slightly lower in the 'stack' and apply the gradient there. Sorry to re-direct you (especially after such a long wait) but this question is beyond me. I would suggest that you ask it on Stack Overflow where there are a bunch of folks smarter than me who might be able to get their teeth into it.
      It IS a really good question. Thanks

      Delete
  3. Does this work on firefox as well?

    ReplyDelete