Skip to content

Instantly share code, notes, and snippets.

@igorzilla
Created July 10, 2012 22:22
Show Gist options
  • Save igorzilla/3086583 to your computer and use it in GitHub Desktop.
Save igorzilla/3086583 to your computer and use it in GitHub Desktop.
Alluvial Diagram
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Alluvial Diagram</title>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js?2.5.0"></script>
<style type="text/css">
body {
margin: 1em;
}
.node {
stroke: #fff;
stroke-width: 2px;
}
.link {
fill: none;
stroke: #000;
opacity: .3;
}
.link.on {
stroke: #F00;
opacity: .7;
}
.node {
stroke: none;
}
</style>
</head>
<body>
<script type="text/javascript">
/* Make Fake Data */
var data = (function() {
var maxt = 5,
maxn = 12,
maxl = 4,
maxv = 10,
times = [],
allLinks = [],
counter = 0,
addNodes = function() {
var ncount = Math.random() * maxn + 1,
nodes = d3.range(0, ncount).map(function(n) {
return {
id: counter++,
nodeName: "Node " + n,
nodeValue: 0,
incoming: []
}
});
times.push(nodes);
return nodes;
},
addNext = function() {
var current = times[times.length-1],
nextt = addNodes();
// make links
current.forEach(function(n) {
var linkCount = Math.min(~~(Math.random() * maxl + 1), nextt.length),
breaks = d3.range(linkCount-1)
.map(function() { return Math.random() * n.nodeValue })
.sort(d3.ascending),
links = {},
target, link, x;
for (x=0; x<linkCount; x++) {
do {
target = nextt[~~(Math.random() * nextt.length)];
} while (target.id in links);
// add link
link = {
source: n.id,
target: target.id,
value: (breaks[x] || n.nodeValue) - (breaks[x-1] || 0)
};
links[target.id] = link;
allLinks.push(link);
target.nodeValue += link.value;
}
});
// prune next
times[times.length-1] = nextt.filter(function(n) { return n.nodeValue });
}
// initial set
addNodes().forEach(function(n) {
n.nodeValue = Math.random() * maxv + 1;
});
// now add rest
for (var t=0; t<maxt-1; t++) {
addNext();
}
return {
times: times,
links: allLinks
};
})();
/* Process Data */
// make a node lookup map
var nodeMap = (function() {
var nm = {};
data.times.forEach(function(nodes) {
nodes.forEach(function(n) {
nm[n.id] = n;
// add links and assure node value
n.links = [];
n.incoming = [];
n.nodeValue = n.nodeValue || 0;
})
});
return nm;
})();
// attach links to nodes
data.links.forEach(function(link) {
nodeMap[link.source].links.push(link);
nodeMap[link.target].incoming.push(link);
});
// sort by value and calculate offsets
data.times.forEach(function(nodes) {
var cumValue = 0;
nodes.sort(function(a,b) {
return d3.descending(a.nodeValue, b.nodeValue)
});
nodes.forEach(function(n, i) {
n.order = i;
n.offsetValue = cumValue;
cumValue += n.nodeValue;
// same for links
var lCumValue;
// outgoing
if (n.links) {
lCumValue = 0;
n.links.sort(function(a,b) {
return d3.descending(a.value, b.value)
});
n.links.forEach(function(l) {
l.outOffset = lCumValue;
lCumValue += l.value;
});
}
// incoming
if (n.incoming) {
lCumValue = 0;
n.incoming.sort(function(a,b) {
return d3.descending(a.value, b.value)
});
n.incoming.forEach(function(l) {
l.inOffset = lCumValue;
lCumValue += l.value;
});
}
})
});
data = data.times;
// calculate maxes
var maxn = d3.max(data, function(t) { return t.length }),
maxv = d3.max(data, function(t) { return d3.sum(t, function(n) { return n.nodeValue }) });
/* Make Vis */
// settings and scales
var w = 1400,
h = 500,
gapratio = .7,
delay = 1500,
padding = 15,
x = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeBands([0, w + (w/(data.length-1))], gapratio),
y = d3.scale.linear()
.domain([0, maxv])
.range([0, h - padding * maxn]),
line = d3.svg.line()
.interpolate('basis');
// root
var vis = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var t = 0;
function update(first) {
// update data
var currentData = data.slice(0, ++t);
// time slots
var times = vis.selectAll('g.time')
.data(currentData)
.enter().append('svg:g')
.attr('class', 'time')
.attr("transform", function(d, i) { return "translate(" + (x(i) - x(0)) + ",0)" });
// node bars
var nodes = times.selectAll('g.node')
.data(function(d) { return d })
.enter().append('svg:g')
.attr('class', 'node');
setTimeout(function() {
nodes.append('svg:rect')
.attr('fill', 'steelblue')
.attr('y', function(n, i) {
return y(n.offsetValue) + i * padding;
})
.attr('width', x.rangeBand())
.attr('height', function(n) { return y(n.nodeValue) })
.append('svg:title')
.text(function(n) { return n.nodeName });
}, (first ? 0 : delay));
var linkLine = function(start) {
return function(l) {
var source = nodeMap[l.source],
target = nodeMap[l.target],
gapWidth = x(0),
bandWidth = x.rangeBand() + gapWidth,
startx = x.rangeBand() - bandWidth,
sourcey = y(source.offsetValue) +
source.order * padding +
y(l.outOffset) +
y(l.value)/2,
targety = y(target.offsetValue) +
target.order * padding +
y(l.inOffset) +
y(l.value)/2,
points = start ?
[
[ startx, sourcey ], [ startx, sourcey ], [ startx, sourcey ], [ startx, sourcey ]
] :
[
[ startx, sourcey ],
[ startx + gapWidth/2, sourcey ],
[ startx + gapWidth/2, targety ],
[ 0, targety ]
];
return line(points);
}
}
// links
var links = nodes.selectAll('path.link')
.data(function(n) { return n.incoming || [] })
.enter().append('svg:path')
.attr('class', 'link')
.style('stroke-width', function(l) { return y(l.value) })
.attr('d', linkLine(true))
.on('mouseover', function() {
d3.select(this).attr('class', 'link on')
})
.on('mouseout', function() {
d3.select(this).attr('class', 'link')
})
.transition()
.duration(delay)
.attr('d', linkLine());
}
function updateNext() {
if (t < data.length) {
update();
window.setTimeout(updateNext, delay)
}
}
update(true);
updateNext();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment