|
|
|
<!DOCTYPE html> |
|
|
|
<html lang='en'> |
|
<head> |
|
<meta charset='utf-8' /> |
|
<title>Sankey Particles</title> |
|
</head> |
|
<body> |
|
<canvas width='1000' height='1000' ></canvas> |
|
<svg width='1000' height='1000' ></svg> |
|
|
|
<script src='https://d3js.org/d3.v3.min.js' charset='utf-8' type='text/javascript'></script> |
|
<script src='d3.sankey.js' charset='utf-8' type='text/javascript'></script> |
|
|
|
<script type='text/javascript'> |
|
|
|
var canvas = d3.select('canvas') |
|
.style('position', 'absolute'); |
|
|
|
var margin = {top: 1, right: 1, bottom: 6, left: 1}, |
|
width = 960 - margin.left - margin.right, |
|
height = 500 - margin.top - margin.bottom, |
|
nodeWidth = 15; |
|
|
|
var formatNumber = d3.format(',.0f'), |
|
format = function(d) { return formatNumber(d) + ' TWh'; }, |
|
color = d3.scale.category20(); |
|
|
|
var svg = d3.select('svg') |
|
.style('position', 'absolute') |
|
.attr('width', width + margin.left + margin.right) |
|
.attr('height', height + margin.top + margin.bottom) |
|
.append('g') |
|
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); |
|
|
|
var sankey = d3.sankey() |
|
.nodeWidth(nodeWidth) |
|
.nodePadding(10) |
|
.size([width, height]); |
|
|
|
var path = sankey.link(); |
|
|
|
var freqCounter = 1; |
|
|
|
|
|
d3.json('energy.json', function(data) { |
|
|
|
var graph = calculateNodeDepth(data); |
|
var maxDepth = d3.max(graph.nodes.map(function(d) { return d.depth; })); |
|
console.log('maxDepth', maxDepth); |
|
|
|
sankey |
|
.nodes(graph.nodes) |
|
.links(graph.links) |
|
.layout(32); |
|
|
|
console.log('graph.nodes', graph.nodes); |
|
|
|
var link = svg.append('g').selectAll('.link') |
|
.data(graph.links) |
|
.enter().append('path') |
|
.attr('class', 'link') |
|
.attr('id', function(d) { |
|
return 'pathTo' + d.target.id; |
|
}) |
|
.attr('d', path) |
|
.style('stroke-width', function(d) { return Math.max(1, d.dy); }) |
|
.style({ |
|
'fill': 'none', |
|
'stroke': '#000', |
|
'stroke-opacity': .15 |
|
}) |
|
.sort(function(a, b) { return b.dy - a.dy; }); |
|
|
|
link |
|
.on('mouseover', function() { |
|
d3.select(this).style('stroke-opacity', .25); |
|
}) |
|
.on('mouseout', function() { |
|
d3.select(this).style('stroke-opacity', .15); |
|
}); |
|
|
|
link.append('title') |
|
.text(function(d) { return d.target.name + ' → ' + d.target.name + '\n' + format(d.value); }); |
|
|
|
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)); |
|
|
|
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': 'none', |
|
'cursor': 'move', |
|
'fill-opacity': .9, |
|
'shape-rendering': 'crispEdges' |
|
}) |
|
.append('title') |
|
.text(function(d) { return d.name + '\n' + format(d.value); }); |
|
|
|
// draw nice curved text for links |
|
var pathTotalLength; |
|
var nameOffset; |
|
var nodeNamesDrawn = {}; |
|
var maxWidth = (width / maxDepth) - nodeWidth*1.2; |
|
console.log('maxWidth', maxWidth); |
|
|
|
var linkText = svg.append('text') |
|
.attr('x', 0) |
|
.attr('dy', '.35em') |
|
.style('pointer-events', 'none'); |
|
|
|
svg.selectAll('.link') |
|
.each(function(d) { |
|
switch (d.depth) { |
|
case 7: |
|
pathTotalLength = d3.select('#pathTo' + d.target.id)[0][0].getTotalLength(); |
|
// console.log('pathTotalLength', pathTotalLength); |
|
|
|
textSize = getTextSize(d.target.name); |
|
nameOffsetStart = pathTotalLength - textSize.width - 3; |
|
nameOffsetEnd = textSize.width; |
|
// console.log('nameOffsetStart', nameOffsetStart); |
|
|
|
// this works for leaf nodes |
|
// if we haven't already drawn a node name, draw it |
|
if (!nodeNamesDrawn.hasOwnProperty(d.target.id)) { |
|
console.log('linkText', linkText); |
|
|
|
linkText.append('textPath') |
|
.attr('xlink:href', '#pathTo' + d.target.id) |
|
.attr('id', '#nameTextPathTo' + d.target.id) |
|
.attr('startOffset', nameOffsetStart) |
|
.attr('text-anchor', 'start') |
|
.text(d.target.name) |
|
/*.filter(function() { |
|
var textPathId = d3.select(this)[0][0].id; |
|
var targetNodeId = Number(textPathId.replace(/#nameTextPathTo/, '')); |
|
var targetNodeDepth = graph.nodes[targetNodeId].depth; |
|
// console.log('textPathId', textPathId); |
|
console.log(graph.nodes[targetNodeId].name); |
|
console.log('targetNodeId', targetNodeId); |
|
console.log('targetNodeDepth', targetNodeDepth); |
|
return targetNodeDepth < 7; |
|
}) |
|
.attr('startOffset', nameOffsetEnd) |
|
.attr('text-anchor', 'end'); |
|
*/ |
|
nodeNamesDrawn[d.target.id] = d.target.name; |
|
} |
|
break; |
|
default: |
|
pathTotalLength = d3.select('#pathTo' + d.target.id)[0][0].getTotalLength(); |
|
// console.log('pathTotalLength', pathTotalLength); |
|
|
|
textSize = getTextSize(d.target.name); |
|
nameOffsetStart = pathTotalLength - textSize.width - 3; |
|
nameOffsetEnd = textSize.width; |
|
// console.log('nameOffsetStart', nameOffsetStart); |
|
|
|
// this works for leaf nodes |
|
// if we haven't already drawn a node name, draw it |
|
if (!nodeNamesDrawn.hasOwnProperty(d.target.id)) { |
|
console.log('linkText', linkText); |
|
|
|
linkText.append('textPath') |
|
.attr('xlink:href', '#pathTo' + d.target.id) |
|
.attr('id', '#nameTextPathTo' + d.target.id) |
|
.attr('startOffset', nameOffsetStart) |
|
.attr('text-anchor', 'start') |
|
.text(d.target.name) |
|
|
|
nodeNamesDrawn[d.target.id] = d.target.name; |
|
} |
|
break; |
|
} |
|
|
|
|
|
// draw link labels |
|
linkText.append('textPath') |
|
.attr('xlink:href', '#pathTo' + d.target.id) |
|
.attr('startOffset', 3) |
|
.text(d.label); |
|
}) |
|
|
|
// draw linear text for 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) |
|
.style({ |
|
'pointer-events': 'none', |
|
'text-shadow': '0 1px 0 #fff' |
|
}) |
|
.text(function(d) { return d.name; }) |
|
.filter(function(d) { return d.x < width / 2; }) |
|
.attr('x', 6 + sankey.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))) + ')'); |
|
sankey.relayout(); |
|
link.attr('d', path); |
|
} |
|
|
|
var linkExtent = d3.extent(graph.links, function (d) {return d.value}); |
|
var frequencyScale = d3.scale.linear().domain(linkExtent).range([0.05,1]); |
|
var particleSize = d3.scale.linear().domain(linkExtent).range([1,5]); |
|
|
|
|
|
graph.links.forEach(function (link) { |
|
link.freq = frequencyScale(link.value); |
|
link.particleSize = 2; |
|
link.particleColor = d3.scale.linear().domain([0,1]) |
|
.range([link.target.color, link.target.color]); |
|
}) |
|
|
|
var t = d3.timer(tick, 1000); |
|
var particles = []; |
|
|
|
function tick(elapsed, time) { |
|
|
|
particles = particles.filter(function (d) {return d.current < d.path.getTotalLength()}); |
|
|
|
d3.selectAll('path.link') |
|
.each( |
|
function (d) { |
|
// if (d.freq < 1) { |
|
for (var x = 0;x<2;x++) { |
|
var offset = (Math.random() - .5) * (d.dy - 4); |
|
if (Math.random() < d.freq) { |
|
var length = this.getTotalLength(); |
|
particles.push({link: d, time: elapsed, offset: offset, path: this, length: length, animateTime: length, speed: 0.5 + (Math.random())}) |
|
} |
|
} |
|
|
|
// } |
|
/* else { |
|
for (var x = 0; x<d.freq; x++) { |
|
var offset = (Math.random() - .5) * d.dy; |
|
particles.push({link: d, time: elapsed, offset: offset, path: this}) |
|
} |
|
} */ |
|
}); |
|
|
|
particleEdgeCanvasPath(elapsed); |
|
} |
|
|
|
function particleEdgeCanvasPath(elapsed) { |
|
var context = d3.select('canvas').node().getContext('2d') |
|
|
|
context.clearRect(0,0,1000,1000); |
|
|
|
context.fillStyle = 'gray'; |
|
context.lineWidth = '1px'; |
|
for (var x in particles) { |
|
var currentTime = elapsed - particles[x].time; |
|
// var currentPercent = currentTime / 1000 * particles[x].path.getTotalLength(); |
|
particles[x].current = currentTime * 0.15 * particles[x].speed; |
|
var currentPos = particles[x].path.getPointAtLength(particles[x].current); |
|
context.beginPath(); |
|
context.fillStyle = particles[x].link.particleColor(0); |
|
context.arc(currentPos.x,currentPos.y + particles[x].offset,particles[x].link.particleSize,0,2*Math.PI); |
|
context.fill(); |
|
} |
|
} |
|
|
|
function calculateNodeDepth(graph) { |
|
var inputGraph = clone(graph); |
|
|
|
// add an id to each node |
|
// if it does not already have one |
|
inputGraph.nodes.forEach(function(d, i) { |
|
if(typeof d.id === 'undefined') d.id = i; |
|
}) |
|
|
|
var treeData = clone(inputGraph); |
|
var treeLinks = treeData.links; |
|
var nodesByName = {}; |
|
|
|
treeLinks.forEach(function(link) { |
|
var parent = link.target = getNodesByName(link.target); |
|
var child = link.target = getNodesByName(link.target); |
|
|
|
if (parent.children) parent.children.push(child); |
|
else parent.children = [child]; |
|
}) |
|
|
|
// Extract the root node |
|
var root = treeLinks[0].target; |
|
|
|
var tree = d3.layout.tree(); |
|
var nodes2 = tree.nodes(root); |
|
// console.log('nodes2', nodes2); |
|
|
|
// take the calculated depth and append it to |
|
// each node in our original nodelist |
|
inputGraph.nodes.forEach(function(d) { |
|
if (typeof d.depth === 'undefined') { |
|
d.depth = nodes2[d.id].depth; |
|
} |
|
}) |
|
|
|
return inputGraph; |
|
|
|
function getNodesByName(name) { |
|
return nodesByName[name] || (nodesByName[name] = {name: name}); |
|
} |
|
} |
|
|
|
function clone(obj) { |
|
var copy; |
|
|
|
// Handle the 3 simple types, and null or undefined |
|
if (null == obj || 'object' != typeof obj) return obj; |
|
|
|
// Handle Date |
|
if (obj instanceof Date) { |
|
copy = new Date(); |
|
copy.setTime(obj.getTime()); |
|
return copy; |
|
} |
|
|
|
// Handle Array |
|
if (obj instanceof Array) { |
|
copy = []; |
|
for (var i = 0, len = obj.length; i < len; i++) { |
|
copy[i] = clone(obj[i]); |
|
} |
|
return copy; |
|
} |
|
|
|
// Handle Object |
|
if (obj instanceof Object) { |
|
copy = {}; |
|
for (var attr in obj) { |
|
if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); |
|
} |
|
return copy; |
|
} |
|
throw new Error("Unable to copy obj! Its type isn't supported."); |
|
} |
|
|
|
function getElement() { |
|
var element = d3.select('text-size svg g'); |
|
if (element.empty()) { |
|
element = d3.select('body') |
|
.append('div') |
|
.style({ |
|
width: 0, |
|
height: 0, |
|
position: 'absolute', |
|
left: '-20000px', |
|
top: '-20000px' |
|
}) |
|
.attr('id', 'text-size') |
|
.append('svg') |
|
.append('g'); |
|
} |
|
return element; |
|
} |
|
|
|
function getTextSize(text, style) { |
|
if (typeof text === 'undefined') text = ["this is a test"]; |
|
if (Object.prototype.toString.call(text) !== '[object Array]') text = [text]; |
|
|
|
var maxHeight = 0; |
|
var maxWidth = 0; |
|
|
|
getElement().selectAll('text.measure').data(text) |
|
.enter() |
|
.append('text') |
|
.text(String) |
|
.style(style) |
|
.style('display', 'block') |
|
.each(function() { |
|
var b = this.getBBox(); |
|
if (b.height > maxHeight) maxHeight = b.height; |
|
if (b.width > maxWidth) maxWidth = b.width; |
|
}).remove(); |
|
|
|
return { |
|
height: maxHeight, |
|
width: maxWidth |
|
} |
|
|
|
} |
|
|
|
}); |
|
</script> |
|
</body> |
|
</html> |