While alluvial/sankey diagrams may be useful for showing how finite resources flow through a closed system the traditional implementation cannot show any sort of change whether it be growth or decay between nodes. Alluvial-Growth seeks remedy by allowing the creator to show growth and decay over time as well as merges and divisions. Built on top of Mike Bostock’s implementation for Sankey this seeks to make a few small changes that create a new kind of graph.
Last active
July 27, 2021 12:50
-
-
Save ChrisManess/ebaacb5fd976657edad2 to your computer and use it in GitHub Desktop.
Alluvial-Growth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
d3.alluvialGrowth = function() { | |
var alluvialGrowth = {}, | |
nodeWidth = 24, | |
nodePadding = 8, | |
size = [1, 1], | |
nodes = [], | |
links = []; | |
alluvialGrowth.nodeWidth = function(_) { | |
if (!arguments.length) return nodeWidth; | |
nodeWidth = +_; | |
return alluvialGrowth; | |
}; | |
alluvialGrowth.nodePadding = function(_) { | |
if (!arguments.length) return nodePadding; | |
nodePadding = +_; | |
return alluvialGrowth; | |
}; | |
alluvialGrowth.nodes = function(_) { | |
if (!arguments.length) return nodes; | |
nodes = _; | |
return alluvialGrowth; | |
}; | |
alluvialGrowth.links = function(_) { | |
if (!arguments.length) return links; | |
links = _; | |
return alluvialGrowth; | |
}; | |
alluvialGrowth.size = function(_) { | |
if (!arguments.length) return size; | |
size = _; | |
return alluvialGrowth; | |
}; | |
alluvialGrowth.layout = function(iterations) { | |
computeNodeLinks(); | |
computeNodeValues(); | |
computeNodeBreadths(); | |
computeNodeDepths(iterations); | |
computeLinkDepths(); | |
return alluvialGrowth; | |
}; | |
alluvialGrowth.relayout = function() { | |
computeLinkDepths(); | |
return alluvialGrowth; | |
}; | |
alluvialGrowth.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, | |
ytr = d.target.y + d.ety, | |
ybr = ytr + d.edy, | |
ybl = y0 + d.dy; | |
return "M" + x0 + "," + y0 //top left corner | |
+ "C" + x2 + "," + y0 //top left curve | |
+ " " + x3 + "," + ytr //top right curve | |
+ " " + x1 + "," + ytr //Top right corner | |
+ "L" + x1 + "," + ybr //bottom right corner | |
+ "C" + x3 + "," + ybr //bottom right curve | |
+ " " + x2 + "," + ybl //bottom left curve | |
+ " " + x0 + "," + ybl //bottom left corner | |
+ "L" + x0 + "," + (y0); | |
} | |
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, endValue), | |
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) { | |
if(typeof link.endValue === "undefined"){ | |
link.edy = link.value * ky; | |
} else { | |
link.edy = link.endValue * ky; //added this in to calculate the ending dy | |
} | |
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, ety = 0; | |
node.sourceLinks.forEach(function(link) { | |
link.sy = sy; | |
sy += link.dy; | |
}); | |
node.targetLinks.forEach(function(link) { | |
link.ety = ety; | |
ety += link.edy; | |
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; | |
} | |
function endValue(link) { | |
return link.endValue; | |
} | |
return alluvialGrowth; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{"nodes":[ | |
{"name":"Node 0", "id":0}, | |
{"name":"Node 1", "id":1}, | |
{"name":"Node 2", "id":2}, | |
{"name":"Node 3", "id":3}, | |
{"name":"Node 4", "id":4}, | |
{"name":"Node 5", "id":5}, | |
{"name":"Node 6", "id":6}, | |
{"name":"Node 7", "id":7}, | |
{"name":"Node 8", "id":8}, | |
{"name":"Node 9", "id":9}, | |
{"name":"Node 10", "id":10}, | |
{"name":"Node 11", "id":11} | |
],"links":[ | |
{"source":0,"target":1,"value":1, "endValue": 1}, | |
{"source":1,"target":2,"value":1, "endValue": 3}, | |
{"source":2,"target":3,"value":1, "endValue": 5}, | |
{"source":4,"target":5,"value":1, "endValue": 1}, | |
{"source":5,"target":6,"value":1, "endValue": 3}, | |
{"source":2,"target":3,"value":1, "endValue": 5}, | |
{"source":7,"target":8,"value":1, "endValue": 4}, | |
{"source":8,"target":3,"value":4, "endValue": 1}, | |
{"source":9,"target":10,"value":1, "endValue": 1}, | |
{"source":10,"target":11,"value":1, "endValue": 3}, | |
{"source":2,"target":6,"value":1, "endValue": 3} | |
]} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>alluvialGrowth Diagram</title> | |
<script data-require="[email protected]" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script> | |
<style> | |
@import url(http://fonts.googleapis.com/css?family=PT+Serif|PT+Serif:b|PT+Serif:i|PT+Sans|PT+Sans:b); | |
svg { | |
font: 10px 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; | |
fill-opacity: .2; | |
} | |
.link:hover { | |
stroke-opacity: .5 !important; | |
} | |
.axis text { | |
font: 10px sans-serif; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #000; | |
shape-rendering: crispEdges; | |
} | |
</style> | |
</head> | |
<body> | |
<p id="chart"> | |
<script src="alluvial-growth.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 alluvialGrowth = d3.alluvialGrowth() | |
.nodeWidth(15) | |
.nodePadding(30) | |
.size([width, height]); | |
var path = alluvialGrowth.link(); | |
d3.json("data.json", function(data) { | |
alluvialGrowth | |
.nodes(data.nodes) | |
.links(data.links) | |
.layout(32); | |
var link = svg.append("g").selectAll(".link") | |
.data(data.links) | |
.enter().append("path") | |
.attr("class", "link") | |
.attr("d", path) | |
.attr("id", function(d,i){ | |
d.id = i; | |
return "link-"+i; | |
}) | |
.style("fill", function(d) { return "steelblue";}) | |
.style("stroke-width", 0) | |
.sort(function(a, b) { return b.dy - a.dy; }); | |
link.append("title") | |
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); }); | |
var node = svg.append("g").selectAll(".node") | |
.data(data.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", alluvialGrowth.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) { 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 + alluvialGrowth.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))) + ")"); | |
alluvialGrowth.relayout(); | |
link.attr("d", path); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment