/* global d3 */ const margin = { top: 1, right: 1, bottom: 6, left: 1 }, width = 1440 - margin.left - margin.right, height = 800 - margin.top - margin.bottom, formatNumber = d3.format(',.0f'), format = d => `${formatNumber(d)} TWh`, color = d3.scaleOrdinal(d3.schemeCategory20); const canvas = d3.select('#my-canvas').append('canvas') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', `translate(${margin.left},${margin.top})`); const svg = d3.select('#my-canvas').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})`); const sankey = d3.sankey() .nodeWidth(15) .nodePadding(10) .size([width, height]); const path = sankey.link(); const url = 'https://gist.githubusercontent.com/jasonhodges/' + '5e661917fcbe74df4276c9d291f6258c/raw/ca91cebb02800a83d4a712b2f36508008350547f/energy.json'; d3.json(url, (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) .on('click', d => createParticles(energy.links)); 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)}`); function dragmove(d) { d3.select(this).attr('transform', 'translate(' + (d.x = Math.max(0, Math.min(width - d.dx, d3.event.x))) + ',' + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ')'); sankey.relayout(); link.attr('d', path); } function createParticles(l) { const linkExtent = d3.extent(l, d => d.value); const frequencyScale = d3.scaleLinear().domain(linkExtent).range([0.05, 1]); l.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) { particles = particles.filter(d => d.current < d.path.getTotalLength()); d3.selectAll('path.link') .each( function (d) { 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()) }); } } }); particleEdgeCanvasPath(elapsed); } function particleEdgeCanvasPath(elapsed) { const context = d3.select('canvas').node().getContext('2d'); context.clearRect(0, 0, width + margin.left + margin.right, height + margin.top + margin.bottom); context.fillStyle = 'gray'; context.lineWidth = '1px'; for (const x in particles) { if ({}.hasOwnProperty.call(particles, x)) { const currentTime = elapsed - particles[x].time; 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(); } } } } });