Skip to content

Instantly share code, notes, and snippets.

@danharr
Last active August 29, 2015 14:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danharr/af796d91926d254dfe99 to your computer and use it in GitHub Desktop.
Save danharr/af796d91926d254dfe99 to your computer and use it in GitHub Desktop.
Position nodes horizontally in Sankey diagram

Solution is not finished and needs more understanding. Trying to position the nodes horizontally in a sankey diagram according to data. Hard coding these values for now at the bottom:

graph.links[3].source.x = 200; graph.links[7].source.x = 100; graph.links[7].target.x = 500; graph.links[5].target.x = 800;

then updating the links and rects. Oddly, the rects know to position themselves automatically. I would have thought I'd need to have changed the data first:

graph.nodes[5].dx = 800 etc //but this isn't necessary it seems, not sure yet why

I've disabled drag functionality too as the labels get all jumbled up otherwise

added back drag functionality, had to comment out this line which was affecting node order //.on("dragstart", function() { this.parentNode.appendChild(this); })
Name SourceApp TargetApp StartDate EndDate Status
Name m1 p1 01-Jan-15 30-Jun-15 red
Name p1 m2 01-Jan-15 30-Jun-15 green
Name p1 tdw 01-Jan-15 30-Jun-15 red
Name m1 m2 01-Jan-15 30-Jun-15 green
Name c1 p2 01-Jan-15 30-Jun-15 green
Name p2 tdw 01-Jan-15 30-Jun-15 green
Name new tdw 01-Jan-15 30-Jun-15 green
Name new other 01-Jan-15 30-Jun-15 green
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Applications drafts</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="sankey.js"></script>
<style>
.node rect {
cursor: move;
fill-opacity: .9;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 0 1px 0 #fff;
font-family: ChevinLight;
}
.link {
fill: none;
/* stroke: #4A0000;*/
stroke-opacity: .2;
}
.link:hover {
stroke-opacity: .8;
}
.toggle {
font-size: 20px;
font-family: arial;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 10, right: 10, bottom: 10, left: 10},
width = 1200 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// Set the sankey diagram properties
var sankey = d3.sankey()
.nodeWidth(36)
.nodePadding(40)
.size([width, height]);
var path = sankey.link();
var timelineScale = d3.time.scale()
.domain([new Date(2014,01,01), new Date(2014,12,31)])
.range(["red", "blue"]);
d3.csv("data.csv",function(error,csv) {
// Parse the date / time
var parseDate = d3.time.format("%d-%b-%y").parse;
csv.forEach(function(d) {
d.StartDate = parseDate(d.StartDate);
d.EndDate = parseDate(d.EndDate);
})
//set up graph data but empty
graph = {"nodes" : [], "links" : []};
csv.forEach(function (d) {
graph.nodes.push({ "name": d.SourceApp});
graph.nodes.push({ "name": d.TargetApp});
graph.links.push({ "source": d.SourceApp,
"target": d.TargetApp ,
"project": d.Name,
"value":4,
"linkCol":d.Status});
});
// return only the distinct / unique nodes - equivalent of a dedupe
graph.nodes = d3.keys(d3.nest()
.key(function (d) { return d.name ; })
.map(graph.nodes));
// loop through each link replacing the text with its index from node
graph.links.forEach(function (d, i) {
graph.links[i].source = graph.nodes.indexOf(graph.links[i].source);
graph.links[i].target = graph.nodes.indexOf(graph.links[i].target);
});
//now loop through each node to make nodes an array of objects
// rather than an array of strings
graph.nodes.forEach(function (d, i) {
graph.nodes[i] = { "name": d };
});
graph.nodes.forEach(function (d) {
if (d.name.charAt(0) == '_') {d.col = "#E9E9E9"} else {d.col = "blue"};
if (d.name.charAt(0) == '_') {d.name = ""};
})
console.log(graph.nodes);
viz(graph);
})
function viz(incoming) {
sankey.nodes(incoming.nodes).links(incoming.links).layout(32);
// append the svg canvas to the page
var svg = d3.select('body').append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// add in the links
var link = svg.append("g").selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("stroke",function(d) {return d.linkCol})
.style("stroke-width", function(d) { return Math.max(1, d.dy); })
.sort(function(a, b) { return b.dy - a.dy; });
// add the link titles
link.append("title")
.text(function(d) {
return d.project + ":" + d.source.name + " → " +
d.target.name ; });
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
//.on("dragstart", function() { this.parentNode.appendChild(this); })
.on("drag", dragmove);
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", function(d){return d.name + " node"})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; })
.call(drag);
// add the rectangles for the nodes
node.append("rect")
.attr("height", function(d) { return d.dy; })
.attr("width", sankey.nodeWidth())
.style("fill", function(d) { return d.col });
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function(d) { return d.name; })
.filter(function(d) { return d.x < width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
// the function for moving the nodes
function dragmove(d) {
d3.select(this).attr("transform",
"translate(" + (
d.x = Math.max(0, Math.min(width - d.dx, d3.event.x))
) + "," + (
d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))
) + ")");
sankey.relayout();
link.attr("d", path);
}
var button = d3.select('body').append("p").html("Click me to see the nodes move to a new position").attr("class","toggle");
button.on("click",test);
function test() {
// var candidate = d3.select(".p2");
// candidate.attr("transform",
// "translate(" + (
// this.x = 200
// ) + "," + (
// this.y = 200
// ) + ")");
// sankey.relayout();
// link.attr("d", path);
//console.log(link.attr("d"));
//link data needs to change before we'll see a change in path
//manually set x position for links, node position data seem to adjust automatically...
graph.links[3].source.x = 200;
graph.links[7].source.x = 100;
graph.links[7].target.x = 500;
graph.links[5].target.x = 800;
// add in the nodes
d3.selectAll(".node")
.data(graph.nodes)
.transition()
.duration(2000)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
d3.selectAll(".link")
.data(graph.links)
.transition()
.duration(2000)
.attr("d", path)
.sort(function(a, b) { return b.dy - a.dy; });
//console.log(link.style("stroke"));
//console.log(path);
}
};
</script>
</body>
</html>
d3.sankey = function() {
var sankey = {},
nodeWidth = 24,
nodePadding = 8,
size = [1, 1],
nodes = [],
links = [];
sankey.nodeWidth = function(_) {
if (!arguments.length) return nodeWidth;
nodeWidth = +_;
return sankey;
};
sankey.nodePadding = function(_) {
if (!arguments.length) return nodePadding;
nodePadding = +_;
return sankey;
};
sankey.nodes = function(_) {
if (!arguments.length) return nodes;
nodes = _;
return sankey;
};
sankey.links = function(_) {
if (!arguments.length) return links;
links = _;
return sankey;
};
sankey.size = function(_) {
if (!arguments.length) return size;
size = _;
return sankey;
};
sankey.layout = function(iterations) {
computeNodeLinks();
computeNodeValues();
computeNodeBreadths();
computeNodeDepths(iterations);
computeLinkDepths();
return sankey;
};
sankey.relayout = function() {
computeLinkDepths();
return sankey;
};
sankey.link = function() {
var curvature = .5;
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),
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;
}
link.curvature = function(_) {
if (!arguments.length) return curvature;
curvature = +_;
return link;
};
return link;
};
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks() {
nodes.forEach(function(node) {
node.sourceLinks = [];
node.targetLinks = [];
});
links.forEach(function(link) {
var source = link.source,
target = link.target;
if (typeof source === "number") source = link.source = nodes[link.source];
if (typeof target === "number") target = link.target = nodes[link.target];
source.sourceLinks.push(link);
target.targetLinks.push(link);
});
}
// Compute the value (size) of each node by summing the associated links.
function computeNodeValues() {
nodes.forEach(function(node) {
node.value = Math.max(
d3.sum(node.sourceLinks, value),
d3.sum(node.targetLinks, value)
);
});
}
// 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.
function computeNodeBreadths() {
var remainingNodes = nodes,
nextNodes,
x = 0;
while (remainingNodes.length) {
nextNodes = [];
remainingNodes.forEach(function(node) {
node.x = x;
node.dx = nodeWidth;
node.sourceLinks.forEach(function(link) {
if (nextNodes.indexOf(link.target) < 0) {
nextNodes.push(link.target);
}
});
});
remainingNodes = nextNodes;
++x;
}
//
moveSinksRight(x);
scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
}
function moveSourcesRight() {
nodes.forEach(function(node) {
if (!node.targetLinks.length) {
node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
}
});
}
function moveSinksRight(x) {
nodes.forEach(function(node) {
if (!node.sourceLinks.length) {
node.x = x - 1;
}
});
}
function scaleNodeBreadths(kx) {
nodes.forEach(function(node) {
node.x *= kx;
});
}
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);
resolveCollisions();
}
function initializeNodeDepth() {
var ky = d3.min(nodesByBreadth, function(nodes) {
return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
});
nodesByBreadth.forEach(function(nodes) {
nodes.forEach(function(node, i) {
node.y = i;
node.dy = node.value * ky;
});
});
links.forEach(function(link) {
link.dy = link.value * ky;
});
}
function relaxLeftToRight(alpha) {
nodesByBreadth.forEach(function(nodes, breadth) {
nodes.forEach(function(node) {
if (node.targetLinks.length) {
var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedSource(link) {
return center(link.source) * link.value;
}
}
function relaxRightToLeft(alpha) {
nodesByBreadth.slice().reverse().forEach(function(nodes) {
nodes.forEach(function(node) {
if (node.sourceLinks.length) {
var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedTarget(link) {
return center(link.target) * link.value;
}
}
function resolveCollisions() {
nodesByBreadth.forEach(function(nodes) {
var node,
dy,
y0 = 0,
n = nodes.length,
i;
// Push any overlapping nodes down.
nodes.sort(ascendingDepth);
for (i = 0; i < n; ++i) {
node = nodes[i];
dy = y0 - node.y;
if (dy > 0) node.y += dy;
y0 = node.y + node.dy + nodePadding;
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y0 - nodePadding - size[1];
if (dy > 0) {
y0 = node.y -= dy;
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i];
dy = node.y + node.dy + nodePadding - y0;
if (dy > 0) node.y -= dy;
y0 = node.y;
}
}
});
}
function ascendingDepth(a, b) {
return a.y - b.y;
}
}
function computeLinkDepths() {
nodes.forEach(function(node) {
node.sourceLinks.sort(ascendingTargetDepth);
node.targetLinks.sort(ascendingSourceDepth);
});
nodes.forEach(function(node) {
var sy = 0, ty = 0;
node.sourceLinks.forEach(function(link) {
link.sy = sy;
sy += link.dy;
});
node.targetLinks.forEach(function(link) {
link.ty = ty;
ty += link.dy;
});
});
function ascendingSourceDepth(a, b) {
return a.source.y - b.source.y;
}
function ascendingTargetDepth(a, b) {
return a.target.y - b.target.y;
}
}
function center(node) {
return node.y + node.dy / 2;
}
function value(link) {
return link.value;
}
return sankey;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment