Last active
October 14, 2016 21:24
-
-
Save Chirishman/66d25864cb7e0f92d28b to your computer and use it in GitHub Desktop.
Sankey D3 Visualization - attempting to colorize links
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> | |
<meta charset="utf-8"> | |
<html> | |
<title>SANKEY Experiment</title> | |
<style> | |
.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; | |
stroke-opacity: .2; | |
} | |
.link:hover { | |
stroke-opacity: .5; | |
} | |
</style> | |
<body> | |
<p id="chart"> | |
<script src="https://d3js.org/d3.v3.js"></script> | |
<script src="sankey.js"></script> | |
<script> | |
var units = "Tickets"; | |
var margin = { top: 10, right: 10, bottom: 10, left: 10 }, | |
width = 1200 - margin.left - margin.right, | |
height = 1800 - margin.top - margin.bottom; | |
var formatNumber = d3.format(",.0f"), // zero decimal places | |
format = function (d) { return formatNumber(d) + " " + units; }, | |
color = d3.scale.category20(); | |
// append the svg canvas to the page | |
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 + ")"); | |
// Set the sankey diagram properties | |
var sankey = d3.sankey() | |
.nodeWidth(36) | |
.nodePadding(20) | |
.size([width, height]); | |
var path = sankey.link(); | |
// load the data with d3.csv instead of d3.json | |
//for another much simpler example uncomment the below | |
d3.csv("ticket.csv", function (error, data) { | |
//d3.csv("d3noob_energy.csv", function(error, data) { | |
//set up graph in same style as original example but empty | |
graph = { "nodes": [], "links": [] }; | |
data.forEach(function (d) { | |
graph.nodes.push({ "name": d.source }); | |
graph.nodes.push({ "name": d.target }); | |
graph.links.push({ "source": d.source, "target": d.target, "value": +d.value }); | |
}); | |
//thanks Mike Bostock https://groups.google.com/d/msg/d3-js/pl297cFtIQk/Eso4q_eBu1IJ | |
//this handy little function returns only the distinct / unique nodes | |
graph.nodes = d3.keys(d3.nest() | |
.key(function (d) { return d.name; }) | |
.map(graph.nodes)); | |
//it appears d3 with force layout wants a numeric source and target | |
//so loop through each link replacing the text with its index from node | |
graph.links.forEach(function (d, i) { | |
graph.links[i].source = graph.nodes.indexOf(graph.links[i].source); | |
graph.links[i].target = graph.nodes.indexOf(graph.links[i].target); | |
}); | |
//now loop through each nodes to make nodes an array of objects rather than an array of strings | |
graph.nodes.forEach(function (d, i) { | |
graph.nodes[i] = { "name": d }; | |
}); | |
sankey | |
.nodes(graph.nodes) | |
.links(graph.links) | |
.layout(32); | |
// add in the links | |
var link = svg.append("g").selectAll(".link") | |
.data(graph.links) | |
.enter().append("path") | |
.attr("class", "link") | |
.attr("d", path) | |
.style("stroke-width", function (d) { return Math.max(1, d.dy); }) | |
.style("stroke", function (d) { | |
d.color = color(d.source.name.replace(/ .*/, "")); | |
return d3.rgb(d.color).darker(2); | |
}) | |
.sort(function (a, b) { return b.dy - a.dy; }); | |
// add the link titles | |
link.append("title") | |
.text(function (d) { | |
return d.source.name + " → " + | |
d.target.name + "\n" + format(d.value); | |
}); | |
// add in the nodes | |
var node = svg.append("g").selectAll(".node") | |
.data(graph.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)); | |
// add the rectangles for the nodes | |
node.append("rect") | |
.attr("height", function (d) { return d.dy; }) | |
.attr("width", sankey.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); | |
}); | |
// add in the title for the nodes | |
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 + sankey.nodeWidth()) | |
.attr("text-anchor", "start"); | |
// the function for moving the nodes | |
function dragmove(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); | |
} | |
}); | |
</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 = []; | |
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.layout = function(iterations) { | |
computeNodeLinks(); | |
computeNodeValues(); | |
computeNodeBreadths(); | |
computeNodeDepths(iterations); | |
computeLinkDepths(); | |
return sankey; | |
}; | |
sankey.relayout = function() { | |
computeLinkDepths(); | |
return sankey; | |
}; | |
sankey.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 + d.dy / 2, | |
y1 = d.target.y + d.ty + d.dy / 2; | |
return "M" + x0 + "," + y0 | |
+ "C" + x2 + "," + y0 | |
+ " " + x3 + "," + y1 | |
+ " " + x1 + "," + y1; | |
} | |
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, 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) { | |
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) { | |
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; | |
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; | |
} | |
} | |
function center(node) { | |
return node.y + node.dy / 2; | |
} | |
function value(link) { | |
return link.value; | |
} | |
return sankey; | |
}; |
We can make this file beautiful and searchable if this error is corrected: No commas found in this CSV file in line 0.
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
source target value | |
Care Coordination Assigned 176 | |
Care Coordination Cancelled 15 | |
Care Coordination Completed 1717 | |
Care Coordination InProgress 4 | |
Care Coordination Rejected 1 | |
Data Integration Assigned 3 | |
Data Integration Cancelled 1 | |
Data Integration Closed 11 | |
Data Integration Completed 15 | |
Data Integration InProgress 2 | |
DCM - Operations Assigned 20 | |
DCM - Operations Cancelled 9 | |
DCM - Operations Closed 93 | |
DCM - Operations Completed 58 | |
DCM - Operations InProgress 10 | |
DCM - Operations Pending 1 | |
EA - Operations Assigned 49 | |
EA - Operations Cancelled 24 | |
EA - Operations Closed 78 | |
EA - Operations Completed 73 | |
EA - Operations InProgress 5 | |
EA - Operations Waiting Approval 17 | |
End User - Operations Assigned 97 | |
End User - Operations Cancelled 80 | |
End User - Operations Closed 825 | |
End User - Operations Completed 146 | |
End User - Operations InProgress 35 | |
End User - Operations Pending 11 | |
End User - Operations Planning 1 | |
End User - Operations Rejected 7 | |
End User - Operations Waiting Approval 9 | |
Enterprise Data Management Assigned 1 | |
Facilities Assigned 2 | |
Facilities Cancelled 11 | |
Facilities Closed 188 | |
Facilities Completed 9 | |
Facilities InProgress 28 | |
Facilities Pending 3 | |
Facilities Rejected 5 | |
Human Resources Assigned 5 | |
Human Resources Cancelled 28 | |
Human Resources Closed 83 | |
Human Resources Completed 19 | |
iBSM Assigned 3 | |
iBSM Cancelled 3 | |
iBSM Closed 9 | |
iBSM Completed 3 | |
iBSM InProgress 1 | |
Imp and Comm Assigned 5 | |
Imp and Comm Cancelled 4 | |
Imp and Comm Closed 5 | |
Imp and Comm Completed 29 | |
Imp and Comm InProgress 4 | |
Inf - Technical Management Assigned 12 | |
Inf - Technical Management Closed 2 | |
Inf - Technical Management Completed 2 | |
IT Procurement Closed 1 | |
LOE Assigned 4 | |
LOE Cancelled 8 | |
LOE Closed 27 | |
LOE Completed 16 | |
LOE Rejected 1 | |
Mailroom Assigned 5 | |
Mailroom Cancelled 8 | |
Mailroom Closed 357 | |
Mailroom Completed 11 | |
Mailroom InProgress 6 | |
Mailroom Pending 6 | |
Mailroom Planning 4 | |
Mailroom Rejected 1 | |
Mednet Closed 11 | |
Mednet Completed 3 | |
Mednet InProgress 1 | |
Quality Spectrum Assigned 2 | |
Quality Spectrum Cancelled 1 | |
Quality Spectrum InProgress 1 | |
Risk Adjustment 1 | |
Risk Adjustment Assigned 5 | |
Risk Adjustment Cancelled 2 | |
Risk Adjustment Closed 4 | |
Risk Adjustment Completed 43 | |
Risk Adjustment InProgress 3 | |
Security Assigned 189 | |
Security Cancelled 39 | |
Security Closed 76 | |
Security Completed 35 | |
Security InProgress 30 | |
Security Pending 4 | |
Security Rejected 1 | |
Security Waiting Approval 65 | |
Telcom - Operations Assigned 1 | |
Telcom - Operations Closed 11 | |
Telcom - Operations Completed 16 | |
Telcom - Operations Planning 1 | |
Travel Closed 2 | |
Travel Pending 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment