Sankey Particles with only inline styles. A fork of the bl.ock Sankey Particles III from emeeks
Since it doesn't appear possible to set CSS pseudo-class rules from JavaScript we'll bind mouseover
and mouseout
events to each link path to replicate the functionality of the :hover
CSS psuedo-class.
.link:hover {
stroke-opacity: .25;
}
becomes
link
.on('mouseover', function() {
d3.select(this).style("stroke-opacity", .25);
})
.on('mouseout', function() {
d3.select(this).style("stroke-opacity", .15);
});
all other styles can be converted to inline styles more directly, like:
.link {
fill: none;
stroke: #000;
stroke-opacity: .15;
}
to
var link = svg.append("g").selectAll(".link")
/* ... */
.style({
"fill": "none",
"stroke": "#000",
"stroke-opacity": .15
})
Original README.md
:
Using particles to indicate flow between reservoirs in a sankey diagram. This time with particles moving at varying speeds and maintaining the color of the source node. You can drag the reservoirs (the rectangles) to adjust the path of the flows.
Other examples of sankeys with particles:
Fixed speed particles transitioning in color from source node color to target node color.
Particles of differing sizes and differing speeds moving in "bursts" between nodes.
forked from micahstubbs's block: Sankey Particles with only inline styles
forked from anonymous's block: Sankey Particles with only inline styles
xxxxxxxxxx
<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>
https://d3js.org/d3.v3.min.js