d3 sankey charts - manually position node along x axis
Asked Answered
N

6

13

A problem I have come across using the d3 sankey implementation is that there's no way to specify where on the x axis a node is. I've been poking through the source and there isn't really a "clean" way to specify the x value on a reasonable scale (ie, 1-5 where the chart is 5 nodes wide). I am creating something that can be used like a course planner for education, so the x value will correspond with the semester. Supposing I had a course I couldn't take until my sophomore year of college, this would be at an x of 3 (1/2 are freshman, 3/4 sophomore, etc). The problem is, if there is nothing that links to this course beforehand, it will always be at an x of 1, so I would like to push it to the right two spaces.

I have noticed that the x value in the actual sankey chart does not reflect how many nodes across it is, so this is a bit difficult to do.

I've also come across this question, and I realize that by default the chart will not let me position a node. I have no problems tweaking the sankey.js example to accomplish this, but I'm stuck as to how to do so, currently.

Newhall answered 3/2, 2014 at 22:35 Comment(2)
There is a ticket for this: github.com/d3/d3-plugins/issues/88Shakespeare
Thanks for opening it @Thilo!Newhall
D
18

This is possible. See this JSFiddle.

enter image description here

The computeNodeBreadths function in sankey.js can be modified to look for an explicit x-position that has been assigned to a node (node.xPos):

  function computeNodeBreadths() {
    var remainingNodes = nodes,
        nextNodes,
        x = 0;

    while (remainingNodes.length) {
      nextNodes = [];
      remainingNodes.forEach(function(node) {

        if (node.xPos)
            node.x = node.xPos;
        else
            node.x = x;

        node.dx = nodeWidth;
        node.sourceLinks.forEach(function(link) {
          nextNodes.push(link.target);
        });
      });
      remainingNodes = nextNodes;
      ++x;
    }

    //
    moveSinksRight(x);
    scaleNodeBreadths((width - nodeWidth) / (x - 1));
  }

Then all you need to do is specify xPos on the desired nodes. In the above example I've set xPos = 1 on node2. See getData() in the JSFiddle example:

... }, {
        "node": 2,
        "name": "node2",
        "xPos": 1
    }, { ...
Dustidustie answered 4/2, 2014 at 0:16 Comment(3)
Thanks. I tried this, but I guess I needed to make some more tweaks in my data set to make it actually work. Your example affirmed that and helped me out greatly!Newhall
@Greg Ross - thanks for the solution. Is there any way to adjust sinks to NOT be on the right side of the svg? I noticed I cannot override the position of node 3 in your JSFiddle: jsfiddle.net/88Akd. Thank you.Sousa
Hey @Sousa - Good spot. If you comment out moveSinksRight(x); at line 137 in the fiddler, you can then place the sink node where you like.Dustidustie
A
4

In case anyone one will be looking for how-to fix the initial position of the deployed nodes (rects) you can do this:

sankey.layout = function(iterations) {
    computeNodeLinks();
    computeNodeValues();
    computeNodeBreadths();
    computeNodeDepths(iterations);
    computeAbsolutePositions(); // add this in sankey.js file
    computeLinkDepths();
    return sankey;
};

And add the positions themselves:

function computeAbsolutePositions() {
    nodes[0].x = 0;
    nodes[0].y = 0;
    nodes[1].x = 260;
    nodes[1].y = 0;
};

Please mind that you will have to yourself mind the positions of the rects when you hardcode them like that, as none collision check functions originally used by sankey.js are used in that example.

Hope that may help someone!

Abutment answered 24/4, 2015 at 8:8 Comment(0)
F
2

Let us call the x-coordinate layer. layer=0 will be left edge, and layer=n will be the right edge. In your JSON file, in the nodes field, add the key-value pair layer: your_desired_x_as_integer.

Then go the sankey.js file and find the function componentsByBreadth.forEach. Replace the line node.x = component.x + node.x; with:

if (node.layer) node.x=node.layer;
else node.x = component.x + node.x;

You can defined density of layers like this also, e.g. nodes placed on layers 0,1,2 or 0,4,8 will have a central node and two on the edges of the width of the sankey, but 0,1,5 will not have.

If you need more help, this feature, among many other is included in my D3 Sankey app: http://sankey.csaladen.es

Frustule answered 1/5, 2015 at 3:56 Comment(0)
S
0

might not be the clean answer you're after, but you can explicitly change the source and target x attributes, then update the links

http://bl.ocks.org/danharr/af796d91926d254dfe99

Samovar answered 30/10, 2014 at 17:14 Comment(0)
A
0

There's a modified version of d3-sankey that supports this (and some other features) which appears to be built against d3 v4:

https://github.com/ricklupton/d3-sankey-diagram#layout-rankSets

It includes a rankSets option which should allow for x-positioning.

However, I found some caveats with it:

  • Doesn't support the official API, so you can't, for example, change the shading of the links.
  • Built against d3 v4, not v7 (latest as if this comment)
  • Isn't api-compatible, so not a drop-in replacement.
  • Documentation looks suspiciously similar to official d3-sankey docs, so much so that I mistook it for offical at first.

There's an open issue that contains a version that's been upgraded to work with d3 v7, but it still isn't compatible with the official d3-sankey api.

On the official repo there are a couple of open tickets related to this, none of which have been merged, so if you want to keep compatibility with the main line, you'll have to fork the official branch and maintain your fork.

Acetophenetidin answered 31/1, 2022 at 13:21 Comment(0)
A
0

For d3 v7 & d3-sankey 0.12.3, patch d3-sankey.js to support setting the level of a node explicitly like this:

Replace computeNodeLayers in d3-sankey with the following function:

    //
    // Patch computeNodeLayers to use existing layer if specified (fixedLayer)
    //
    function computeNodeLayers({ nodes }) {
      const x = d3Array.max(nodes, d => d.depth) + 1;
      const kx = (x1 - x0 - dx) / (x - 1);
      const columns = new Array(x);
      for (const node of nodes) {
        // Patch: Override here
        var i = Math.max(0, Math.min(x - 1, Math.floor(align.call(null, node, x))));
        i = node.fixedLayer !== undefined ? node.fixedLayer : 1
        node.layer = i
        node.x0 = x0 + i * kx;
        node.x1 = node.x0 + dx;
        if (columns[i]) columns[i].push(node);
        else columns[i] = [node];
      }
      if (sort) for (const column of columns) {
        column.sort(sort);
      }
      return columns;
    }

If your node has a fixedLayer attribute, it will be used instead of the calculated node layer.

Applies to https://github.com/d3/d3-sankey v0.12.3 using d3v7

Acetophenetidin answered 31/1, 2022 at 15:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.