Skip to content

Instantly share code, notes, and snippets.

@duhaime
Last active August 30, 2018 10:00
Show Gist options
  • Save duhaime/e39a32d0d494010f71a9b8d5b26012f5 to your computer and use it in GitHub Desktop.
Save duhaime/e39a32d0d494010f71a9b8d5b26012f5 to your computer and use it in GitHub Desktop.
Sankey Transitions
/*
* Create synthetic data
*
* Groups:
* 0: yellow
* 1: green
* 2: light blue
* 3: dark blue
**/
var nodes = [
/* col 0 */
{name: 'Angus', col: 0, group: 0},
{name: 'Malcomb', col: 0, group: 0},
{name: 'Dolly', col: 0, group: 0},
{name: 'Johnny', col: 0, group: 0},
{name: 'Robert', col: 0, group: 0},
{name: 'McP', col: 0, group: 0},
/* col 1 */
{name: 'Advent Geneva', col: 1, group: 3},
{name: 'Eagle Pace', col: 1, group: 3},
{name: 'Calypso', col: 1, group: 3},
{name: 'Data Warehose', col: 1, group: 3},
{name: 'Simcorp', col: 1, group: 3},
{name: 'Bi-sam', col: 1, group: 3},
{name: 'Charles River', col: 1, group: 3},
{name: 'Barra', col: 1, group: 3},
{name: 'Cadis', col: 1, group: 3},
{name: 'Factset', col: 1, group: 3},
{name: 'Risk Metrics', col: 1, group: 3},
/* col 2 */
{name: 'APAC', col: 2, group: 1},
{name: 'Europe', col: 2, group: 1},
{name: 'North America', col: 2, group: 1},
{name: 'Front', col: 2, group: 2},
{name: 'Middle', col: 2, group: 2},
{name: 'Back', col: 2, group: 2},
];
// Assign id numbers
nodes.map(function(d, i) {
d.node = i;
});
// Map each col to its nodes
var nodesByCol = nodes.reduce(function(obj, d) {
obj[d.col] ? obj[d.col].push(d) : obj[d.col] = [d];
return obj;
}, {})
// Connect all nodes in each column to the next,
// but conditionally give weight to edge
function getLinks() {
var links = [];
Object.keys(nodesByCol).map(function(i) {
var i = parseInt(i);
if (nodesByCol[i+1]) {
var maxTarget = nodesByCol[i+1].length;
for (var j=0; j<nodesByCol[i].length; j++) {
for (var k=0; k<nodesByCol[i+1].length; k++) {
var link = {
source: nodesByCol[i][j].node,
target: nodesByCol[i+1][k].node,
};
if (Math.random() > 0.85) {
link.value = 1;
link.display = true;
} else {
link.value = 0.0001;
link.display = false;
};
links.push(link);
}
}
}
})
return links;
}
function getData() {
return {
nodes: nodes,
links: getLinks()
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>Sankey Transitions</title>
<link href='style.css' type='text/css' rel='stylesheet'></link>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js'></script>
<script src='https://unpkg.com/d3-sankey@0.4.1'></script>
</head>
<body>
<div class='container'>
<h1>Sankey Transitions</h1>
<div>
<select>
<option value='all'>All</option>
<option value='filter-one'>Filter One</option>
<option value='filter-two'>Filter Two</option>
</select>
</div>
<div id='target'></div>
<script src='data.js'></script>
<script src='sankey.js'></script>
</div>
</body>
</html>
{
"nodes":[
{"node":0,"name":"A"},
{"node":1,"name":"B"},
{"node":2,"name":"C"},
{"node":3,"name":"D"},
{"node":4,"name":"E"}
],
"links":[
{"source":0,"target":2,"value":2},
{"source":1,"target":2,"value":2},
{"source":1,"target":3,"value":4},
{"source":0,"target":4,"value":2},
{"source":2,"target":4,"value":4},
{"source":3,"target":4,"value":4}
]
}
(function() {
var margin = {top: 10, right: 10, bottom: 10, left: 10},
width = 800 - margin.left - margin.right,
height = 380 - margin.top - margin.bottom;
var svg = d3.select('#target').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 + ')');
svg.append('g').attr('class', 'links')
svg.append('g').attr('class', 'nodes')
var sankey = d3.sankey()
.nodeWidth(42)
.nodePadding(20)
.size([width, height]);
var colors = [
'#F7EF99',
'#9CB380',
'#D7F9F1',
'#759EB8'
]
var path = sankey.link(),
duration = 250,
node,
link;
d3.select('select').on('change', handleChange);
function update(data) {
sankey
.nodes(data.nodes)
.links(data.links)
.layout(64);
updateLinks(data.links);
updateNodes(data.nodes);
}
function updateLinks(links) {
link = d3.select('.links').selectAll('.link').data(links, linkKey)
link.exit().remove();
/**
* Enter
**/
var linkEnter = link.enter().append('path')
.attr('class', function(d) {
return d.display ? 'link visible' : 'link';
})
.sort(function(a, b) {
return b.dy - a.dy;
})
linkEnter.append('title');
/**
* Transition
**/
var linkMerge = link.merge(linkEnter);
linkMerge.transition().duration(duration)
.attr('d', path)
.attr('stroke-width', function(d) {
return d.display ? Math.max(10, d.dy) : 0;
})
.attr('stroke', function(d) {
return d.display ? 'rgba(195, 185, 155, 0.3)' : 'rgba(0,0,0,0)';
})
.style('pointer-events', function(d) {
return d.display ? 'initial' : 'none';
})
.select('title').text(function(d) {
return d.source.name + ' to ' + d.target.name;
});
}
function updateNodes(nodes) {
node = d3.select('.nodes').selectAll('.node').data(nodes, nodeKey);
node.exit().remove();
/**
* Enter
**/
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.call(d3.drag()
.subject(function(d) { return d; })
.on('start', function() {
this.parentNode.appendChild(this);
})
.on('drag', handleDrag));
nodeEnter.append('rect')
.attr('width', sankey.nodeWidth())
.style('fill', function(d) {
return d.color = colors[d.group];
})
.append('title');
nodeEnter.append('text')
.attr('x', -6)
.attr('dy', '.4em')
.attr('text-anchor', 'end')
.attr('transform', null);
/**
* Transition
**/
var nodeMerge = node.merge(nodeEnter).transition().duration(duration)
nodeMerge.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
nodeMerge.select('rect')
.attr('height', function(d) {
return Math.max(10, d.dy);
})
.style('fill', function(d) {
return d.color = colors[d.group];
})
.style('stroke', '#888')
nodeMerge.select('title')
.text(function(d) {
return d.name + '\n' + d.value;
})
nodeMerge.select('text')
.attr('y', function(d) { return d.dy / 2; })
.text(function(d) { return d.name; })
.filter(function(d) { return d.x < width / 2; })
.attr('x', 6 + sankey.nodeWidth())
.attr('text-anchor', 'start');
}
function handleDrag(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);
}
function linkKey(d) {
return d.source.name + '-' + d.target.name
}
function nodeKey(d) {
return d.node;
}
function handleChange(d) {
var data = getData();
update(data);
}
var data = getData();
update(data);
})()
@import url('https://fonts.googleapis.com/css?family=Lato:300,400');
* {
box-sizing: border-box;
font-family: Lato;
font-weight: 300;
}
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.container {
padding: 20px;
display: inline-block;
text-align: center;
}
h1 {
margin: 0;
padding: 0;
}
select {
padding: 3px 10px;
height: 34px;
min-width: 100px;
font-size: 1em;
float: left;
}
path.link {
fill: none;
}
path.link.visible {
stroke: rgba(195, 185, 155, 0.3);
}
path.link.visible:hover {
stroke: rgba(195, 185, 155, 0.6);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment