an es2015 iteration on the block Sankey Particles III - d3v4 from @BenDilday
Forked from Sankey Particles III from @elijah_meeks
Updated to use d3 v4
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<title>Sankey Particles</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: .15;
}
.link:hover {
stroke-opacity: .25;
}
svg {
position: absolute;
}
canvas {
position: absolute;
}
</style>
</head>
<body>
<canvas width='960' height='960' ></canvas>
<svg width='960' height='960' ></svg>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.js' charset='utf-8' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3-sankey/0.4.2/d3-sankey.js' charset='utf-8' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3-timer/1.0.5/d3-timer.js' charset='utf-8' type='text/javascript'></script>
<script src='vis.js'></script>
</body>
</html>
{"nodes":[
{"name":"Agricultural 'waste'"},
{"name":"Bio-conversion"},
{"name":"Liquid"},
{"name":"Losses"},
{"name":"Solid"},
{"name":"Gas"},
{"name":"Biofuel imports"},
{"name":"Biomass imports"},
{"name":"Coal imports"},
{"name":"Coal"},
{"name":"Coal reserves"},
{"name":"District heating"},
{"name":"Industry"},
{"name":"Heating and cooling - commercial"},
{"name":"Heating and cooling - homes"},
{"name":"Electricity grid"},
{"name":"Over generation / exports"},
{"name":"H2 conversion"},
{"name":"Road transport"},
{"name":"Agriculture"},
{"name":"Rail transport"},
{"name":"Lighting & appliances - commercial"},
{"name":"Lighting & appliances - homes"},
{"name":"Gas imports"},
{"name":"Ngas"},
{"name":"Gas reserves"},
{"name":"Thermal generation"},
{"name":"Geothermal"},
{"name":"H2"},
{"name":"Hydro"},
{"name":"International shipping"},
{"name":"Domestic aviation"},
{"name":"International aviation"},
{"name":"National navigation"},
{"name":"Marine algae"},
{"name":"Nuclear"},
{"name":"Oil imports"},
{"name":"Oil"},
{"name":"Oil reserves"},
{"name":"Other waste"},
{"name":"Pumped heat"},
{"name":"Solar PV"},
{"name":"Solar Thermal"},
{"name":"Solar"},
{"name":"Tidal"},
{"name":"UK land based bioenergy"},
{"name":"Wave"},
{"name":"Wind"}
],
"links":[
{"source":0,"target":1,"value":124.729},
{"source":1,"target":2,"value":0.597},
{"source":1,"target":3,"value":26.862},
{"source":1,"target":4,"value":280.322},
{"source":1,"target":5,"value":81.144},
{"source":6,"target":2,"value":35},
{"source":7,"target":4,"value":35},
{"source":8,"target":9,"value":11.606},
{"source":10,"target":9,"value":63.965},
{"source":9,"target":4,"value":75.571},
{"source":11,"target":12,"value":10.639},
{"source":11,"target":13,"value":22.505},
{"source":11,"target":14,"value":46.184},
{"source":15,"target":16,"value":104.453},
{"source":15,"target":14,"value":113.726},
{"source":15,"target":17,"value":27.14},
{"source":15,"target":12,"value":342.165},
{"source":15,"target":18,"value":37.797},
{"source":15,"target":19,"value":4.412},
{"source":15,"target":13,"value":40.858},
{"source":15,"target":3,"value":56.691},
{"source":15,"target":20,"value":7.863},
{"source":15,"target":21,"value":90.008},
{"source":15,"target":22,"value":93.494},
{"source":23,"target":24,"value":40.719},
{"source":25,"target":24,"value":82.233},
{"source":5,"target":13,"value":0.129},
{"source":5,"target":3,"value":1.401},
{"source":5,"target":26,"value":151.891},
{"source":5,"target":19,"value":2.096},
{"source":5,"target":12,"value":48.58},
{"source":27,"target":15,"value":7.013},
{"source":17,"target":28,"value":20.897},
{"source":17,"target":3,"value":6.242},
{"source":28,"target":18,"value":20.897},
{"source":29,"target":15,"value":6.995},
{"source":2,"target":12,"value":121.066},
{"source":2,"target":30,"value":128.69},
{"source":2,"target":18,"value":135.835},
{"source":2,"target":31,"value":14.458},
{"source":2,"target":32,"value":206.267},
{"source":2,"target":19,"value":3.64},
{"source":2,"target":33,"value":33.218},
{"source":2,"target":20,"value":4.413},
{"source":34,"target":1,"value":4.375},
{"source":24,"target":5,"value":122.952},
{"source":35,"target":26,"value":839.978},
{"source":36,"target":37,"value":504.287},
{"source":38,"target":37,"value":107.703},
{"source":37,"target":2,"value":611.99},
{"source":39,"target":4,"value":56.587},
{"source":39,"target":1,"value":77.81},
{"source":40,"target":14,"value":193.026},
{"source":40,"target":13,"value":70.672},
{"source":41,"target":15,"value":59.901},
{"source":42,"target":14,"value":19.263},
{"source":43,"target":42,"value":19.263},
{"source":43,"target":41,"value":59.901},
{"source":4,"target":19,"value":0.882},
{"source":4,"target":26,"value":400.12},
{"source":4,"target":12,"value":46.477},
{"source":26,"target":15,"value":525.531},
{"source":26,"target":3,"value":787.129},
{"source":26,"target":11,"value":79.329},
{"source":44,"target":15,"value":9.452},
{"source":45,"target":1,"value":182.01},
{"source":46,"target":15,"value":19.013},
{"source":47,"target":15,"value":289.366}
]}
# safe
lebab --replace vis.js --transform arrow
lebab --replace vis.js --transform for-of
lebab --replace vis.js --transform for-each
lebab --replace vis.js --transform arg-rest
lebab --replace vis.js --transform arg-spread
lebab --replace vis.js --transform obj-method
lebab --replace vis.js --transform obj-shorthand
lebab --replace vis.js --transform multi-var
# unsafe
lebab --replace vis.js --transform let
lebab --replace vis.js --transform template
/* global d3 */
const margin = { top: 1, right: 1, bottom: 6, left: 1 };
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const formatNumber = d3.format(',.0f');
const format = d => `${formatNumber(d)} TWh`;
const color = d3.scaleOrdinal(d3.schemeCategory20);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.size([width, height]);
const path = sankey.link();
const freqCounter = 1;
d3.json('energy.json', (energy) => {
sankey
.nodes(energy.nodes)
.links(energy.links)
.layout(32);
const link = svg.append('g').selectAll('.link')
.data(energy.links)
.enter().append('path')
.attr('class', 'link')
.attr('d', path)
.style('stroke-width', d => Math.max(1, d.dy))
.sort((a, b) => b.dy - a.dy);
link.append('title')
.text(d => `${d.source.name} → ${d.target.name}\n${format(d.value)}`);
const node = svg.append('g').selectAll('.node')
.data(energy.nodes)
.enter().append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.x},${d.y})`)
.call(d3.drag()
.subject(d => d)
.on('start', function () { this.parentNode.appendChild(this); })
.on('drag', dragmove));
node.append('rect')
.attr('height', d => d.dy)
.attr('width', sankey.nodeWidth())
.style('fill', (d) => {
d.color = color(d.name.replace(/ .*/, ''));
return d.color;
})
.style('stroke', 'none')
.append('title')
.text(d => `${d.name}\n${format(d.value)}`);
node.append('text')
.attr('x', -6)
.attr('y', d => d.dy / 2)
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.attr('transform', null)
.text(d => d.name)
.filter(d => 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);
}
const linkExtent = d3.extent(energy.links, d => d.value);
const frequencyScale = d3.scaleLinear().domain(linkExtent).range([0.05, 1]);
const particleSize = d3.scaleLinear().domain(linkExtent).range([1, 5]);
energy.links.forEach((link) => {
link.freq = frequencyScale(link.value);
link.particleSize = 2;
link.particleColor = d3.scaleLinear().domain([0, 1])
.range([link.source.color, link.target.color]);
});
const t = d3.timer(tick, 1000);
let particles = [];
function tick(elapsed, time) {
particles = particles.filter(d => d.current < d.path.getTotalLength());
d3.selectAll('path.link')
.each(
function (d) {
// if (d.freq < 1) {
for (let x = 0; x < 2; x += 1) {
const offset = (Math.random() - 0.5) * (d.dy - 4);
if (Math.random() < d.freq) {
const length = this.getTotalLength();
particles.push({ link: d, time: elapsed, offset, path: this, 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) {
const context = d3.select('canvas').node().getContext('2d');
context.clearRect(0, 0, 1000, 1000);
context.fillStyle = 'gray';
context.lineWidth = '1px';
for (const x in particles) {
if ({}.hasOwnProperty.call(particles, x)) {
const currentTime = elapsed - particles[x].time;
// var currentPercent = currentTime / 1000 * particles[x].path.getTotalLength();
particles[x].current = currentTime * 0.15 * particles[x].speed;
const 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();
}
}
}
});