Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active April 20, 2023 15:23
Show Gist options
  • Select an option

  • Save emeeks/e749224c89f82788cb18 to your computer and use it in GitHub Desktop.

Select an option

Save emeeks/e749224c89f82788cb18 to your computer and use it in GitHub Desktop.

Revisions

  1. emeeks revised this gist Mar 8, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion index.html
    Original file line number Diff line number Diff line change
    @@ -42,7 +42,7 @@
    <canvas width="1000" height="1000" ></canvas>
    <svg width="1000" height="1000" ></svg>

    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8" type="text/javascript"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.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">
  2. emeeks revised this gist Nov 24, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    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.
    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:

  3. emeeks revised this gist Nov 24, 2015. No changes.
  4. Elijah Meeks created this gist Nov 24, 2015.
    294 changes: 294 additions & 0 deletions d3.sankey.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,294 @@
    d3.sankey = function() {
    var sankey = {},
    nodeWidth = 24,
    nodePadding = 8,
    size = [1, 1],
    nodes = [],
    links = [];

    sankey.nodeWidth = function(_) {
    if (!arguments.length) return nodeWidth;
    nodeWidth = +_;
    return sankey;
    };

    sankey.nodePadding = function(_) {
    if (!arguments.length) return nodePadding;
    nodePadding = +_;
    return sankey;
    };

    sankey.nodes = function(_) {
    if (!arguments.length) return nodes;
    nodes = _;
    return sankey;
    };

    sankey.links = function(_) {
    if (!arguments.length) return links;
    links = _;
    return sankey;
    };

    sankey.size = function(_) {
    if (!arguments.length) return size;
    size = _;
    return sankey;
    };

    sankey.layout = function(iterations) {
    computeNodeLinks();
    computeNodeValues();
    computeNodeBreadths();
    computeNodeDepths(iterations);
    computeLinkDepths();
    return sankey;
    };

    sankey.relayout = function() {
    computeLinkDepths();
    return sankey;
    };

    sankey.link = function() {
    var curvature = .5;

    function link(d) {
    var x0 = d.source.x + d.source.dx,
    x1 = d.target.x,
    xi = d3.interpolateNumber(x0, x1),
    x2 = xi(curvature),
    x3 = xi(1 - curvature),
    y0 = d.source.y + d.sy + d.dy / 2,
    y1 = d.target.y + d.ty + d.dy / 2;
    return "M" + x0 + "," + y0
    + "C" + x2 + "," + y0
    + " " + x3 + "," + y1
    + " " + x1 + "," + y1;
    }

    link.curvature = function(_) {
    if (!arguments.length) return curvature;
    curvature = +_;
    return link;
    };

    return link;
    };

    // Populate the sourceLinks and targetLinks for each node.
    // Also, if the source and target are not objects, assume they are indices.
    function computeNodeLinks() {
    nodes.forEach(function(node) {
    node.sourceLinks = [];
    node.targetLinks = [];
    });
    links.forEach(function(link) {
    var source = link.source,
    target = link.target;
    if (typeof source === "number") source = link.source = nodes[link.source];
    if (typeof target === "number") target = link.target = nodes[link.target];
    source.sourceLinks.push(link);
    target.targetLinks.push(link);
    });
    }

    // Compute the value (size) of each node by summing the associated links.
    function computeNodeValues() {
    nodes.forEach(function(node) {
    node.value = Math.max(
    d3.sum(node.sourceLinks, value),
    d3.sum(node.targetLinks, value)
    );
    });
    }

    // Iteratively assign the breadth (x-position) for each node.
    // Nodes are assigned the maximum breadth of incoming neighbors plus one;
    // nodes with no incoming links are assigned breadth zero, while
    // nodes with no outgoing links are assigned the maximum breadth.
    function computeNodeBreadths() {
    var remainingNodes = nodes,
    nextNodes,
    x = 0;

    while (remainingNodes.length) {
    nextNodes = [];
    remainingNodes.forEach(function(node) {
    node.x = x;
    node.dx = nodeWidth;
    node.sourceLinks.forEach(function(link) {
    if (nextNodes.indexOf(link.target) < 0) {
    nextNodes.push(link.target);
    }
    });
    });
    remainingNodes = nextNodes;
    ++x;
    }

    //
    moveSinksRight(x);
    scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
    }

    function moveSourcesRight() {
    nodes.forEach(function(node) {
    if (!node.targetLinks.length) {
    node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
    }
    });
    }

    function moveSinksRight(x) {
    nodes.forEach(function(node) {
    if (!node.sourceLinks.length) {
    node.x = x - 1;
    }
    });
    }

    function scaleNodeBreadths(kx) {
    nodes.forEach(function(node) {
    node.x *= kx;
    });
    }

    function computeNodeDepths(iterations) {
    var nodesByBreadth = d3.nest()
    .key(function(d) { return d.x; })
    .sortKeys(d3.ascending)
    .entries(nodes)
    .map(function(d) { return d.values; });

    //
    initializeNodeDepth();
    resolveCollisions();
    for (var alpha = 1; iterations > 0; --iterations) {
    relaxRightToLeft(alpha *= .99);
    resolveCollisions();
    relaxLeftToRight(alpha);
    resolveCollisions();
    }

    function initializeNodeDepth() {
    var ky = d3.min(nodesByBreadth, function(nodes) {
    return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
    });

    nodesByBreadth.forEach(function(nodes) {
    nodes.forEach(function(node, i) {
    node.y = i;
    node.dy = node.value * ky;
    });
    });

    links.forEach(function(link) {
    link.dy = link.value * ky;
    });
    }

    function relaxLeftToRight(alpha) {
    nodesByBreadth.forEach(function(nodes, breadth) {
    nodes.forEach(function(node) {
    if (node.targetLinks.length) {
    var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
    node.y += (y - center(node)) * alpha;
    }
    });
    });

    function weightedSource(link) {
    return center(link.source) * link.value;
    }
    }

    function relaxRightToLeft(alpha) {
    nodesByBreadth.slice().reverse().forEach(function(nodes) {
    nodes.forEach(function(node) {
    if (node.sourceLinks.length) {
    var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
    node.y += (y - center(node)) * alpha;
    }
    });
    });

    function weightedTarget(link) {
    return center(link.target) * link.value;
    }
    }

    function resolveCollisions() {
    nodesByBreadth.forEach(function(nodes) {
    var node,
    dy,
    y0 = 0,
    n = nodes.length,
    i;

    // Push any overlapping nodes down.
    nodes.sort(ascendingDepth);
    for (i = 0; i < n; ++i) {
    node = nodes[i];
    dy = y0 - node.y;
    if (dy > 0) node.y += dy;
    y0 = node.y + node.dy + nodePadding;
    }

    // If the bottommost node goes outside the bounds, push it back up.
    dy = y0 - nodePadding - size[1];
    if (dy > 0) {
    y0 = node.y -= dy;

    // Push any overlapping nodes back up.
    for (i = n - 2; i >= 0; --i) {
    node = nodes[i];
    dy = node.y + node.dy + nodePadding - y0;
    if (dy > 0) node.y -= dy;
    y0 = node.y;
    }
    }
    });
    }

    function ascendingDepth(a, b) {
    return a.y - b.y;
    }
    }

    function computeLinkDepths() {
    nodes.forEach(function(node) {
    node.sourceLinks.sort(ascendingTargetDepth);
    node.targetLinks.sort(ascendingSourceDepth);
    });
    nodes.forEach(function(node) {
    var sy = 0, ty = 0;
    node.sourceLinks.forEach(function(link) {
    link.sy = sy;
    sy += link.dy;
    });
    node.targetLinks.forEach(function(link) {
    link.ty = ty;
    ty += link.dy;
    });
    });

    function ascendingSourceDepth(a, b) {
    return a.source.y - b.source.y;
    }

    function ascendingTargetDepth(a, b) {
    return a.target.y - b.target.y;
    }
    }

    function center(node) {
    return node.y + node.dy / 2;
    }

    function value(link) {
    return link.value;
    }

    return sankey;
    };
    120 changes: 120 additions & 0 deletions energy.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,120 @@
    {"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}
    ]}
    194 changes: 194 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,194 @@

    <!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="1000" height="1000" ></canvas>
    <svg width="1000" height="1000" ></svg>

    <script src="http://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 margin = {top: 1, right: 1, bottom: 6, left: 1},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

    var formatNumber = d3.format(",.0f"),
    format = function(d) { return formatNumber(d) + " TWh"; },
    color = d3.scale.category20();

    var 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 + ")");

    var sankey = d3.sankey()
    .nodeWidth(15)
    .nodePadding(10)
    .size([width, height]);

    var path = sankey.link();

    var freqCounter = 1;


    d3.json("energy.json", function(energy) {

    sankey
    .nodes(energy.nodes)
    .links(energy.links)
    .layout(32);

    var link = svg.append("g").selectAll(".link")
    .data(energy.links)
    .enter().append("path")
    .attr("class", "link")
    .attr("d", path)
    .style("stroke-width", function(d) { return Math.max(1, d.dy); })
    .sort(function(a, b) { return b.dy - a.dy; });

    link.append("title")
    .text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); });

    var node = svg.append("g").selectAll(".node")
    .data(energy.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")
    .append("title")
    .text(function(d) { return d.name + "\n" + format(d.value); });

    node.append("text")
    .attr("x", -6)
    .attr("y", function(d) { return d.dy / 2; })
    .attr("dy", ".35em")
    .attr("text-anchor", "end")
    .attr("transform", null)
    .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(energy.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]);


    energy.links.forEach(function (link) {
    link.freq = frequencyScale(link.value);
    link.particleSize = 2;
    link.particleColor = d3.scale.linear().domain([0,1])
    .range([link.source.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();
    }
    }


    });

    </script>
    </body>
    </html>
    7 changes: 7 additions & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    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.

    Other examples of sankeys with particles:

    * [Fixed speed particles transitioning in color from source node color to target node color.](http://bl.ocks.org/emeeks/9673c96a682fe3948379)

    * [Particles of differing sizes and differing speeds moving in "bursts" between nodes.](http://bl.ocks.org/emeeks/21f99959d48dd0d0c746)
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.