-
-
Save Semant1ka/356273d8691c32911414744493782519 to your computer and use it in GitHub Desktop.
Sankey cycle test
This file contains 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
sankeyDraw( | |
{ | |
"nodes": [ | |
{"name": "Improve CX Flow"}, | |
{"name": "Nevermind Response"}, | |
{"name": "Download Resources"}, | |
{"name": "Our Amazing Customers"}, | |
{"name": "Last Response"}, | |
{"name": "Emoji Response"}, | |
{"name": "Greeting"}, | |
{"name": "Abandoned"}, | |
{"name": "About Ada"}, | |
{"name": "Customers"}, | |
{"name": "Customer's Bot On Site"}, | |
{"name": "Mike Murchison"}, | |
{"name": "David Hariri"}, | |
{"name": "How much can I save"}, | |
{"name": "Ada in the Press"}, | |
{"name": "Positive Review"}, | |
{"name": "Careers"}, | |
{"name": "Mikael"}, | |
{"name": "1000-5000"}, | |
{"name": "Wolfram Alpha API"}, | |
{"name": "About"}, | |
{"name": "Price"}, | |
{"name": "Channels"}, | |
{"name": "> 5000"}, | |
{"name": "Sad"}, | |
{"name": "Acknowledgement"}, | |
{"name": "No, I'm just browsing"}, {"name": "Wolfram API - II"}, {"name": "Competition"}, {"name": "24/7 Office Hours"}, {"name": "Personalize - Website Landing Page"}, {"name": "Personalize - Customize every convo"}, {"name": "Optimize - Calculate your savings"}, {"name": "Languages"}, {"name": "Self-Learning"}, {"name": "Common Questions for Ada"}, {"name": "Druska"}, {"name": "Building Time"}, {"name": "Team"}, {"name": "Are you a human?"}, {"name": "OK reply to \"Yeah\""}, {"name": ">5000 (Old)"}, {"name": "Where is Ada located?"}, {"name": "Time to Implement/Setup"}, {"name": "Customization"}, {"name": "Zendesk Integration"}, {"name": "Calculation"}, {"name": "API"}, {"name": "Technology"}, {"name": "What is AI?"}, {"name": "Build - Website Landing Page"}, {"name": "Chat Integrations"}, {"name": "Negative Review"}, {"name": "Love"}, {"name": "Who does the training?"}, {"name": "Build - Ada\u2019s codeless solution"}, {"name": "coinbase"}], | |
"links": [{"source": 0, "target": 1, "value": 0.108310626702997}, {"source": 1, "target": 2, "value": 0.0497275204359673}, {"source": 1, "target": 3, "value": 0.0408719346049046}, {"source": 2, "target": 0, "value": 0.0381471389645777}, {"source": 4, "target": 0, "value": 0.032016348773842}, {"source": 5, "target": 5, "value": 0.032016348773842}, | |
{"source": 6, "target": 7, "value": 0.0306539509536785}, {"source": 6, "target": 8, "value": 0.029291553133515}, {"source": 2, "target": 9, "value": 0.0279291553133515}, {"source": 3, "target": 4, "value": 0.0258855585831063}, {"source": 4, "target": 4, "value": 0.0258855585831063}, {"source": 9, "target": 10, "value": 0.023841961852861}, | |
{"source": 8, "target": 11, "value": 0.023841961852861}, {"source": 11, "target": 12, "value": 0.0224795640326975}, {"source": 4, "target": 13, "value": 0.0211171662125341}, {"source": 13, "target": 1, "value": 0.0204359673024523}, {"source": 4, "target": 2, "value": 0.0197547683923706}, | |
{"source": 12, "target": 0, "value": 0.0170299727520436}, {"source": 1, "target": 14, "value": 0.0156675749318801}, {"source": 15, "target": 4, "value": 0.0149863760217984}, {"source": 14, "target": 4, "value": 0.0122615803814714}, {"source": 4, "target": 7, "value": 0.0115803814713896}, {"source": 0, "target": 7, "value": 0.0108991825613079}, | |
{"source": 15, "target": 5, "value": 0.0102179836512262}, {"source": 10, "target": 4, "value": 0.0102179836512262}, {"source": 10, "target": 15, "value": 0.00953678474114441}, {"source": 6, "target": 0, "value": 0.00885558583106267}, {"source": 1, "target": 16, "value": 0.00817438692098093}, {"source": 6, "target": 17, "value": 0.00817438692098093}, | |
{"source": 16, "target": 4, "value": 0.00749318801089918}, {"source": 1, "target": 15, "value": 0.00749318801089918}, {"source": 5, "target": 7, "value": 0.00681198910081744}, {"source": 15, "target": 0, "value": 0.0061307901907357}, {"source": 0, "target": 18, "value": 0.00544959128065395}, {"source": 6, "target": 19, "value": 0.00544959128065395}, | |
{"source": 3, "target": 15, "value": 0.00544959128065395}, {"source": 1, "target": 4, "value": 0.00544959128065395}, {"source": 12, "target": 15, "value": 0.00544959128065395}, {"source": 0, "target": 4, "value": 0.00544959128065395}, {"source": 17, "target": 7, "value": 0.00544959128065395}, {"source": 5, "target": 4, "value": 0.00476839237057221}, {"source": 6, "target": 4, "value": 0.00476839237057221}, {"source": 20, "target": 0, "value": 0.00408719346049046}, {"source": 21, "target": 0, "value": 0.00408719346049046}, {"source": 8, "target": 0, "value": 0.00408719346049046}, {"source": 6, "target": 6, "value": 0.00340599455040872}, {"source": 8, "target": 7, "value": 0.00340599455040872}, {"source": 2, "target": 7, "value": 0.00340599455040872}, {"source": 22, "target": 0, "value": 0.00340599455040872}, {"source": 17, "target": 6, "value": 0.00340599455040872}, {"source": 0, "target": 23, "value": 0.00340599455040872}, {"source": 9, "target": 15, "value": 0.00340599455040872}, {"source": 4, "target": 24, "value": 0.00340599455040872}, {"source": 25, "target": 7, "value": 0.00272479564032698}, {"source": 21, "target": 26, "value": 0.00272479564032698}, {"source": 6, "target": 21, "value": 0.00272479564032698}, {"source": 3, "target": 20, "value": 0.00272479564032698}, {"source": 3, "target": 7, "value": 0.00272479564032698}, {"source": 19, "target": 6, "value": 0.00272479564032698}, {"source": 19, "target": 27, "value": 0.00272479564032698}, {"source": 4, "target": 22, "value": 0.00272479564032698}, {"source": 10, "target": 6, "value": 0.00204359673024523}, {"source": 25, "target": 5, "value": 0.00204359673024523}, {"source": 26, "target": 8, "value": 0.00204359673024523}, {"source": 23, "target": 1, "value": 0.00204359673024523}, {"source": 18, "target": 7, "value": 0.00204359673024523}, {"source": 15, "target": 10, "value": 0.00204359673024523}, {"source": 21, "target": 7, "value": 0.00204359673024523}, {"source": 3, "target": 22, "value": 0.00204359673024523}, {"source": 1, "target": 7, "value": 0.00204359673024523}, {"source": 4, "target": 21, "value": 0.00204359673024523}, {"source": 28, "target": 0, "value": 0.00204359673024523}, {"source": 4, "target": 29, "value": 0.00136239782016349}, {"source": 4, "target": 15, "value": 0.00136239782016349}, {"source": 30, "target": 31, "value": 0.00136239782016349}, {"source": 17, "target": 17, "value": 0.00136239782016349}, {"source": 23, "target": 7, "value": 0.00136239782016349}, {"source": 18, "target": 3, "value": 0.00136239782016349}, {"source": 31, "target": 32, "value": 0.00136239782016349}, {"source": 33, "target": 0, "value": 0.00136239782016349}, {"source": 4, "target": 34, "value": 0.00136239782016349}, {"source": 35, "target": 15, "value": 0.00136239782016349}, {"source": 11, "target": 15, "value": 0.00136239782016349}, {"source": 24, "target": 4, "value": 0.00136239782016349}, {"source": 15, "target": 21, "value": 0.00136239782016349}, {"source": 10, "target": 20, "value": 0.00136239782016349}, {"source": 15, "target": 20, "value": 0.00136239782016349}, {"source": 6, "target": 5, "value": 0.00136239782016349}, {"source": 36, "target": 7, "value": 0.00136239782016349}, {"source": 16, "target": 7, "value": 0.00136239782016349}, {"source": 37, "target": 15, "value": 0.00136239782016349}, {"source": 4, "target": 16, "value": 0.00136239782016349}, {"source": 38, "target": 16, "value": 0.00136239782016349}, {"source": 3, "target": 39, "value": 0.00136239782016349}, {"source": 15, "target": 35, "value": 0.00136239782016349}, {"source": 15, "target": 25, "value": 0.00136239782016349}, {"source": 32, "target": 3, "value": 0.00136239782016349}, {"source": 4, "target": 17, "value": 0.00136239782016349}, {"source": 9, "target": 4, "value": 0.00136239782016349}, {"source": 6, "target": 16, "value": 0.00136239782016349}, {"source": 29, "target": 0, "value": 0.00136239782016349}, {"source": 13, "target": 13, "value": 0.00136239782016349}, {"source": 24, "target": 7, "value": 0.00136239782016349}, {"source": 22, "target": 7, "value": 0.00136239782016349}, {"source": 28, "target": 8, "value": 0.00136239782016349}, {"source": 10, "target": 7, "value": 0.00136239782016349}, {"source": 40, "target": 4, "value": 0.00136239782016349}, {"source": 27, "target": 6, "value": 0.00136239782016349}, {"source": 13, "target": 4, "value": 0.00136239782016349}, {"source": 26, "target": 3, "value": 0.000681198910081744}, {"source": 14, "target": 15, "value": 0.000681198910081744}, {"source": 0, "target": 41, "value": 0.000681198910081744}, {"source": 15, "target": 13, "value": 0.000681198910081744}, {"source": 27, "target": 7, "value": 0.000681198910081744}, {"source": 24, "target": 20, "value": 0.000681198910081744}, {"source": 5, "target": 21, "value": 0.000681198910081744}, {"source": 4, "target": 42, "value": 0.000681198910081744}, {"source": 20, "target": 15, "value": 0.000681198910081744}, {"source": 14, "target": 43, "value": 0.000681198910081744}, {"source": 26, "target": 2, "value": 0.000681198910081744}, {"source": 2, "target": 4, "value": 0.000681198910081744}, {"source": 5, "target": 40, "value": 0.000681198910081744}, {"source": 39, "target": 0, "value": 0.000681198910081744}, {"source": 44, "target": 4, "value": 0.000681198910081744}, {"source": 39, "target": 13, "value": 0.000681198910081744}, {"source": 16, "target": 25, "value": 0.000681198910081744}, {"source": 14, "target": 25, "value": 0.000681198910081744}, {"source": 20, "target": 7, "value": 0.000681198910081744}, {"source": 4, "target": 6, "value": 0.000681198910081744}, {"source": 3, "target": 38, "value": 0.000681198910081744}, {"source": 34, "target": 4, "value": 0.000681198910081744}, {"source": 22, "target": 15, "value": 0.000681198910081744}, {"source": 6, "target": 9, "value": 0.000681198910081744}, {"source": 28, "target": 26, "value": 0.000681198910081744}, {"source": 10, "target": 1, "value": 0.000681198910081744}, {"source": 27, "target": 4, "value": 0.000681198910081744}, {"source": 21, "target": 28, "value": 0.000681198910081744}, {"source": 16, "target": 39, "value": 0.000681198910081744}, {"source": 18, "target": 4, "value": 0.000681198910081744}, {"source": 4, "target": 36, "value": 0.000681198910081744}, {"source": 45, "target": 4, "value": 0.000681198910081744}, {"source": 4, "target": 39, "value": 0.000681198910081744}, {"source": 21, "target": 15, "value": 0.000681198910081744}, {"source": 35, "target": 37, "value": 0.000681198910081744}, {"source": 26, "target": 7, "value": 0.000681198910081744}, {"source": 18, "target": 15, "value": 0.000681198910081744}, {"source": 5, "target": 20, "value": 0.000681198910081744}, {"source": 42, "target": 7, "value": 0.000681198910081744}, {"source": 6, "target": 36, "value": 0.000681198910081744}, {"source": 13, "target": 46, "value": 0.000681198910081744}, {"source": 24, "target": 25, "value": 0.000681198910081744}, {"source": 15, "target": 2, "value": 0.000681198910081744}, {"source": 47, "target": 48, "value": 0.000681198910081744}, {"source": 49, "target": 15, "value": 0.000681198910081744}, {"source": 15, "target": 37, "value": 0.000681198910081744}, {"source": 15, "target": 29, "value": 0.000681198910081744}, {"source": 17, "target": 24, "value": 0.000681198910081744}, {"source": 50, "target": 6, "value": 0.000681198910081744}, {"source": 17, "target": 4, "value": 0.000681198910081744}, {"source": 33, "target": 15, "value": 0.000681198910081744}, {"source": 4, "target": 33, "value": 0.000681198910081744}, {"source": 41, "target": 25, "value": 0.000681198910081744}, {"source": 6, "target": 28, "value": 0.000681198910081744}, {"source": 4, "target": 38, "value": 0.000681198910081744}, {"source": 10, "target": 28, "value": 0.000681198910081744}, {"source": 8, "target": 51, "value": 0.000681198910081744}, {"source": 4, "target": 52, "value": 0.000681198910081744}, {"source": 1, "target": 48, "value": 0.000681198910081744}, {"source": 53, "target": 15, "value": 0.000681198910081744}, {"source": 39, "target": 4, "value": 0.000681198910081744}, {"source": 20, "target": 38, "value": 0.000681198910081744}, {"source": 6, "target": 47, "value": 0.000681198910081744}, {"source": 48, "target": 7, "value": 0.000681198910081744}, {"source": 16, "target": 21, "value": 0.000681198910081744}, {"source": 38, "target": 28, "value": 0.000681198910081744}, {"source": 33, "target": 33, "value": 0.000681198910081744}, {"source": 15, "target": 40, "value": 0.000681198910081744}, {"source": 6, "target": 45, "value": 0.000681198910081744}, {"source": 21, "target": 4, "value": 0.000681198910081744}, {"source": 40, "target": 15, "value": 0.000681198910081744}, {"source": 51, "target": 7, "value": 0.000681198910081744}, {"source": 54, "target": 4, "value": 0.000681198910081744}, {"source": 43, "target": 4, "value": 0.000681198910081744}, {"source": 4, "target": 28, "value": 0.000681198910081744}, {"source": 15, "target": 24, "value": 0.000681198910081744}, {"source": 2, "target": 28, "value": 0.000681198910081744}, {"source": 14, "target": 6, "value": 0.000681198910081744}, {"source": 28, "target": 15, "value": 0.000681198910081744}, {"source": 3, "target": 28, "value": 0.000681198910081744}, {"source": 52, "target": 4, "value": 0.000681198910081744}, {"source": 3, "target": 21, "value": 0.000681198910081744}, {"source": 29, "target": 15, "value": 0.000681198910081744}, {"source": 52, "target": 0, "value": 0.000681198910081744}, {"source": 3, "target": 35, "value": 0.000681198910081744}, {"source": 50, "target": 55, "value": 0.000681198910081744}, {"source": 28, "target": 7, "value": 0.000681198910081744}, {"source": 18, "target": 21, "value": 0.000681198910081744}, {"source": 15, "target": 7, "value": 0.000681198910081744}, {"source": 3, "target": 40, "value": 0.000681198910081744}, {"source": 16, "target": 6, "value": 0.000681198910081744}, {"source": 15, "target": 26, "value": 0.000681198910081744}, {"source": 14, "target": 20, "value": 0.000681198910081744}, {"source": 55, "target": 4, "value": 0.000681198910081744}, {"source": 0, "target": 21, "value": 0.000681198910081744}, {"source": 15, "target": 52, "value": 0.000681198910081744}, {"source": 16, "target": 1, "value": 0.000681198910081744}, {"source": 8, "target": 4, "value": 0.000681198910081744}, {"source": 6, "target": 22, "value": 0.000681198910081744}, {"source": 20, "target": 49, "value": 0.000681198910081744}, {"source": 1, "target": 33, "value": 0.000681198910081744}, {"source": 46, "target": 21, "value": 0.000681198910081744}, {"source": 48, "target": 33, "value": 0.000681198910081744}, {"source": 0, "target": 44, "value": 0.000681198910081744}, {"source": 3, "target": 6, "value": 0.000681198910081744}, {"source": 4, "target": 54, "value": 0.000681198910081744}, {"source": 24, "target": 17, "value": 0.000681198910081744}, {"source": 1, "target": 21, "value": 0.000681198910081744}, {"source": 4, "target": 56, "value": 0.000681198910081744}, {"source": 39, "target": 15, "value": 0.000681198910081744}, {"source": 56, "target": 6, "value": 0.000681198910081744}, {"source": 1, "target": 28, "value": 0.000681198910081744}, {"source": 4, "target": 53, "value": 0.000681198910081744}, {"source": 21, "target": 21, "value": 0.000681198910081744}, {"source": 20, "target": 4, "value": 0.000681198910081744}, {"source": 15, "target": 3, "value": 0.000681198910081744}, {"source": 3, "target": 25, "value": 0.000681198910081744}, {"source": 34, "target": 15, "value": 0.000681198910081744}], | |
'sinksRight': false | |
} | |
); |
This file contains 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"> | |
<style> | |
body { | |
font-family: sans-serif; | |
} | |
.node rect { | |
fill-opacity: .9; | |
shape-rendering: crispEdges; | |
} | |
.link { | |
fill: none; | |
stroke: #024; | |
stroke-opacity: .2; | |
} | |
.link:hover { | |
stroke-opacity: .5; | |
} | |
.link.backwards { | |
stroke: #402; | |
stroke-dasharray: 9,1; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="data-set-select"></div> | |
<div id="chart"></div> | |
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<script src="sankey.js"></script> | |
<script> | |
// Some setup stuff. | |
var margin = {top: 20, right: 20, bottom: 20, left: 20}; | |
var width = 960 - margin.left - margin.right; | |
var height = 500 - margin.top - margin.bottom; | |
var color = d3.scale.category20(); | |
// SVG (group) to draw in. | |
var svg = d3.select("#chart").append("svg") | |
.attr({ | |
width: width + margin.left + margin.right, | |
height: height + margin.top + margin.bottom | |
}) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
var linksGroup = svg.append("g").attr('class', 'links'); | |
var nodesGroup = svg.append("g").attr('class', 'nodes'); | |
// Set up Sankey object. | |
var sankey = d3.sankey() | |
.nodeWidth(30) | |
.nodePadding(5) | |
.size([width, height]); | |
// Get path data generator | |
var path = sankey.link(); | |
// Callback to draw on a data set. | |
function sankeyDraw(data) { | |
sankey.nodes(data.nodes) | |
.links(data.links) | |
.sinksRight('sinksRight' in data ? data.sinksRight : true) | |
.layout(32); | |
// Draw the links. | |
var links = linksGroup.selectAll('.link').data(data.links); | |
// Enter | |
links.enter() | |
.append("path") | |
.attr('class', 'link'); | |
// Enter + Update | |
links.attr('d', path) | |
.style("stroke-width", function (d) { | |
return Math.max(1, d.dy); | |
}); | |
links.classed('backwards', function (d) { return d.target.x < d.source.x; }); | |
links.append("title") | |
.text(function (d) { | |
return d.source.name + " to " + d.target.name + " = " + d.value; | |
}); | |
// Exit | |
links.exit().remove(); | |
// Draw the nodes. | |
var nodes = nodesGroup.selectAll('.node').data(data.nodes); | |
// Enter | |
var nodesEnterSelection = nodes.enter() | |
.append("g") | |
.attr('class', 'node'); | |
nodesEnterSelection.append("rect") | |
.attr('width', sankey.nodeWidth()) | |
.append("title"); | |
nodesEnterSelection.append("text") | |
.attr('x', sankey.nodeWidth() / 2) | |
.attr('dy', ".35em") | |
.attr("text-anchor", "middle") | |
.attr('transform', null); | |
// Enter + Update | |
nodes | |
.attr('transform', function (d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}); | |
nodes.select('rect') | |
.attr('height', function (d) { | |
return d.dy; | |
}) | |
.style('fill', function (d) { | |
return d.color = color(d.name.replace(/ .*/, "")); | |
}) | |
.style('stroke', function (d) { | |
return d3.rgb(d.color).darker(2); | |
}); | |
nodes.select('rect').select('title') | |
.text(function (d) { | |
return d.name; | |
}); | |
nodes.select('text') | |
.attr('y', function (d) { | |
return d.dy / 2; | |
}) | |
.text(function (d) { | |
return d.name; | |
}); | |
// Exit | |
nodes.exit().remove(); | |
} | |
</script> | |
<script src="data07-big-with-cycle.js"></script> | |
</body> | |
</html> |
This file contains 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.sankey = function() { | |
var sankey = {}, | |
nodeWidth = 24, | |
nodePadding = 8, | |
size = [1, 1], | |
nodes = [], | |
links = [], | |
extent = null, | |
sinksRight = true; | |
sankey.nodeWidth = function(_) { | |
if (!arguments.length) return nodeWidth; | |
nodeWidth = +_; | |
return sankey; | |
}; | |
sankey.nodePadding = function(_) { | |
if (!arguments.length) return nodePadding; | |
nodePadding = +_; | |
return sankey; | |
}; | |
sankey.nodes = function(_) { | |
if (!arguments.length) return nodes; | |
nodes = _; | |
return sankey; | |
}; | |
sankey.links = function(_) { | |
if (!arguments.length) return links; | |
links = _; | |
return sankey; | |
}; | |
sankey.size = function(_) { | |
if (!arguments.length) return size; | |
size = _; | |
return sankey; | |
}; | |
sankey.extent = function(_) { | |
if (!arguments.length) return extent; | |
extent = _; | |
return sankey; | |
}; | |
sankey.sinksRight = function (_) { | |
if (!arguments.length) return sinksRight; | |
sinksRight = _; | |
return sankey; | |
}; | |
sankey.layout = function(iterations) { | |
computeNodeLinks(); | |
computeNodeValues(); | |
computeNodeBreadths(); | |
computeNodeDepths(iterations); | |
return sankey; | |
}; | |
sankey.relayout = function() { | |
computeLinkDepths(); | |
return sankey; | |
}; | |
// SVG path data generator, to be used as "d" attribute on "path" element selection. | |
sankey.link = function() { | |
var curvature = .5; | |
function link(d) { | |
var xs = d.source.x + d.source.dx, | |
xt = d.target.x, | |
xi = d3.interpolateNumber(xs, xt), | |
xsc = xi(curvature), | |
xtc = xi(1 - curvature), | |
ys = d.source.y + d.sy + d.dy / 2, | |
yt = d.target.y + d.ty + d.dy / 2; | |
if (!d.cycleBreaker) { | |
return "M" + xs + "," + ys | |
+ "C" + xsc + "," + ys | |
+ " " + xtc + "," + yt | |
+ " " + xt + "," + yt; | |
} else { | |
var xdelta = (1.5 * d.dy + 0.05 * Math.abs(xs - xt)); | |
xsc = xs + xdelta; | |
xtc = xt - xdelta; | |
var xm = xi(0.5); | |
var ym = d3.interpolateNumber(ys, yt)(0.5); | |
var ydelta = (2 * d.dy + 0.1 * Math.abs(xs - xt) + 0.1 * Math.abs(ys - yt)) * (ym < (size[1] / 2) ? -1 : 1); | |
return "M" + xs + "," + ys | |
+ "C" + xsc + "," + ys | |
+ " " + xsc + "," + (ys + ydelta) | |
+ " " + xm + "," + (ym + ydelta) | |
+ "S" + xtc + "," + yt | |
+ " " + xt + "," + yt; | |
} | |
} | |
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) { | |
// Links that have this node as source. | |
node.sourceLinks = []; | |
// Links that have this node as target. | |
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, 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; | |
// Work from left to right. | |
// Keep updating the breath (x-position) of nodes that are target of recently updated nodes. | |
while (remainingNodes.length && x < nodes.length) { | |
nextNodes = []; | |
remainingNodes.forEach(function(node) { | |
node.x = x; | |
node.dx = nodeWidth; | |
node.sourceLinks.forEach(function(link) { | |
if (nextNodes.indexOf(link.target) < 0 && !link.cycleBreaker) { | |
nextNodes.push(link.target); | |
} | |
}); | |
}); | |
if (nextNodes.length == remainingNodes.length) { | |
// There must be a cycle here. Let's search for a link that breaks it. | |
findAndMarkCycleBreaker(nextNodes); | |
// Start over. | |
// TODO: make this optional? | |
return computeNodeBreadths(); | |
} | |
else { | |
remainingNodes = nextNodes; | |
++x; | |
} | |
} | |
// Optionally move pure sinks always to the right. | |
if (sinksRight) { | |
moveSinksRight(x); | |
} | |
scaleNodeBreadths((size[0] - nodeWidth) / (x - 1)); | |
} | |
// Find a link that breaks a cycle in the graph (if any). | |
function findAndMarkCycleBreaker(nodes) { | |
// Go through all nodes from the given subset and traverse links searching for cycles. | |
var link; | |
for (var n=nodes.length - 1; n >= 0; n--) { | |
link = depthFirstCycleSearch(nodes[n], []); | |
if (link) { | |
return link; | |
} | |
} | |
// Depth-first search to find a link that is part of a cycle. | |
function depthFirstCycleSearch(cursorNode, path) { | |
var target, link; | |
for (var n = cursorNode.sourceLinks.length - 1; n >= 0; n--) { | |
link = cursorNode.sourceLinks[n]; | |
if (link.cycleBreaker) { | |
// Skip already known cycle breakers. | |
continue; | |
} | |
// Check if target of link makes a cycle in current path. | |
target = link.target; | |
for (var l = 0; l < path.length; l++) { | |
if (path[l].source == target) { | |
// We found a cycle. Search for weakest link in cycle | |
var weakest = link; | |
for (; l < path.length; l++) { | |
if (path[l].value < weakest.value) { | |
weakest = path[l]; | |
} | |
} | |
// Mark weakest link as (known) cycle breaker and abort search. | |
weakest.cycleBreaker = true; | |
return weakest; | |
} | |
} | |
// Recurse deeper. | |
path.push(link); | |
link = depthFirstCycleSearch(target, path); | |
path.pop(); | |
// Stop further search if we found a cycle breaker. | |
if (link) { | |
return link; | |
} | |
} | |
} | |
} | |
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; | |
}); | |
} | |
// Compute the depth (y-position) for each node. | |
function computeNodeDepths(iterations) { | |
// Group nodes by breath. | |
var nodesByBreadth = d3.nest() | |
.key(function(d) { return d.x; }) | |
.sortKeys(d3.ascending) | |
.entries(nodes) | |
.map(function(d) { return d.values; }); | |
// | |
initializeNodeDepth(); | |
resolveCollisions(); | |
computeLinkDepths(); | |
for (var alpha = 1; iterations > 0; --iterations) { | |
relaxRightToLeft(alpha *= .99); | |
resolveCollisions(); | |
computeLinkDepths(); | |
relaxLeftToRight(alpha); | |
resolveCollisions(); | |
computeLinkDepths(); | |
} | |
function initializeNodeDepth() { | |
// Calculate vertical scaling factor. | |
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) { | |
link.dy = link.value * ky; | |
}); | |
} | |
function relaxLeftToRight(alpha) { | |
nodesByBreadth.forEach(function(nodes, breadth) { | |
nodes.forEach(function(node) { | |
if (node.targetLinks.length) { | |
// Value-weighted average of the y-position of source node centers linked to this node. | |
var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value); | |
node.y += (y - center(node)) * alpha; | |
} | |
}); | |
}); | |
function weightedSource(link) { | |
return (link.source.y + link.sy + link.dy / 2) * link.value; | |
} | |
} | |
function relaxRightToLeft(alpha) { | |
nodesByBreadth.slice().reverse().forEach(function(nodes) { | |
nodes.forEach(function(node) { | |
if (node.sourceLinks.length) { | |
// Value-weighted average of the y-positions of target nodes linked to this node. | |
var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value); | |
node.y += (y - center(node)) * alpha; | |
} | |
}); | |
}); | |
function weightedTarget(link) { | |
return (link.target.y + link.ty + link.dy / 2) * 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; | |
} | |
} | |
// Compute y-offset of the source endpoint (sy) and target endpoints (ty) of links, | |
// relative to the source/target node's y-position. | |
function computeLinkDepths() { | |
nodes.forEach(function(node) { | |
node.sourceLinks.sort(ascendingTargetDepth); | |
node.targetLinks.sort(ascendingSourceDepth); | |
}); | |
nodes.forEach(function(node) { | |
var sy = 0, ty = 0; | |
node.sourceLinks.forEach(function(link) { | |
link.sy = sy; | |
sy += link.dy; | |
}); | |
node.targetLinks.forEach(function(link) { | |
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; | |
} | |
} | |
// Y-position of the middle of a node. | |
function center(node) { | |
return node.y + node.dy / 2; | |
} | |
// Value property accessor. | |
function value(x) { | |
return x.value; | |
} | |
return sankey; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment