block by micahstubbs ae0946f9efe18fc4f67460e7387abc0b

Sankey Particles | d3v4 & es2015+

Full Screen

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

index.html

<!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>

energy.json

{"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}
]}

lebab.sh

# 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

vis.js

/* 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();
      }
    }
  }
});