Gradient along links in D3 Sankey diagram
Asked Answered
R

1

16

Here is jsfiddle of a Sankey diagram:

enter image description here

I am trying to modify colors of the links so that the color of each link is actually gradient from its source node color to its target node color. (it is assumed that opacity will remain 0.2 or 0.5 depending whether a mouse hovers or not over the link; so links will remain a little "paler" than nodes)

I took a look at this nice and instructive example, which draws this gradient filled loop:

enter image description here

However, I simply couldn't integrate that solution to mine, it looks too complex for the given task.

Also, note that links in original Sankey diagram move while node is being dragged, and must display gradient even in those transitory states. A slight problem is also transparency of links and nodes, and order of drawing. I would appreciate ideas, hints.

Roye answered 18/1, 2014 at 15:28 Comment(2)
Oh, I'm starting to hate Mike Bostock for that example. It looks so pretty, but it's a mess underneath and not something you'd ever want to use in a data visualization, let alone an interactive one. Breakdown here. Since (a) you're pretty comfortable with d3, and (b) your paths are almost horizontal lines that don't twist on themselves, you should still be able to make it work be creating a custom gradient for each path. Set gradientUnits to objectBoundingBox and it should adjust for moving the nodes.Einkorn
@AmeliaBR, I have an impression that your hints are right. Will try something and come back if I have some results. Thanks a lot!Roye
E
37

@VividD: Just saw your comment, but I was about done anyway. Feel free to ignore this until you've figured it out on the own, but I wanted to make sure I knew how to do it, too. Plus, it's a really common question, so good to have for reference.

How to get a gradient positioned along a line

With the caveat for anyone reading this later, that it will only work because the paths are almost straight lines, so a linear gradient will look half-decent -- setting a path stroke to a gradient does not make the gradient curve with the path!

  1. In initialization, create a <defs> (definitions) element in the SVG and save the selection to a variable:

    var defs = svg.append("defs");
    
  2. Define a function that will create a unique id for your gradient from a link data object. It's also a good idea to give a name to the function for determining node colour:

    function getGradID(d){return "linkGrad-" + d.source.name + d.target.name;}
    function nodeColor(d) { return d.color = color(d.name.replace(/ .*/, ""));}
    
  3. Create a selection of <linearGradient> objects within <defs> and join it to your link data, then set the stop offsets and line coordinates according to the source and target data objects.

    For your example, it probably will look fine if you just make all the gradients horizontal. Since that's conveniently the default I thought all we would have to do is tell the gradient to fit to the size of the path it is painting:

    var grads = defs.selectAll("linearGradient")
                     .data(graph.links, getLinkID);
    
    grads.enter().append("linearGradient")
                 .attr("id", getGradID)
                 .attr("gradientUnits", "objectBoundingBox"); //stretch to fit
    
    grads.html("") //erase any existing <stop> elements on update
         .append("stop")
         .attr("offset", "0%")
         .attr("stop-color", function(d){
               return nodeColor( (d.source.x <= d.target.x)? d.source: d.target) 
              });
    
    grads.append("stop")
         .attr("offset", "100%")
         .attr("stop-color", function(d){
               return nodeColor( (d.source.x > d.target.x)? d.source: d.target) 
              });
    

    Unfortunately, when the path is a completely straight line, its bounding box doesn't exist (no matter how wide the stroke width), and the net result is the gradient doesn't get painted.

    So I had to switch to the more general pattern, in which the gradient is positioned and angled along the line between source and target:

    grads.enter().append("linearGradient")
        .attr("id", getGradID)
        .attr("gradientUnits", "userSpaceOnUse");
    
    grads.attr("x1", function(d){return d.source.x;})
        .attr("y1", function(d){return d.source.y;})
        .attr("x2", function(d){return d.target.x;})
        .attr("y2", function(d){return d.target.y;});
    
    /* and the stops set as before */
    
  4. Of course, now that the gradient is defined based on the coordinate system instead of based on the length of the path, you have to update those coordinates whenever a node moves, so I had to wrap those positioning statements in a function that I could call in the dragmove() function.

  5. Finally, when creating your link paths, set their fill to be a CSS url() function referencing the corresponding unique gradient id derived from the data (using the pre-defined utility function):

    link.style("stroke", function(d){
        return "url(#" + getGradID(d) + ")";
    })
    

And Voila!
Sankey diagram with custom gradients from the data

Einkorn answered 18/1, 2014 at 18:47 Comment(3)
I've actually been meaning to write up an example like this for a while. I was unsatisfied with my "sorry, it's too hard" answer on that other question I linked to, and had been puzzling it over, eventually coming up with the idea of using d3 to create custom-positioned gradients. This is actually a good example for it, since each link has custom colours as well, so you have to create individual gradients anyway. In contrast, if you have a node graph with hundreds of links and you want to use a gradient to show source versus target, this method creates an awful lot of extra DOM content.Einkorn
Just another note: If anyone does use the "objectBoundingBox" approach, you'll still need to check after moving a node whether or not you need to reverse the colours of the gradient, based on whether the source or the target is on the left or the right.Einkorn
I am using custom colors to color the rect using this code: pastebin.com/z09gPGLa but then the gradient stops working. can you please help?Maiduguri

© 2022 - 2024 — McMap. All rights reserved.