From e369524eec30a9fff23f597b3f6903dc7d325f26 Mon Sep 17 00:00:00 2001 From: michaelgasser Date: Tue, 27 Jan 2015 12:15:47 +0100 Subject: [PATCH 1/8] Nicer link start / end - Links defined with 2 additional points. The links starts and ends with a short horizontal part, ensuring that arrows can be placed properly. - Minimal curvature to ensure nicer visuals for links going backwards (so far only if you move a target node more left than a source node (proper backlinks not implemented yet). --- sankey/sankey.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sankey/sankey.js b/sankey/sankey.js index abe137b..8f933fe 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -56,16 +56,20 @@ d3.sankey = function() { 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), + x4 = x0 + d.source.dx, + x5 = x1 - d.target.dx, + xi = d3.interpolateNumber(x0, x1), + x2 = Math.max(xi(curvature), x4+d.dy), + x3 = Math.min(xi(curvature), x5-d.dy), y0 = d.source.y + d.sy + d.dy / 2, y1 = d.target.y + d.ty + d.dy / 2; return "M" + x0 + "," + y0 - + "C" + x2 + "," + y0 - + " " + x3 + "," + y1 - + " " + x1 + "," + y1; - } + + "L" + x4 + "," + y0 + + "C" + x2 + "," + y0 + + " " + x3 + "," + y1 + + " " + x5 + "," + y1 + + "L" + x1 + "," + y1; + }; link.curvature = function(_) { if (!arguments.length) return curvature; From 07c529340b55edbd1fecc810bc3f2ea1ae703701 Mon Sep 17 00:00:00 2001 From: michaelgasser Date: Tue, 27 Jan 2015 13:29:05 +0100 Subject: [PATCH 2/8] Revert "Nicer link start / end" This reverts commit e369524eec30a9fff23f597b3f6903dc7d325f26. --- sankey/sankey.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/sankey/sankey.js b/sankey/sankey.js index 8f933fe..abe137b 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -56,20 +56,16 @@ d3.sankey = function() { function link(d) { var x0 = d.source.x + d.source.dx, x1 = d.target.x, - x4 = x0 + d.source.dx, - x5 = x1 - d.target.dx, - xi = d3.interpolateNumber(x0, x1), - x2 = Math.max(xi(curvature), x4+d.dy), - x3 = Math.min(xi(curvature), x5-d.dy), + xi = d3.interpolateNumber(x0, x1), + x2 = xi(curvature), + x3 = xi(1 - curvature), y0 = d.source.y + d.sy + d.dy / 2, y1 = d.target.y + d.ty + d.dy / 2; return "M" + x0 + "," + y0 - + "L" + x4 + "," + y0 - + "C" + x2 + "," + y0 - + " " + x3 + "," + y1 - + " " + x5 + "," + y1 - + "L" + x1 + "," + y1; - }; + + "C" + x2 + "," + y0 + + " " + x3 + "," + y1 + + " " + x1 + "," + y1; + } link.curvature = function(_) { if (!arguments.length) return curvature; From b3fb2d49a332fddf1b6b24cc3c62be0e8f5af531 Mon Sep 17 00:00:00 2001 From: michaelgasser Date: Tue, 27 Jan 2015 13:32:58 +0100 Subject: [PATCH 3/8] Nicer links as before, now without tab --- sankey/sankey.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/sankey/sankey.js b/sankey/sankey.js index abe137b..fdbdb33 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -56,16 +56,20 @@ d3.sankey = function() { 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), + x4 = x0 + d.source.dx, + x5 = x1 - d.target.dx, + xi = d3.interpolateNumber(x0, x1), + x2 = Math.max(xi(curvature), x4+d.dy), + x3 = Math.min(xi(curvature), x5-d.dy), y0 = d.source.y + d.sy + d.dy / 2, y1 = d.target.y + d.ty + d.dy / 2; return "M" + x0 + "," + y0 + + "L" + x4 + "," + y0 + "C" + x2 + "," + y0 + " " + x3 + "," + y1 - + " " + x1 + "," + y1; - } + + " " + x5 + "," + y1 + + "L" + x1 + "," + y1; + }; link.curvature = function(_) { if (!arguments.length) return curvature; From e153a734cc5671355149160d06f6e518070bad4c Mon Sep 17 00:00:00 2001 From: michaelgasser Date: Wed, 28 Jan 2015 09:04:12 +0100 Subject: [PATCH 4/8] vertical link support added support for vertical target links using the attribute vertical --- sankey/sankey.js | 73 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/sankey/sankey.js b/sankey/sankey.js index fdbdb33..8d9c279 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -53,8 +53,43 @@ 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. function link(d) { - var x0 = d.source.x + d.source.dx, + if (d.target.vertical) { + var x0 = d.source.x + d.source.dx, + x1 = d.target.x + d.dy/2, + x4 = x0 + d.source.dx, + x5 = x1, + xi = d3.interpolateNumber(x0, x1), + x2 = Math.max(xi(curvature), x4+d.dy), + x3 = x1, + y0 = d.source.y + d.sy + d.dy / 2, + y1 = d.target.y + d.ty + d.dy / 2, + y4 = y0, + y2 = y0, + deltay = y1-y0; + y5; + if (deltay > 0) { + y1 = d.target.y; + y5 = y1 - d.target.dx; + } + else { + y1 = d.target.y + d.source.dx; + y5 = y1 + d.target.dx; + }; + var yi = d3.interpolateNumber(y0, y1); + if (deltay > 0) { + y3 = Math.min(yi(curvature), y5-d.dy); + } + else { + y3 = Math.max(yi(curvature), y5+d.dy); + }; + } + + else { + var x0 = d.source.x + d.source.dx, x1 = d.target.x, x4 = x0 + d.source.dx, x5 = x1 - d.target.dx, @@ -62,12 +97,18 @@ d3.sankey = function() { x2 = Math.max(xi(curvature), x4+d.dy), x3 = Math.min(xi(curvature), x5-d.dy), y0 = d.source.y + d.sy + d.dy / 2, - y1 = d.target.y + d.ty + d.dy / 2; + y1 = d.target.y + d.ty + d.dy / 2, + y4 = y0, + y5 = y1, + y2 = y0, + y3 = y1; + } + return "M" + x0 + "," + y0 - + "L" + x4 + "," + y0 - + "C" + x2 + "," + y0 - + " " + x3 + "," + y1 - + " " + x5 + "," + y1 + + "L" + x4 + "," + y4 + + "C" + x2 + "," + y2 + + " " + x3 + "," + y3 + + " " + x5 + "," + y5 + "L" + x1 + "," + y1; }; @@ -110,7 +151,8 @@ d3.sankey = function() { // 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, @@ -147,7 +189,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; } } }); } @@ -158,6 +201,8 @@ d3.sankey = function() { }); } + // 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; }) @@ -165,7 +210,6 @@ d3.sankey = function() { .entries(nodes) .map(function(d) { return d.values; }); - // initializeNodeDepth(); resolveCollisions(); for (var alpha = 1; iterations > 0; --iterations) { @@ -174,7 +218,8 @@ d3.sankey = function() { relaxLeftToRight(alpha); resolveCollisions(); } - + moveVerticalDown(); + function initializeNodeDepth() { var ky = d3.min(nodesByBreadth, function(nodes) { return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value); @@ -258,6 +303,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() { From a90e67ead781c62f74641747e6c2dbec60a62d9e Mon Sep 17 00:00:00 2001 From: michaelgasser Date: Wed, 28 Jan 2015 09:33:42 +0100 Subject: [PATCH 5/8] Restructured link() - rearranged variable usage for x0-5 and y0-5 to make more sense. - changed flow of code to make variable assignments more clear. - curve interpolation now from x,y 1 to x,y 4. --- sankey/sankey.js | 71 ++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/sankey/sankey.js b/sankey/sankey.js index 8d9c279..6f17dd1 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -56,60 +56,65 @@ d3.sankey = function() { // 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) { if (d.target.vertical) { var x0 = d.source.x + d.source.dx, - x1 = d.target.x + d.dy/2, - x4 = x0 + d.source.dx, - x5 = x1, - xi = d3.interpolateNumber(x0, x1), - x2 = Math.max(xi(curvature), x4+d.dy), - x3 = x1, + 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, - y4 = y0, - y2 = y0, - deltay = y1-y0; - y5; - if (deltay > 0) { - y1 = d.target.y; - y5 = y1 - d.target.dx; + 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 { - y1 = d.target.y + d.source.dx; - y5 = y1 + d.target.dx; + y5 = d.target.y + d.source.dx; + y4 = y5 + d.target.dx; }; - var yi = d3.interpolateNumber(y0, y1); - if (deltay > 0) { - y3 = Math.min(yi(curvature), y5-d.dy); + var yi = d3.interpolateNumber(y1, y4); + if (deltaY > 0) { + y3 = Math.min(yi(curvature), y4-d.dy); } else { - y3 = Math.max(yi(curvature), y5+d.dy); + y3 = Math.max(yi(curvature), y4+d.dy); }; } else { var x0 = d.source.x + d.source.dx, - x1 = d.target.x, - x4 = x0 + d.source.dx, - x5 = x1 - d.target.dx, - xi = d3.interpolateNumber(x0, x1), - x2 = Math.max(xi(curvature), x4+d.dy), - x3 = Math.min(xi(curvature), x5-d.dy), + 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+d.dy), + x3 = Math.min(xi(curvature), x4-d.dy), y0 = d.source.y + d.sy + d.dy / 2, - y1 = d.target.y + d.ty + d.dy / 2, - y4 = y0, - y5 = y1, + y1 = y0, + y5 = d.target.y + d.ty + d.dy / 2, + y4 = y5, y2 = y0, - y3 = y1; + y3 = y5; } return "M" + x0 + "," + y0 - + "L" + x4 + "," + y4 + + "L" + x1 + "," + y1 + "C" + x2 + "," + y2 + " " + x3 + "," + y3 - + " " + x5 + "," + y5 - + "L" + x1 + "," + y1; + + " " + x4 + "," + y4 + + "L" + x5 + "," + y5; }; link.curvature = function(_) { From 458819a32c6dfbdd4ccf8975adbf80acbe86dd12 Mon Sep 17 00:00:00 2001 From: michaelgasser Date: Fri, 30 Jan 2015 15:02:17 +0100 Subject: [PATCH 6/8] allow manual x positioning - If a posX attribute is specified in the node definition it will be used to set the node depth. The values specified are scaled in order to use the whole svg canvas. - Using absoute placements also allows for backlinks (albeit not that nice yet). --- sankey/sankey.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sankey/sankey.js b/sankey/sankey.js index 6f17dd1..55f7907 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -39,7 +39,8 @@ d3.sankey = function() { sankey.layout = function(iterations) { computeNodeLinks(); computeNodeValues(); - computeNodeBreadths(); + // use posX is specified in node definition (needs to be specified for all nodes). + if ("posX" in nodes[1]) {setNodeBreadths();} else {computeNodeBreadths();}; computeNodeDepths(iterations); computeLinkDepths(); return sankey; @@ -153,6 +154,17 @@ 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 From 7e822803731f3b2ef51c2eaf9af7fc327d245baf Mon Sep 17 00:00:00 2001 From: michaelgasser Date: Mon, 2 Feb 2015 14:15:38 +0100 Subject: [PATCH 7/8] allow manual y positioning using the posY attribute in the note defintion, the node depth (y location) can be set. Crude implementation, but it works --- sankey/sankey.js | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/sankey/sankey.js b/sankey/sankey.js index 55f7907..a25e975 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -39,9 +39,10 @@ d3.sankey = function() { sankey.layout = function(iterations) { computeNodeLinks(); computeNodeValues(); - // use posX is specified in node definition (needs to be specified for all nodes). - if ("posX" in nodes[1]) {setNodeBreadths();} else {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; }; @@ -217,7 +218,7 @@ 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) { @@ -226,25 +227,40 @@ d3.sankey = function() { .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; }); }); From 4ba84b36e501bfa3ef0c7aaae88ea8cb6fa63aea Mon Sep 17 00:00:00 2001 From: michaelgasser Date: Mon, 2 Feb 2015 15:09:55 +0100 Subject: [PATCH 8/8] better link() straigth path segments extended for better visualisation of "fat" links. --- sankey/sankey.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sankey/sankey.js b/sankey/sankey.js index a25e975..f9ca751 100644 --- a/sankey/sankey.js +++ b/sankey/sankey.js @@ -88,10 +88,10 @@ d3.sankey = function() { }; var yi = d3.interpolateNumber(y1, y4); if (deltaY > 0) { - y3 = Math.min(yi(curvature), y4-d.dy); + y3 = Math.min(yi(curvature), y4-2*d.dy); } else { - y3 = Math.max(yi(curvature), y4+d.dy); + y3 = Math.max(yi(curvature), y4+2*d.dy); }; } @@ -101,8 +101,8 @@ d3.sankey = function() { x5 = d.target.x, x4 = x5 - d.target.dx, xi = d3.interpolateNumber(x1, x4), - x2 = Math.max(xi(curvature), x1+d.dy), - x3 = Math.min(xi(curvature), x4-d.dy), + 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,