See
- Altered Sankey plugin: https://github.com/briantjacobs/d3-plugins/tree/master/sankey
- Blog post http://briantjacobs.com/d3-sankey-schema
See
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
<meta charset="utf-8"> | |
<title>Sankey Diagram</title> | |
<style> | |
body {font-family: sans-serif;} | |
#chart { | |
height: 500px; | |
} | |
.node rect { | |
cursor: move; | |
fill-opacity: .9; | |
shape-rendering: crispEdges; | |
} | |
.node text { | |
pointer-events: none; | |
text-shadow: 0 1px 0 #fff; | |
} | |
.link { | |
fill: none; | |
stroke: #000; | |
stroke-opacity: .2; | |
} | |
.link:hover { | |
stroke-opacity: .5; | |
} | |
</style> | |
</head> | |
<body> | |
<p id="chart"></p> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.2/d3.min.js"></script> | |
<script src="https://rawgithub.com/briantjacobs/d3-plugins/master/sankey/sankey.js"></script> | |
<script> | |
var margin = {top: 1, right: 1, bottom: 6, left: 1}, | |
width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
var formatNumber = d3.format(",.0f"), | |
format = function(d) { return formatNumber(d) + " TWh"; }, | |
color = d3.scale.category20(); | |
var svg = d3.select("#chart").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 + ")"); | |
var sankey = d3.sankey() | |
.nodeWidth(15) | |
.nodePadding(10) | |
.size([width, height]) | |
.schema({ | |
id: "type_id", | |
source: "fuel_origin", | |
target: "fuel_dest", | |
value: "val" | |
}); | |
var path = sankey.link(); | |
//incomplete extract from | |
//http://bost.ocks.org/mike/sankey/energy.json | |
//http://www.decc.gov.uk/en/content/cms/tackling/2050/calculator_on/calculator_on.aspx" | |
var energy = {"nodes":[ | |
{"type_id": 12, "name":"Agricultural 'waste'"}, | |
{"type_id": 16, "name":"Bio-conversion"}, | |
{"type_id": 17, "name":"Liquid"}, | |
{"type_id": 10, "name":"Losses"}, | |
{"type_id": 19, "name":"Solid"}, | |
{"type_id": 13, "name":"Gas"}, | |
{"type_id": 14, "name":"Biofuel imports"} | |
], | |
"links": [ | |
{"fuel_origin":12,"fuel_dest":16,"val":124.729}, | |
{"fuel_origin":16,"fuel_dest":17,"val":0.597}, | |
{"fuel_origin":16,"fuel_dest":10,"val":26.862}, | |
{"fuel_origin":16,"fuel_dest":19,"val":280.322}, | |
{"fuel_origin":16,"fuel_dest":13,"val":81.144}, | |
{"fuel_origin":14,"fuel_dest":17,"val":35} | |
] | |
}; | |
sankey | |
.nodes(energy.nodes) | |
.links(energy.links) | |
.layout(32); | |
var link = svg.append("g").selectAll(".link") | |
.data(energy.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; }); | |
link.append("title") | |
.text(function(d) { return d.fuel_origin.name + " → " + d.fuel_dest.name + "\n" + format(d.value); }); | |
var node = svg.append("g").selectAll(".node") | |
.data(energy.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)); | |
node.append("rect") | |
.attr("height", function(d) { return d.dy; }) | |
.attr("width", sankey.nodeWidth()) | |
.style("fill", function(d) {console.log(d); return d.color = color(d.name.replace(/ .*/, "")); }) | |
.style("stroke", function(d) { return d3.rgb(d.color).darker(2); }) | |
.append("title") | |
.text(function(d) { return d.name + "\n" + format(d.value); }); | |
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"); | |
function dragmove(d) { | |
d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")"); | |
sankey.relayout(); | |
link.attr("d", path); | |
} | |
</script> | |
</body></html> |
One issue that comes to mind, though, is when the nodes
involve more than one category or table of data such that the id's from the database could overlap. What would be the best approach in this scenario?
I suppose one approach is, if your database id columns are integer
data points, you could add a varchar
prefix to the id (uniquely differentiating each of the source tables), and thus each identifier field in a node would be unique.
Thanks for writing the blog post and plugin.
After searching quite a while for any way to better understand the cryptic d3 sankey data structure so that I could try pulling data from a database to fit the structure, your post and solution are the best material I've come across.