Skip to content

Instantly share code, notes, and snippets.

@ChrisManess
Last active July 27, 2021 12:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ChrisManess/ebaacb5fd976657edad2 to your computer and use it in GitHub Desktop.
Save ChrisManess/ebaacb5fd976657edad2 to your computer and use it in GitHub Desktop.
Alluvial-Growth
license: gpl-3.0

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.

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;
};
{"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}
]}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>alluvialGrowth Diagram</title>
<script data-require="d3@3.5.3" 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