Skip to content

Instantly share code, notes, and snippets.

@johanneseckert
Forked from timelyportfolio/d3noob_energy.csv
Last active March 22, 2018 04:27
Show Gist options
  • Save johanneseckert/7667996 to your computer and use it in GitHub Desktop.
Save johanneseckert/7667996 to your computer and use it in GitHub Desktop.
Sankey Diagram with auto links

This is a fork of timelyportfolio's sankey with data from a csv source

I modified the sankey plugin to allow »auto« nodes that will inherit the value from the connecting node and use that as it's weight. This is great to create sankeys where only parts of the data is defined, while other streams compute automatically. Only the first column has real numbers, the other two sides are computed automatically (all links center and right inherit their values from their source).

see sankey.js #100 computeNodeValues()

This is probably a very rough way of doing it, but it works for my data (needed to group the nodes into a third category). This does not work if an auto link is made behind an auto node, as this will duplicate the output.

Thanks to d3noob.org and timelyportfolio

<!DOCTYPE html>
<meta charset="utf-8">
<title>SANKEY with auto links</title>
<style>
#graph {
font-family: "Helvetica Neue", sans-serif;
}
.node rect {
cursor: move;
fill-opacity: .9;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 0 0px 0 #fff;
font-size: 14px;
font-family: "Helvetica Neue", sans-serif;
}
.link {
fill: none;
stroke: #000;
stroke-opacity: .2;
}
.link:hover {
stroke-opacity: .5;
}
</style>
<body>
<div id="chart">Loading data …</div>
<script src="http://d3js.org/d3.v3.js"></script>
<script src="sankey.js"></script>
<script>
var units = "Units";
var margin = {top: 1, right: 1, bottom: 1, left: 1},
width = 950 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"), // zero decimal places
format = function(d) { return formatNumber(d) + " " + units; },
color = d3.scale.category20();
// append the svg canvas to the page
var svg = d3.select("#chart").html("").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 + ")");
// Set the sankey diagram properties
var sankey = d3.sankey()
.nodeWidth(12)
.nodePadding(20)
.size([width, height]);
var path = sankey.link();
// load the data with d3.csv instead of d3.json
d3.csv("kommunen_auto.csv", function(error, data) {
//set up graph in same style as original example but empty
graph = {"nodes" : [], "links" : []};
data.forEach(function (d, i) {
graph.nodes.push({ "name": d.source });
graph.nodes.push({ "name": d.target });
graph.links.push({ "source": d.source,
"target": d.target,
"value": d.value });
})
//thanks Mike Bostock https://groups.google.com/d/msg/d3-js/pl297cFtIQk/Eso4q_eBu1IJ
//this handy little function returns only the distinct / unique nodes
graph.nodes = d3.keys(d3.nest()
.key(function (d) { return d.name; })
.map(graph.nodes));
//it appears d3 with force layout wants a numeric source and target
//so 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 nodes to make nodes an array of objects rather than an array of strings
graph.nodes.forEach(function (d, i) {
graph.nodes[i] = { "name": d };
});
sankey
.nodes(graph.nodes)
.links(graph.links)
.layout(32);
// 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-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, i) {
return "link #" + (parseInt(i)+2) + "\n" +
d.source.name + " → " + d.target.name + "\n" +
format(d.value); });
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; })
.call(d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", function() {
this.parentNode.appendChild(this);
})
.on("drag", dragmove))
.on("mouseover", fade(0.2))
.on("mouseout", fade(1));
// 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.color = color(d.name.replace(/ .*/, "")); })
.style("stroke", function(d) {
return d3.rgb(d.color).darker(2); })
.append("title")
.text(function(d, i) {
return "node #" + i + "\n" + d.name + "\n" + format(d.value);
});
// 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);
};
// Returns an event handler for fading a given chord group.
// http://bl.ocks.org/mbostock/4062006
function fade(opacity) {
return function(g, i) {
var elements = svg.selectAll(".node");
elements = elements.filter(function(d) { return d.name != graph.nodes[i].name });
elements.transition()
.style("opacity", opacity);
svg.selectAll(".link")
.filter(function(d) { return d.source.name != graph.nodes[i].name && d.target.name != graph.nodes[i].name })
.transition()
.style("opacity", opacity);
};
}
});
</script>
</body>
</html>
source target value
Elvis Jasper 2
Elvis Marge 1
Elvis Boobarella 3
Bart Jasper 1
Bart Marge 1
Bart Homer 1
Lisa Jasper 1
Lisa Boobarella 2
Jasper Elementary School auto
Homer Elementary School auto
Boobarella Elementary School auto
Marge Krusty Burger auto
Elementary School Springfield auto
Krusty Burger Springfield auto
d3.sankey = function() {
var sankey = {},
nodeWidth = 24,
nodePadding = 1,
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, i) {
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() {
// finding links with auto value and inherit value from source node
links.forEach(function(link, i) {
if (link.value == "auto") {
// compute source's targetLinks to determine value for auto ones
autoValue = d3.sum(links[i].source.targetLinks, value);
link.value = autoValue;
console.log("found auto one at link #"+i+" and set to "+autoValue);
}
});
nodes.forEach(function(node) {
var input = d3.sum(node.sourceLinks, value),
output = d3.sum(node.targetLinks, value);
node.value = Math.max(input, output);
});
}
// 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) {
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;
};
@gouroujo
Copy link

Hey! thanks for your project, it was very helpfull!
I have made the following modification:

// Compute the value (size) of each node by summing the associated links.
function computeNodeValues() {
// finding links with auto value and inherit value from source node
links.forEach(function(link, i) {
if (isNaN(link.value)) {
// compute source's targetLinks to determine value for auto ones
autoValue = d3.sum(link.source.targetLinks, value)-d3.sum(link.source.sourceLinks, value);
link.value = autoValue;
console.log("found NaN link at #"+(i-1)+" and set to "+autoValue);
}
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment