diff --git a/sankey/sankey.js b/sankey/sankey.js index abe137b..f9ca751 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -39,8 +39,10 @@ d3.sankey = function() { sankey.layout = function(iterations) { computeNodeLinks(); computeNodeValues(); - computeNodeBreadths(); - computeNodeDepths(iterations); + // use posX /posY if specified in node definition. + // posX /posY needs to be specified for all nodes, but is only checked in first). + if ("posX" in nodes[1]) {setNodeBreadths();} else {computeNodeBreadths();}; + computeNodeDepths(iterations); computeLinkDepths(); return sankey; }; @@ -53,19 +55,69 @@ d3.sankey = function() { sankey.link = function() { var curvature = .5; + // Determine the link x,y values. + // Links are made up of 2 small straight elements at beginning and end + // with a curved element in between. + // (x0,y0), (x5,y5) are the start/end points of the links + // (x1,y1), (x4,y4) change between straight and curved elements + // (x2,y2), (x3,y3) curve helper points + // Nodes with the attribute vertical have vertical incoming links, + // either from the top or bottom, depending on deltaY. function link(d) { - var x0 = d.source.x + d.source.dx, - x1 = d.target.x, - xi = d3.interpolateNumber(x0, x1), - x2 = xi(curvature), - x3 = xi(1 - curvature), + if (d.target.vertical) { + var x0 = d.source.x + d.source.dx, + x1 = x0 + d.source.dx, + x5 = d.target.x + d.dy/2, + x4 = x5, + xi = d3.interpolateNumber(x1, x4), + x2 = Math.max(xi(curvature), x1+d.dy), + x3 = x5, y0 = d.source.y + d.sy + d.dy / 2, - y1 = d.target.y + d.ty + d.dy / 2; + y1 = y0, + y5 = d.target.y + d.ty + d.dy / 2, + y4, + deltaY = y5-y0, + y2 = y0; + if (deltaY > 0) { + y5 = d.target.y; + y4 = y5 - d.target.dx; + } + else { + y5 = d.target.y + d.source.dx; + y4 = y5 + d.target.dx; + }; + var yi = d3.interpolateNumber(y1, y4); + if (deltaY > 0) { + y3 = Math.min(yi(curvature), y4-2*d.dy); + } + else { + y3 = Math.max(yi(curvature), y4+2*d.dy); + }; + } + + else { + var x0 = d.source.x + d.source.dx, + x1 = x0 + d.source.dx, + x5 = d.target.x, + x4 = x5 - d.target.dx, + xi = d3.interpolateNumber(x1, x4), + x2 = Math.max(xi(curvature), x1+2*d.dy), + x3 = Math.min(xi(curvature), x4-2*d.dy), + y0 = d.source.y + d.sy + d.dy / 2, + y1 = y0, + y5 = d.target.y + d.ty + d.dy / 2, + y4 = y5, + y2 = y0, + y3 = y5; + } + return "M" + x0 + "," + y0 - + "C" + x2 + "," + y0 - + " " + x3 + "," + y1 - + " " + x1 + "," + y1; - } + + "L" + x1 + "," + y1 + + "C" + x2 + "," + y2 + + " " + x3 + "," + y3 + + " " + x4 + "," + y4 + + "L" + x5 + "," + y5; + }; link.curvature = function(_) { if (!arguments.length) return curvature; @@ -103,10 +155,22 @@ d3.sankey = function() { }); } + // Set breadth (x-position) for each node, if given in data. + function setNodeBreadths() { + var posXlist = [] + nodes.forEach(function(node) { + node.x = node.posX; + node.dx = nodeWidth; + posXlist.push(node.posX); + }); + scaleNodeBreadths((size[0] - nodeWidth) / Math.max.apply(null, posXlist)); + }; + // Iteratively assign the breadth (x-position) for each node. // Nodes are assigned the maximum breadth of incoming neighbors plus one; // nodes with no incoming links are assigned breadth zero, while - // nodes with no outgoing links are assigned the maximum breadth. + // nodes with no outgoing links are assigned the maximum breadth, except + // nodes with the vertical attribute, which are moved to the incoming neighbor plus 0.5 function computeNodeBreadths() { var remainingNodes = nodes, nextNodes, @@ -143,7 +207,8 @@ d3.sankey = function() { function moveSinksRight(x) { nodes.forEach(function(node) { if (!node.sourceLinks.length) { - node.x = x - 1; + if (node.vertical) { node.x = node.x -0.5;} + else { node.x = x - 1; } } }); } @@ -153,32 +218,49 @@ d3.sankey = function() { node.x *= kx; }); } - + + // Iteratively assign the depth (y-position) for each node + // Nodes with the vertical attribute are moved to the bottom function computeNodeDepths(iterations) { var nodesByBreadth = d3.nest() .key(function(d) { return d.x; }) .sortKeys(d3.ascending) .entries(nodes) .map(function(d) { return d.values; }); - - // + initializeNodeDepth(); - resolveCollisions(); - for (var alpha = 1; iterations > 0; --iterations) { - relaxRightToLeft(alpha *= .99); - resolveCollisions(); - relaxLeftToRight(alpha); + + if (!("posY" in nodes[1])) { resolveCollisions(); - } - + for (var alpha = 1; iterations > 0; --iterations) { + relaxRightToLeft(alpha *= .99); + resolveCollisions(); + relaxLeftToRight(alpha); + resolveCollisions(); + } + moveVerticalDown(); + }; + function initializeNodeDepth() { - var ky = d3.min(nodesByBreadth, function(nodes) { - return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value); + var posYlist = [ ], + maxPosY, + ky = d3.min(nodesByBreadth, function(nodes) { + return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value); + }); + + nodes.forEach(function(node) { + posYlist.push(node.posY) }); - + + maxPosY = Math.max(Math.max.apply(null, posYlist), 1); nodesByBreadth.forEach(function(nodes) { nodes.forEach(function(node, i) { - node.y = i; + if ("posY" in node) { + node.y = node.posY / maxPosY * size[1]; + } + else { + node.y = i; + }; node.dy = node.value * ky; }); }); @@ -254,6 +336,14 @@ d3.sankey = function() { function ascendingDepth(a, b) { return a.y - b.y; } + + function moveVerticalDown() { + nodesByBreadth.forEach(function(nodes) { + nodes.forEach(function(node, i) { + if ( node.vertical ) { node.y = size[1] - node.dy; }; + }); + }); + } } function computeLinkDepths() {