UNPKG

d3-sankey-diagram

Version:
2,749 lines 72.7 kB
'use strict';

var d3Array = require('d3-array');
var pkg = require('@dagrejs/graphlib');
var d3Collection = require('d3-collection');
var d3Selection = require('d3-selection');
var d3Transition = require('d3-transition');
var d3Dispatch = require('d3-dispatch');
var d3Format = require('d3-format');
var d3Interpolate = require('d3-interpolate');

/**
 * Create a new graph where nodes in the same rank set are merged into one node.
 *
 * Depends on the "backwards" attribute of the nodes in G, and the "delta"
 * atribute of the edges.
 *
 */
function groupedGraph (G, rankSets = []) {
  // Not multigraph because this is only used for calculating ranks
  const GG = new pkg.Graph({ directed: true });
  if (G.nodes().length === 0) return GG

  // Make sure there is a minimum-rank set
  rankSets = ensureSmin(G, rankSets);

  // Construct map of node ids to the set they are in, if any
  const nodeSets = d3Collection.map();
  let set;
  let id;
  let i;
  let j;
  for (i = 0; i < rankSets.length; ++i) {
    set = rankSets[i];
    if (!set.nodes || set.nodes.length === 0) continue
    id = '' + i;
    for (j = 0; j < set.nodes.length; ++j) {
      nodeSets.set(set.nodes[j], id);
    }
    GG.setNode(id, { type: set.type, nodes: set.nodes });
  }

  // use i to keep counting new ids
  G.nodes().forEach(u => {
    if (!nodeSets.has(u)) {
      id = '' + (i++);
      set = { type: 'same', nodes: [u] };
      nodeSets.set(u, id);
      GG.setNode(id, set);
    }
  });

  // Add edges between nodes/groups
  G.edges().forEach(e => {
    const sourceSet = nodeSets.get(e.v);
    const targetSet = nodeSets.get(e.w);

    // Minimum edge length depends on direction of nodes:
    //  -> to -> : 1
    //  -> to <- : 0
    //  <- to -> : 0 (in opposite direction??)
    //  <- to <- : 1 in opposite direction
    const edge = GG.edge(sourceSet, targetSet) || { delta: 0 };
    if (sourceSet === targetSet) {
      edge.delta = 0;
      GG.setEdge(sourceSet, targetSet, edge);
    } else if (G.node(e.v).backwards) {
      edge.delta = Math.max(edge.delta, G.node(e.w).backwards ? 1 : 0);
      GG.setEdge(targetSet, sourceSet, edge);
    } else {
      edge.delta = Math.max(edge.delta, G.node(e.w).backwards ? 0 : 1);
      GG.setEdge(sourceSet, targetSet, edge);
    }
  });

  return GG
}

// export function linkDelta (nodeBackwards, link) {
//   if (nodeBackwards(link.source)) {
//     return nodeBackwards(link.target) ? 1 : 0
//   } else {
//     return nodeBackwards(link.target) ? 0 : 1
//   }
// }

function ensureSmin (G, rankSets) {
  for (let i = 0; i < rankSets.length; ++i) {
    if (rankSets[i].type === 'min') {
      return rankSets // ok
    }
  }

  // find the first sourceSet node, or else use the first node
  const sources = G.sources();
  const n0 = sources.length ? sources[0] : G.nodes()[0];
  return [{ type: 'min', nodes: [n0] }].concat(rankSets)
}

/**
 * Reverse edges in G to make it acyclic
 */
function makeAcyclic (G, v0) {
  const tree = findSpanningTree(G, v0);

  G.edges().forEach(e => {
    const rel = nodeRelationship(tree, e.v, e.w);
    if (rel < 0) {
      const label = G.edge(e) || {};
      label.reversed = true;
      G.removeEdge(e);
      G.setEdge(e.w, e.v, label);
    }
  });

  return G
}

// find spanning tree, starting from the given node.
// return new graph where nodes have depth and thread
function findSpanningTree (G, v0) {
  const visited = d3Collection.set();
  const tree = new pkg.Graph({ directed: true });
  const thread = [];

  if (!G.hasNode(v0)) throw Error('node not in graph')

  doDfs(G, v0, visited, tree, thread);
  G.nodes().forEach(u => {
    if (!visited.has(u)) {
      doDfs(G, u, visited, tree, thread);
    }
  });

  thread.forEach((u, i) => {
    tree.node(u).thread = (i + 1 < thread.length) ? thread[i + 1] : thread[0];
  });

  return tree
}

/**
 * Returns 1 if w is a descendent of v, -1 if v is a descendent of w, and 0 if
 * they are unrelated.
 */
function nodeRelationship (tree, v, w) {
  const V = tree.node(v);
  const W = tree.node(w);
  if (V.depth < W.depth) {
    let u = V.thread; // next node
    while (tree.node(u).depth > V.depth) {
      if (u === w) return 1
      u = tree.node(u).thread;
    }
  } else if (W.depth < V.depth) {
    let u = W.thread; // next node
    while (tree.node(u).depth > W.depth) {
      if (u === v) return -1
      u = tree.node(u).thread;
    }
  }
  return 0
}

function doDfs (G, v, visited, tree, thread, depth = 0) {
  if (!visited.has(v)) {
    visited.add(v);
    thread.push(v);
    tree.setNode(v, { depth });

    // It doesn't seem to cause a problem with letters as node ids, but numbers
    // are sorted when using G.successors(). So use G.outEdges() instead.
    const next = G.outEdges(v).map(e => e.w);
    next.forEach((w, i) => {
      if (!visited.has(w)) {
        tree.setEdge(v, w, { delta: 1 });
      }
      doDfs(G, w, visited, tree, thread, depth + 1);
    });
  }
}

/**
 * Take an acyclic graph and assign initial ranks to the nodes
 */
function assignInitialRanks (G) {
  // Place nodes on queue when they have no unmarked in-edges. Initially, this
  // means sources.
  const queue = G.sources();
  const seen = d3Collection.set();
  const marked = d3Collection.set();

  // Mark any loops, since they don't affect rank assignment
  G.edges().forEach(e => {
    if (e.v === e.w) marked.add(edgeIdString(e));
  });

  G.nodes().forEach(v => {
    G.node(v).rank = 0;
  });

  while (queue.length > 0) {
    const v = queue.shift();
    seen.add(v);

    let V = G.node(v);
    if (!V) G.setNode(v, (V = {}));

    // Set rank to minimum of incoming edges
    V.rank = 0;
    G.inEdges(v).forEach(e => {
      const delta = G.edge(e).delta === undefined ? 1 : G.edge(e).delta;
      V.rank = Math.max(V.rank, G.node(e.v).rank + delta);
    });

    // Mark outgoing edges
    G.outEdges(v).forEach(e => {
      marked.add(edgeIdString(e));
    });

    // Add nodes to queue when they have no unmarked in-edges.
    G.nodes().forEach(n => {
      if (queue.indexOf(n) < 0 && !seen.has(n) &&
          !G.inEdges(n).some(e => !marked.has(edgeIdString(e)))) {
        queue.push(n);
      }
    });
  }
}

function edgeIdString (e) {
  return e.v + '\x01' + e.w + '\x01' + e.name
}

/**
 * Assign ranks to the nodes in G, according to rankSets.
 */
function assignRanks (G, rankSets) {
  // Group nodes together, and add additional edges from Smin to sources
  const GG = groupedGraph(G, rankSets);
  if (GG.nodeCount() === 0) return

  // Add additional edges from Smin to sources
  addTemporaryEdges(GG);

  // Make the graph acyclic
  makeAcyclic(GG, '0');

  // Assign the initial ranks
  assignInitialRanks(GG);

  // XXX improve initial ranking...
  moveSourcesRight(GG);

  // Apply calculated ranks to original graph
  // const ranks = []
  GG.nodes().forEach(u => {
    const groupedNode = GG.node(u);
    // while (node.rank >= ranks.length) ranks.push([])
    groupedNode.nodes.forEach(v => {
      G.node(v).rank = groupedNode.rank;
    });
  });
  // return ranks
}

// export function nodeBackwards (link) {
//   if (link.source.direction === 'l') {
//     return link.target.direction === 'l' ? 1 : 0
//   } else {
//     return link.target.direction === 'l' ? 0 : 1
//   }
// }

function addTemporaryEdges (GG) {
  // Add temporary edges between Smin and sources
  GG.sources().forEach(u => {
    if (u !== '0') {
      GG.setEdge('0', u, { temp: true, delta: 0 });
    }
  });

  // XXX Should also add edges from sinks to Smax

  // G.nodes().forEach(u => {
  //   if (!nodeSets.has(u)) {
  //     GG.
  //   }
  // });
}

function moveSourcesRight (GG) {
  GG.edges().forEach(e => {
    const edge = GG.edge(e);
    if (edge.temp) moveRight(e.w);
  });

  function moveRight (v) {
    const V = GG.node(v);
    const rank = d3Array.min(GG.outEdges(v), e => GG.node(e.w).rank - GG.edge(e).delta);
    if (rank !== undefined) V.rank = rank;
  }
}

const { alg } = pkg;

function initialOrdering (G, ranks) {
  const order = [];
  if (ranks.length === 0) return order

  // Start with sources & nodes in rank 0
  const start = G.sources();
  const nodeRanks = d3Collection.map();
  ranks.forEach((nodes, i) => {
    order.push([]);
    nodes.forEach(u => {
      if (i === 0 && start.indexOf(u) < 0) start.push(u);
      nodeRanks.set(u, i);
    });
  });

  alg.preorder(G, start).forEach(u => {
    order[nodeRanks.get(u)].push(u);
  });

  return order
}

/** @module node-ordering/count-crossings */

/**
 * Count the total number of crossings between 2 layers.
 *
 * This is the sum of the countBetweenCrossings and countLoopCrossings.
 *
 * @param {Graph} G - The graph.
 * @param {Array} orderA - List of node ids on left side.
 * @param {Array} orderB - List of node ids on right side.
 */
function countCrossings (G, orderA, orderB) {
  return (
    countBetweenCrossings(G, orderA, orderB) // +
    // countLoopCrossings(G, orderA, orderB)
  )
}

/**
 * Count the number of crossings of edges passing between 2 layers.
 *
 * Algorithm from
 * http://jgaa.info/accepted/2004/BarthMutzelJuenger2004.8.2.pdf
 *
 * @param {Graph} G - The graph.
 * @param {Array} orderA - List of node ids on left side.
 * @param {Array} orderB - List of node ids on right side.
 */
function countBetweenCrossings (G, orderA, orderB) {
  let north;
  let south;

  if (orderA.length > orderB.length) {
    north = orderA;
    south = orderB;
  } else {
    north = orderB;
    south = orderA;
  }
  const q = south.length;

  // lexicographically sorted edges from north to south
  const southSeq = [];
  north.forEach(u => {
    south.forEach((v, j) => {
      if (G.hasEdge(u, v) || G.hasEdge(v, u)) southSeq.push(j);
    });
  });

  // build accumulator tree
  let firstIndex = 1;
  while (firstIndex < q) firstIndex *= 2;
  const treeSize = 2 * firstIndex - 1; // number of tree nodes
  firstIndex -= 1; // index of leftmost leaf

  const tree = new Array(treeSize);
  for (let i = 0; i < treeSize; i++) tree[i] = 0;

  // count the crossings
  let count = 0;
  southSeq.forEach(k => {
    let index = k + firstIndex;
    tree[index]++;
    while (index > 0) {
      if (index % 2) count += tree[index + 1];
      index = Math.floor((index - 1) / 2);
      tree[index]++;
    }
  });

  return count
}

function swapNodes (G, order) {
  let improved = true;
  while (improved) {
    improved = false;
    for (let i = 0; i < order.length; ++i) {
      for (let j = 0; j < order[i].length - 1; ++j) {
        const count0 = allCrossings$1(G, order, i);
        transpose(order[i], j, j + 1);
        const count1 = allCrossings$1(G, order, i);

        if (count1 < count0) {
          improved = true;
        } else {
          transpose(order[i], j, j + 1); // put back
        }
      }
    }
  }
}

function allCrossings$1 (G, order, i) {
  let count = 0;
  if (i > 0) {
    count += countCrossings(G, order[i - 1], order[i]);
  }
  if (i + 1 < order.length) {
    count += countCrossings(G, order[i], order[i + 1]);
  }
  return count
}

function transpose (list, i, j) {
  const tmp = list[i];
  list[i] = list[j];
  list[j] = tmp;
}

function medianValue (positions) {
  const m = Math.floor(positions.length / 2);
  if (positions.length === 0) {
    return -1
  } else if (positions.length % 2 === 1) {
    return positions[m]
  } else if (positions.length === 2) {
    return (positions[0] + positions[1]) / 2
  } else {
    const left = positions[m - 1] - positions[0];
    const right = positions[positions.length - 1] - positions[m];
    return (positions[m - 1] * right + positions[m] * left) / (left + right)
  }
}

function neighbourPositions (G, order, i, j, u, includeLoops = false) {
  // current rank i
  // neighbour rank j
  const thisRank = order[i];
  const otherRank = order[j];

  const positions = [];

  // neighbouring positions on other rank
  otherRank.forEach((n, i) => {
    if (G.nodeEdges(n, u).length > 0) {
      positions.push(i);
    }
  });

  if (positions.length === 0 && includeLoops) {
    // if no neighbours in other rank, look for loops to this rank
    // XXX only on one side?
    thisRank.forEach((n, i) => {
      if (G.nodeEdges(n, u).length > 0) {
        positions.push(i + 0.5);
      }
    });
  }

  positions.sort((a, b) => a - b);

  return positions
}

/**
 * Sort arr according to order. -1 in order means stay in same position.
 */
function sortByPositions (arr, order) {
  const origOrder = d3Collection.map(arr.map((d, i) => [d, i]), d => d[0]);

  // console.log('sorting', arr, order, origOrder)
  for (let i = 1; i < arr.length; ++i) {
    // console.group('start', i, arr[i])
    for (let k = i; k > 0; --k) {
      let j = k - 1;
      let a = order.get(arr[j]);
      let b = order.get(arr[k]);

      // count back over any fixed positions (-1)
      while ((a = order.get(arr[j])) === -1 && j > 0) j--;

      // console.log(j, k, arr[j], arr[k], a, b)
      if (b === -1 || a === -1) {
        // console.log('found -1', a, b, 'skipping', j, k)
        break
      }

      if (a === b) {
        a = origOrder.get(arr[j]);
        b = origOrder.get(arr[k]);
        // console.log('a == b, switching to orig order', a, b)
      }

      if (b >= a) {
        // console.log('k > k -1, stopping')
        break
      }
      // console.log('swapping', arr[k], arr[j])
      // swap arr[k], arr[j]
      [arr[k], arr[j]] = [arr[j], arr[k]];
      // console.log(arr)
    }
    // console.groupEnd()
  }
  // console.log('-->', arr)
}

function sortNodes$1 (G, order, sweepDirection = 1, includeLoops = false) {
  if (sweepDirection > 0) {
    for (let r = 1; r < order.length; ++r) {
      const medians = d3Collection.map();
      order[r].forEach(u => {
        const neighbour = medianValue(neighbourPositions(G, order, r, r - 1, u, includeLoops));
        medians.set(u, neighbour);
      });
      sortByPositions(order[r], medians);
    }
  } else {
    for (let r = order.length - 2; r >= 0; --r) {
      const medians = d3Collection.map();
      order[r].forEach(u => {
        const neighbour = medianValue(neighbourPositions(G, order, r, r + 1, u, includeLoops));
        medians.set(u, neighbour);
      });
      sortByPositions(order[r], medians);
    }
  }
}

/** @module node-ordering */


/**
 * Sorts the nodes in G, setting the `depth` attribute on each.
 *
 * @param {Graph} G - The graph. Nodes must have a `rank` attribute.
 *
 */
function sortNodes (G, maxIterations = 25) {
  const ranks = getRanks(G);
  const order = initialOrdering(G, ranks);
  let best = order;
  let i = 0;

  while (i++ < maxIterations) {
    sortNodes$1(G, order, (i % 2 === 0));
    swapNodes(G, order);
    if (allCrossings(G, order) < allCrossings(G, best)) {
      // console.log('improved', allCrossings(G, order), order);
      best = copy(order);
    }
  }

  // Assign depth to nodes
  // const depths = map()
  best.forEach(nodes => {
    nodes.forEach((u, i) => {
      // depths.set(u, i)
      G.node(u).depth = i;
    });
  });
}

function getRanks (G) {
  const ranks = [];
  G.nodes().forEach(u => {
    const r = G.node(u).rank || 0;
    while (r >= ranks.length) ranks.push([]);
    ranks[r].push(u);
  });
  return ranks
}

function allCrossings (G, order) {
  let count = 0;
  for (let i = 0; i < order.length - 1; ++i) {
    count += countCrossings(G, order[i], order[i + 1]);
  }
  return count
}

function copy (order) {
  const result = [];
  order.forEach(rank => {
    result.push(rank.map(d => d));
  });
  return result
}

function addDummyNodes (G) {
  // Add edges & dummy nodes
  if (typeof G.graph() !== 'object') G.setGraph({});
  G.graph().dummyChains = [];
  G.edges().forEach(e => normaliseEdge(G, e));
}

// based on https://github.com/cpettitt/dagre/blob/master/lib/normalize.js
function normaliseEdge (G, e) {
  const edge = G.edge(e);
  const dummies = dummyNodes(G.node(e.v), G.node(e.w));
  if (dummies.length === 0) return

  G.removeEdge(e);

  let v = e.v;
  dummies.forEach((dummy, i) => {
    const id = `__${e.v}_${e.w}_${i}`;
    if (!G.hasNode(id)) {
      dummy.dummy = 'edge';
      G.setNode(id, dummy);
      if (i === 0) {
        G.graph().dummyChains.push(id);
      }
    }
    addDummyEdge(v, (v = id));
  });
  addDummyEdge(v, e.w);

  function addDummyEdge (v, w) {
    const label = { points: [], value: edge.value, origEdge: e, origLabel: edge };
    G.setEdge(v, w, label, e.name);
  }
}

function removeDummyNodes (G) {
  const chains = G.graph().dummyChains || [];
  chains.forEach(v => {
    let node = G.node(v);
    let dummyEdges = G.inEdges(v).map(e => G.edge(e));

    // Set dy and starting point of edge and add back to graph
    dummyEdges.forEach(dummyEdge => {
      dummyEdge.origLabel.dy = dummyEdge.dy;
      dummyEdge.origLabel.x0 = dummyEdge.x0;
      dummyEdge.origLabel.y0 = dummyEdge.y0;
      dummyEdge.origLabel.r0 = dummyEdge.r0;
      dummyEdge.origLabel.d0 = dummyEdge.d0;
      G.setEdge(dummyEdge.origEdge, dummyEdge.origLabel);
    });
    let r1s = dummyEdges.map(dummyEdge => dummyEdge.r1);

    // Walk through chain
    let w;
    while (node.dummy) {
      dummyEdges = G.outEdges(v).map(e => G.edge(e));
      dummyEdges.forEach((dummyEdge, i) => {
        dummyEdge.origLabel.points.push({
          x: (node.x0 + node.x1) / 2,
          y: dummyEdge.y0,
          d: dummyEdge.d0,
          ro: dummyEdge.r0,
          ri: r1s[i] // from last edge
        });
      });
      r1s = dummyEdges.map(dummyEdge => dummyEdge.r1);

      // move on
      w = G.successors(v)[0];
      G.removeNode(v);
      node = G.node(v = w);
    }

    // Set ending point of edge
    dummyEdges.forEach(dummyEdge => {
      dummyEdge.origLabel.x1 = dummyEdge.x1;
      dummyEdge.origLabel.y1 = dummyEdge.y1;
      dummyEdge.origLabel.r1 = dummyEdge.r1;
      dummyEdge.origLabel.d1 = dummyEdge.d1;
    });
  });
}

function dummyNodes (source, target) {
  const dummyNodes = [];
  let r = source.rank;

  if (r + 1 <= target.rank) {
    // add more to get forwards
    if (source.backwards) {
      dummyNodes.push({ rank: r, backwards: false }); // turn around
    }
    while (++r < target.rank) {
      dummyNodes.push({ rank: r, backwards: false });
    }
    if (target.backwards) {
      dummyNodes.push({ rank: r, backwards: false }); // turn around
    }
  } else if (r > target.rank) {
    // add more to get backwards
    if (!source.backwards) {
      dummyNodes.push({ rank: r, backwards: true }); // turn around
    }
    while (r-- > target.rank + 1) {
      dummyNodes.push({ rank: r, backwards: true });
    }
    if (!target.backwards) {
      dummyNodes.push({ rank: r, backwards: true }); // turn around
    }
  }

  return dummyNodes
}

function nestGraph (nodes) {
  const maxRank = d3Array.max(nodes, d => d.rank || 0) || 0;
  const maxBand = d3Array.max(nodes, d => d.band || 0) || 0;

  // const nodes = graph.nodes().concat(graph.dummyNodes())

  const nested = d3Collection.nest()
    .key(d => d.rank || 0)
    .key(d => d.band || 0)
    .sortValues((a, b) => a.depth - b.depth)
    .map(nodes);

  const result = new Array(maxRank + 1);
  let rank;
  for (let i = 0; i <= maxRank; ++i) {
    result[i] = new Array(maxBand + 1);
    rank = nested.get(i);
    if (rank) {
      for (let j = 0; j <= maxBand; ++j) {
        result[i][j] = rank.get(j) || [];
      }
    } else {
      for (let j = 0; j <= maxBand; ++j) {
        result[i][j] = [];
      }
    }
  }

  result.bandValues = bandValues(result);

  return result
}

function bandValues (nested) {
  if (nested.length === 0 || nested[0].length === 0) return []

  const Nb = nested[0].length;
  const values = new Array(Nb);
  for (let i = 0; i < Nb; i++) values[i] = 0;

  nested.forEach(rank => {
    rank.forEach((band, j) => {
      const total = d3Array.sum(band, d => d.value);
      values[j] = Math.max(values[j], total);
    });
  });

  return values
}

// export function minEdgeDx (w, y0, y1) {
//   console.log('mindx', w, y0, y1)
//   const dy = y1 - y0
//   const ay = Math.abs(dy) - w  // final sign doesn't matter
//   const dx2 = w * w - ay * ay
//   const dx = dx2 >= 0 ? Math.sqrt(dx2) : w
//   return dx
// }

function positionHorizontally (G, width, nodeWidth) {
  // const minWidths = new Array(maxRank).fill(0)
  // G.edges().forEach(e => {
  //   const r0 = G.node(e.v).rank || 0
  //   minWidths[r0] = Math.max(minWidths[r0], minEdgeDx(G.edge(e).dy, G.node(e.v).y, G.node(e.w).y))
  // })
  // for (let i = 0; i < nested.length - 1; ++i) {
  //   minWidths[i] = 0
  //   nested[i].forEach(band => {
  //     band.forEach(d => {
  //       // edges for dummy nodes, outgoing for real nodes
  //       (d.outgoing || d.edges).forEach(e => {
  //         minWidths[i] = Math.max(minWidths[i], minEdgeDx(e))
  //       })
  //     })
  //   })
  // }
  // const totalMinWidth = sum(minWidths)
  // let dx
  // if (totalMinWidth > width) {
  //   // allocate fairly
  //   dx = minWidths.map(w => width * w / totalMinWidth)
  // } else {
  //   const spare = (width - totalMinWidth) / (nested.length - 1)
  //   dx = minWidths.map(w => w + spare)
  // }

  const maxRank = d3Array.max(G.nodes(), u => G.node(u).rank || 0) || 0;
  const dx = (width - nodeWidth) / maxRank;

  G.nodes().forEach(u => {
    const node = G.node(u);
    node.x0 = dx * (node.rank || 0);
    node.x1 = node.x0 + nodeWidth;
  });
}

function defaultSeparation (a, b) {
  return 1
}

function positionNodesVertically$1 () {
  let separation = defaultSeparation;

  function layout (nested, totalHeight, whitespace) {
    nested.forEach(layer => {
      let y = 0;
      layer.forEach((band, j) => {
        // Height of this band, based on fraction of value
        const bandHeight = nested.bandValues[j] / d3Array.sum(nested.bandValues) * totalHeight;

        const margin = whitespace * bandHeight / 5;
        const height = bandHeight - 2 * margin;
        const total = d3Array.sum(band, d => d.dy);
        const gaps = band.map((d, i) => {
          if (!d.value) return 0
          return band[i + 1] ? separation(band[i], band[i + 1], layout) : 0
        });
        const space = Math.max(0, height - total);
        const kg = d3Array.sum(gaps) ? space / d3Array.sum(gaps) : 0;

        let yy = y + margin;
        if (band.length === 1) {
          // centre vertically
          yy += (height - band[0].dy) / 2;
        }

        let prevGap = Number.MAX_VALUE ; // edge of graph
        band.forEach((node, i) => {
          node.y = yy;
          node.spaceAbove = prevGap;
          node.spaceBelow = gaps[i] * kg;
          yy += node.dy + node.spaceBelow;
          prevGap = node.spaceBelow;

          // XXX is this a good idea?
          if (node.data && node.data.forceY !== undefined) {
            node.y = margin + node.data.forceY * (height - node.dy);
          }
        });
        if (band.length > 0) {
          band[band.length - 1].spaceBelow = Number.MAX_VALUE ; // edge of graph
        }

        y += bandHeight;
      });
    });
  }

  layout.separation = function (x) {
    if (!arguments.length) return separation
    separation = required$3(x);
    return layout
  };

  return layout
}

function required$3 (f) {
  if (typeof f !== 'function') throw new Error()
  return f
}

function prepareNodePorts (G, sortPorts) {
  G.nodes().forEach(u => {
    const node = G.node(u);
    const ports = d3Collection.map();
    function getOrSet (id, side) {
      if (ports.has(id)) return ports.get(id)
      const port = { id, node: node.data, side, incoming: [], outgoing: [] };
      ports.set(id, port);
      return port
    }

    G.inEdges(u).forEach(e => {
      const edge = G.edge(e);
      const port = getOrSet(edge.targetPortId || 'in', node.direction !== 'l' ? 'west' : 'east');
      port.incoming.push(e);
      edge.targetPort = port;
    });
    G.outEdges(u).forEach(e => {
      const edge = G.edge(e);
      const port = getOrSet(edge.sourcePortId || 'out', node.direction !== 'l' ? 'east' : 'west');
      port.outgoing.push(e);
      edge.sourcePort = port;
    });

    node.ports = ports.values();
    node.ports.sort(sortPorts);

    // Initialise from/to elsewhere lists
    // XXX need to take more care with node directions
    node.fromElsewhere = node.fromElsewhere || [];
    node.toElsewhere = node.toElsewhere || [];
    let fromElsewhereDy = 0;
    node.fromElsewhere.forEach(link => {
      link.x1 = node.x0;
      fromElsewhereDy += link.dy;
    });

    // Set positions of ports, roughly -- so the other endpoints of links are
    // known approximately when being sorted.
    const y = { west: fromElsewhereDy, east: 0 };
    const i = { west: 0, east: 0 };
    node.ports.forEach(port => {
      port.y = y[port.side];
      port.index = i[port.side];
      port.dy = Math.max(d3Array.sum(port.incoming, e => G.edge(e).dy),
        d3Array.sum(port.outgoing, e => G.edge(e).dy));
      const x = (port.side === 'west' ? node.x0 : node.x1);

      port.outgoing.forEach(e => {
        const link = G.edge(e);
        link.x0 = x;
        link.y0 = node.y + port.y + link.dy / 2;
      });
      port.incoming.forEach(e => {
        const link = G.edge(e);
        link.x1 = x;
        link.y1 = node.y + port.y + link.dy / 2;
      });
      y[port.side] += port.dy;
      i[port.side] += 1;
    });

    node.toElsewhere.forEach(link => {
      link.x0 = node.x1;
    });
  });
}

function linkDirection (G, e, head = true) {
  if (e.v === e.w) {
    // pretend self-links go downwards
    return Math.PI / 2 * (head ? +1 : -1)
  } else {
    // const source = G.node(e.v)
    // const target = G.node(e.w)
    // return Math.atan2(target.y - source.y,
    //                   target.x0 - source.x1)
    const link = G.edge(e);
    return Math.atan2(link.y1 - link.y0,
      link.x1 - link.x0)
  }
}

/** @module edge-ordering */


/**
 * Order the edges at all nodes.
 */
function orderEdges (G, opts) {
  G.nodes().forEach(u => orderEdgesOne(G, u));
}

/**
 * Order the edges at the given node.
 * The ports have already been setup and sorted.
 */
function orderEdgesOne (G, v) {
  const node = G.node(v);
  node.ports.forEach(port => {
    port.incoming.sort(compareDirection(G, node, false));
    port.outgoing.sort(compareDirection(G, node, true));
  });
}

/**
 * Sort links based on their endpoints & type
 */
function compareDirection (G, node, head = true) {
  return function (a, b) {
    const da = linkDirection(G, a, head);
    const db = linkDirection(G, b, head);
    const c = head ? 1 : -1;

    // links between same node, sort on type
    if (a.v === b.v && a.w === b.w && Math.abs(da - db) < 1e-3) {
      if (typeof a.name === 'number' && typeof b.name === 'number') {
        return a.name - b.name
      } else if (typeof a.name === 'string' && typeof b.name === 'string') {
        return a.name.localeCompare(b.name)
      } else {
        return 0
      }
    }

    // loops to same slice based on y-position
    if (Math.abs(da - db) < 1e-3) {
      if (a.w === b.w) {
        return G.node(b.v).y - G.node(a.v).y
      } else if (a.v === b.v) {
        return G.node(b.w).y - G.node(a.w).y
      } else {
        return 0
      }
    }

    // otherwise sort by direction
    return c * (da - db)
  }
}

function findFirst (links, p) {
  let jmid = null;
  for (let j = 0; j < links.length; ++j) {
    if (p(links[j])) { jmid = j; break }
  }
  return jmid
}

/**
 * Adjust radii of curvature to avoid overlaps, as much as possible.
 * @param links - the list of links, ordered from outside to inside of bend
 * @param rr - "r0" or "r1", the side to work on
 */
function sweepCurvatureInwards (links, rr) {
  if (links.length === 0) return

  // sweep from inside of curvature towards outside
  let Rmin = 0; let h;
  for (let i = links.length - 1; i >= 0; --i) {
    h = links[i].dy / 2;
    if (links[i][rr] - h < Rmin) { // inner radius
      links[i][rr] = Math.min(links[i].Rmax, Rmin + h);
    }
    Rmin = links[i][rr] + h;
  }

  // sweep from outside of curvature towards centre
  let Rmax = links[0].Rmax + links[0].dy / 2;
  for (let i = 0; i < links.length; ++i) {
    h = links[i].dy / 2;
    if (links[i][rr] + h > Rmax) { // outer radius
      links[i][rr] = Math.max(h, Rmax - h);
    }
    Rmax = links[i][rr] - h;
  }
}

/**
 * Edge positioning.
 *
 * @module link-positioning
 */


/*
 * Requires incoming and outgoing attributes on nodes
 */
function layoutLinks (G) {
  setEdgeEndpoints(G);
  setEdgeCurvatures(G);
  return G
}

function setEdgeEndpoints (G) {
  G.nodes().forEach(u => {
    const node = G.node(u);

    let sy = node.y;
    let ty = node.y;

    node.fromElsewhere.forEach(link => {
      link.y1 = ty + link.dy / 2;
      link.d1 = node.backwards ? 'l' : 'r';
      ty += link.dy;
    });

    node.ports.forEach(port => {
      sy = node.y + port.y;
      ty = node.y + port.y;

      port.outgoing.forEach(e => {
        const link = G.edge(e);
        // link.x0 = node.x1
        link.y0 = sy + link.dy / 2;
        link.d0 = node.backwards ? 'l' : 'r';
        sy += link.dy;
      });

      port.incoming.forEach(e => {
        const link = G.edge(e);
        // link.x1 = node.x0
        link.y1 = ty + link.dy / 2;
        link.d1 = node.backwards ? 'l' : 'r';
        ty += link.dy;
      });
    });

    node.toElsewhere.forEach(link => {
      link.y0 = sy + link.dy / 2;
      link.d0 = node.backwards ? 'l' : 'r';
      sy += link.dy;
    });
  });
}

function setEdgeCurvatures (G) {
  G.nodes().forEach(u => {
    const node = G.node(u);
    setEdgeEndCurvatures(node.toElsewhere, 'r0');
    setEdgeEndCurvatures(node.fromElsewhere, 'r1');
    node.ports.forEach(port => {
      setEdgeEndCurvatures(port.outgoing.map(e => G.edge(e)), 'r0');
      setEdgeEndCurvatures(port.incoming.map(e => G.edge(e)), 'r1');
    });
  });
}

function maximumRadiusOfCurvature (link) {
  const Dx = link.x1 - link.x0;
  const Dy = link.y1 - link.y0;
  if (link.d0 !== link.d1) {
    return Math.abs(Dy) / 2.1
  } else {
    return (Dy !== 0) ? (Dx * Dx + Dy * Dy) / Math.abs(4 * Dy) : Infinity
  }
}

function setEdgeEndCurvatures (links, rr) {
  // initialise segments, find reversal of curvature
  links.forEach(link => {
    // const link = (i < 0) ? link.segments[link.segments.length + i] : link.segments[i]
    link.Rmax = maximumRadiusOfCurvature(link);
    link[rr] = Math.max(link.dy / 2, (link.d0 === link.d1 ? link.Rmax * 0.6 : (5 + link.dy / 2)));
  });

  let jmid = (rr === 'r0'
    ? findFirst(links, f => f.y1 > f.y0)
    : findFirst(links, f => f.y0 > f.y1));
  if (jmid === null) jmid = links.length;

  // Set maximum radius down from middle
  sweepCurvatureInwards(links.slice(jmid), rr);

  // Set maximum radius up from middle
  if (jmid > 0) {
    const links2 = [];
    for (let j = jmid - 1; j >= 0; j--) links2.push(links[j]);
    sweepCurvatureInwards(links2, rr);
  }
}

const { Graph } = pkg;

function buildGraph (graph, nodeId, nodeBackwards, sourceId, targetId, linkType, linkValue) {
  const G = new Graph({ directed: true, multigraph: true });
  graph.nodes.forEach(function (node, i) {
    const id = nodeId(node, i);
    if (G.hasNode(id)) throw new Error('duplicate: ' + id)
    G.setNode(id, {
      data: node,
      index: i,
      backwards: nodeBackwards(node, i),
      fromElsewhere: node.fromElsewhere || [],
      toElsewhere: node.toElsewhere || [],
      // XXX don't need these now have nodePositions?
      x0: node.x0,
      x1: node.x1,
      y: node.y0
    });
  });

  graph.links.forEach(function (link, i) {
    const v = idAndPort(sourceId(link, i));
    const w = idAndPort(targetId(link, i));
    const label = {
      data: link,
      sourcePortId: v.port,
      targetPortId: w.port,
      index: i,
      points: [],
      value: linkValue(link, i),
      type: linkType(link, i)
    };
    if (!G.hasNode(v.id)) throw new Error('missing: ' + v.id)
    if (!G.hasNode(w.id)) throw new Error('missing: ' + w.id)
    G.setEdge(v.id, w.id, label, linkType(link, i));
  });

  G.setGraph({});

  return G
}

function idAndPort (x) {
  if (typeof x === 'object') return x
  return { id: x, port: undefined }
}

/**
 */


function defaultNodes (graph) {
  return graph.nodes
}

function defaultLinks (graph) {
  return graph.links
}

function defaultNodeId (d) {
  return d.id
}

function defaultNodeBackwards (d) {
  return d.direction && d.direction.toLowerCase() === 'l'
}

function defaultSourceId (d) {
  // return typeof d.source === 'object' ? d.source.id : d.source
  return {
    id: typeof d.source === 'object' ? d.source.id : d.source,
    port: typeof d.sourcePort === 'object' ? d.sourcePort.id : d.sourcePort
  }
}

function defaultTargetId (d) {
  // return typeof d.target === 'object' ? d.target.id : d.target
  return {
    id: typeof d.target === 'object' ? d.target.id : d.target,
    port: typeof d.targetPort === 'object' ? d.targetPort.id : d.targetPort
  }
}

function defaultLinkType (d) {
  return d.type
}

function defaultSortPorts (a, b) {
  // XXX weighted sum
  return a.id.localeCompare(b.id)
}

// function defaultNodeSubdivisions

function sankeyLayout () {
  let nodes = defaultNodes;
  let links = defaultLinks;
  let nodeId = defaultNodeId;
  let nodeBackwards = defaultNodeBackwards;
  let sourceId = defaultSourceId;
  let targetId = defaultTargetId;
  let linkType = defaultLinkType;
  let ordering = null;
  let rankSets = [];
  const maxIterations = 25; // XXX setter/getter
  let nodePosition = null;
  let sortPorts = defaultSortPorts;

  // extent
  let x0 = 0;
  let y0 = 0;
  let x1 = 1;
  let y1 = 1;

  // node width
  let dx = 1;

  let scale = null;
  let linkValue = function (e) { return e.value };
  let whitespace = 0.5;
  let verticalLayout = positionNodesVertically$1();

  function sankey () {
    const graph = { nodes: nodes.apply(null, arguments), links: links.apply(null, arguments) };
    const G = buildGraph(graph, nodeId, nodeBackwards, sourceId, targetId, linkType, linkValue);

    setNodeValues(G, linkValue);

    if (nodePosition) {
      // hard-coded node positions

      G.nodes().forEach(u => {
        const node = G.node(u);
        const pos = nodePosition(node.data);
        node.x0 = pos[0];
        node.x1 = pos[0] + dx;
        node.y = pos[1];
      });
      setWidths(G, scale);
    } else {
      // calculate node positions

      if (ordering !== null) {
        applyOrdering(G, ordering);
      } else {
        assignRanks(G, rankSets);
        sortNodes(G, maxIterations);
      }

      addDummyNodes(G);
      setNodeValues(G, linkValue);
      if (ordering === null) {
        // XXX sort nodes?
        sortNodes(G, maxIterations);
      }

      const nested = nestGraph(G.nodes().map(u => G.node(u)));
      maybeScaleToFit(G, nested);
      setWidths(G, scale);

      // position nodes
      verticalLayout(nested, y1 - y0, whitespace);
      positionHorizontally(G, x1 - x0, dx);

      // adjust origin
      G.nodes().forEach(u => {
        const node = G.node(u);
        node.x0 += x0;
        node.x1 += x0;
        node.y += y0;
      });
    }

    // sort & position links
    prepareNodePorts(G, sortPorts);
    orderEdges(G);
    layoutLinks(G);

    removeDummyNodes(G);
    addLinkEndpoints(G);

    copyResultsToGraph(G);

    return graph
  }

  sankey.update = function (graph, doOrderLinks) {
    const G = buildGraph(graph, nodeId, nodeBackwards, sourceId, targetId, linkType, linkValue);
    setNodeValues(G, linkValue);
    const nested = nestGraph(G.nodes().map(u => G.node(u)));
    maybeScaleToFit(G, nested);
    setWidths(G, scale);

    prepareNodePorts(G, sortPorts);
    orderEdges(G);
    layoutLinks(G);

    // removeDummyNodes(G)
    addLinkEndpoints(G);

    copyResultsToGraph(G);

    return graph
  };
  //   if (scale === null) sankey.scaleToFit(graph)
  //   // set node and edge sizes
  //   setNodeValues(graph, linkValue, scale)
  //   if (doOrderLinks) {
  //     orderLinks(graph)
  //   }
  //   layoutLinks(graph)
  //   return graph
  // }

  sankey.nodes = function (x) {
    if (arguments.length) {
      nodes = required$2(x);
      return sankey
    }
    return nodes
  };

  sankey.links = function (x) {
    if (arguments.length) {
      links = required$2(x);
      return sankey
    }
    return links
  };

  sankey.nodeId = function (x) {
    if (arguments.length) {
      nodeId = required$2(x);
      return sankey
    }
    return nodeId
  };

  sankey.nodeBackwards = function (x) {
    if (arguments.length) {
      nodeBackwards = required$2(x);
      return sankey
    }
    return nodeBackwards
  };

  sankey.sourceId = function (x) {
    if (arguments.length) {
      sourceId = required$2(x);
      return sankey
    }
    return sourceId
  };

  sankey.targetId = function (x) {
    if (arguments.length) {
      targetId = required$2(x);
      return sankey
    }
    return targetId
  };

  sankey.linkType = function (x) {
    if (arguments.length) {
      linkType = required$2(x);
      return sankey
    }
    return linkType
  };

  sankey.sortPorts = function (x) {
    if (arguments.length) {
      sortPorts = required$2(x);
      return sankey
    }
    return sortPorts
  };

  // sankey.scaleToFit = function (graph) {
  function maybeScaleToFit (G, nested) {
    if (scale !== null) return
    const maxValue = d3Array.sum(nested.bandValues);
    if (maxValue <= 0) {
      scale = 1;
    } else {
      scale = (y1 - y0) / maxValue;
      if (whitespace !== 1) scale *= (1 - whitespace);
    }
  }

  sankey.ordering = function (x) {
    if (!arguments.length) return ordering
    ordering = x;
    return sankey
  };

  sankey.rankSets = function (x) {
    if (!arguments.length) return rankSets
    rankSets = x;
    return sankey
  };

  sankey.nodeWidth = function (x) {
    if (!arguments.length) return dx
    dx = x;
    return sankey
  };

  sankey.nodePosition = function (x) {
    if (!arguments.length) return nodePosition
    nodePosition = x;
    return sankey
  };

  sankey.size = function (x) {
    if (!arguments.length) return [x1 - x0, y1 - y0]
    x0 = y0 = 0;
    x1 = +x[0];
    y1 = +x[1];
    return sankey
  };

  sankey.extent = function (x) {
    if (!arguments.length) return [[x0, y0], [x1, y1]]
    x0 = +x[0][0];
    y0 = +x[0][1];
    x1 = +x[1][0];
    y1 = +x[1][1];
    return sankey
  };

  sankey.whitespace = function (x) {
    if (!arguments.length) return whitespace
    whitespace = x;
    return sankey
  };

  sankey.scale = function (x) {
    if (!arguments.length) return scale
    scale = x;
    return sankey
  };

  sankey.linkValue = function (x) {
    if (!arguments.length) return linkValue
    linkValue = x;
    return sankey
  };

  sankey.verticalLayout = function (x) {
    if (!arguments.length) return verticalLayout
    verticalLayout = required$2(x);
    return sankey
  };

  function applyOrdering (G, ordering) {
    ordering.forEach((x, i) => {
      x.forEach((u, j) => {
        if (u.forEach) {
          u.forEach((v, k) => {
            const d = G.node(v);
            if (d) {
              d.rank = i;
              d.band = j;
              d.depth = k;
            }
          });
        } else {
          const d = G.node(u);
          if (d) {
            d.rank = i;
            // d.band = 0
            d.depth = j;
          }
        }
      });
    });
  }

  return sankey
}

function setNodeValues (G, linkValue) {
  G.nodes().forEach(u => {
    const d = G.node(u);
    let incoming = d3Array.sum(G.inEdges(u), e => G.edge(e).value);
    let outgoing = d3Array.sum(G.outEdges(u), e => G.edge(e).value);
    incoming += d3Array.sum(d.fromElsewhere || [], link => linkValue(link));
    outgoing += d3Array.sum(d.toElsewhere || [], link => linkValue(link));
    d.value = Math.max(incoming, outgoing);
  });
}

function setWidths (G, scale) {
  G.edges().forEach(e => {
    const edge = G.edge(e);
    edge.dy = edge.value * scale;
  });
  G.nodes().forEach(u => {
    const node = G.node(u);
    node.dy = node.value * scale;

    // Initialise from/to elsewhere lists
    node.fromElsewhere = (node.fromElsewhere || []);
    node.toElsewhere = (node.toElsewhere || []);
    node.fromElsewhere.forEach(link => {
      link.dy = link.value * scale;
      link.source = { id: '__from_elsewhere_' + u };
      link.target = node.data;
    });
    node.toElsewhere.forEach(link => {
      link.dy = link.value * scale;
      link.source = node.data;
      link.target = { id: '__to_elsewhere_' + u };
    });
  });
}

function required$2 (f) {
  if (typeof f !== 'function') throw new Error()
  return f
}

function addLinkEndpoints (G) {
  G.edges().forEach(e => {
    const edge = G.edge(e);
    edge.points.unshift({ x: edge.x0, y: edge.y0, ro: edge.r0, d: edge.d0 });
    edge.points.push({ x: edge.x1, y: edge.y1, ri: edge.r1, d: edge.d1 });
  });

  G.nodes().forEach(u => {
    const node = G.node(u);
    node.fromElsewhere.forEach(link => {
      link.points = [{ x: link.x1, y: link.y1, ri: link.r1, d: link.d1, style: 'down-right' }];
    });
    node.toElsewhere.forEach(link => {
      link.points = [{ x: link.x0, y: link.y0, ri: link.r0, d: link.d0, style: 'right-down' }];
    });
  });
}

function copyResultsToGraph (G, graph) {
  G.nodes().forEach(u => {
    const node = G.node(u);

    // Build lists of edge data objects
    node.data.incoming = [];
    node.data.outgoing = [];
    node.data.ports = node.ports;
    node.data.ports.forEach(port => {
      port.incoming = [];
      port.outgoing = [];
    });

    node.data.dy = node.dy;
    node.data.x0 = node.x0;
    node.data.x1 = node.x1;
    node.data.y0 = node.y;
    node.data.y1 = node.y + node.dy;
    node.data.rank = node.rank;
    node.data.band = node.band;
    node.data.depth = node.depth;
    node.data.value = node.value;
    node.data.spaceAbove = node.spaceAbove;
    node.data.spaceBelow = node.spaceBelow;
  });

  G.edges().forEach(e => {
    const edge = G.edge(e);
    edge.data.source = G.node(e.v).data;
    edge.data.target = G.node(e.w).data;
    edge.data.sourcePort = edge.sourcePort;
    edge.data.targetPort = edge.targetPort;
    // console.log(edge)
    edge.data.source.outgoing.push(edge.data);
    edge.data.target.incoming.push(edge.data);
    if (edge.data.sourcePort) edge.data.sourcePort.outgoing.push(edge.data);
    if (edge.data.targetPort) edge.data.targetPort.incoming.push(edge.data);
    edge.data.value = edge.value;
    edge.data.type = edge.type;
    edge.data.dy = edge.dy;
    edge.data.points = edge.points || [];
    // edge.data.id = `${e.v}-${e.w}-${e.name}`
  });
}

function positionNodesVertically () {
  let iterations = 25;
  let nodePadding = 8;

  function layout (nested, height) {
    initializeNodeDepth();
    resolveCollisions();
    for (let alpha = 1, i = iterations; i > 0; --i) {
      relaxRightToLeft(alpha *= 0.99);
      resolveCollisions();
      relaxLeftToRight(alpha);
      resolveCollisions();
    }

    function initializeNodeDepth () {
      nested.forEach(layer => {
        let i = 0;
        layer.forEach(band => {
          // ignore bands for this layout
          band.forEach(node => {
            node.y = i++;
          });
        });
      });
    }

    function relaxLeftToRight (alpha) {
      nested.forEach(layer => {
        layer.forEach(band => {
          band.forEach(node => {
            const edges = node.incoming || node.edges;
            if (edges.length) {
              const y = d3Array.sum(edges, weightedSource) / d3Array.sum(edges, value);
              node.y += (y - center(node)) * alpha;
            }
          });
        });
      });

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

    function relaxRightToLeft (alpha) {
      nested.slice().reverse().forEach(layer => {
        layer.forEach(band => {
          band.forEach(node => {
            const edges = node.outgoing || node.edges;
            if (edges.length) {
              const y = d3Array.sum(edges, weightedTarget) / d3Array.sum(edges, value);
              node.y += (y - center(node)) * alpha;
            }
          });
        });
      });

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

    function resolveCollisions () {
      nested.forEach(layer => {
        layer.forEach(nodes => {
          let node;
          let dy;
          let y0 = 0;
          const n = nodes.length;
          let 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 - height;
          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;
            }
          }
        });
      });
    }
  }

  layout.iterations = function (x) {
    if (!arguments.length) return iterations
    iterations = +x;
    return layout
  };

  layout.padding = function (x) {
    if (!arguments.length) return nodePadding
    nodePadding = +x;
    return layout
  };

  return layout
}

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

function value (link) {
  return link.value
}

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

// function defaultSegments (d) {
//   return d.segments
// }

function defaultMinWidth (d) {
  return (d.dy === 0) ? 0 : 2
}

function sankeyLink () {
  // var segments = defaultSegments
  let minWidth = defaultMinWidth;

  function radiusBounds (d) {
    const Dx = d.x1 - d.x0;
    const Dy = d.y1 - d.y0;
    const Rmin = d.dy / 2;
    const Rmax = (Dx * Dx + Dy * Dy) / Math.abs(4 * Dy);
    return [Rmin, Rmax]
  }

  function link (d) {
    if (d.points.length === 1) {
      return toOrFromElsewherePath(d)
    }

    let path = '';
    let seg;
    for (let i = 0; i < d.points.length - 1; ++i) {
      seg = {
        x0: d.points[i].x,
        y0: d.points[i].y,
        x1: d.points[i + 1].x,
        y1: d.points[i + 1].y,
        r0: d.points[i].ro,
        r1: d.points[i + 1].ri,
        d0: d.points[i].d,
        d1: d.points[i + 1].d,
        dy: d.dy
      };
      path += segmentPath(seg);
    }
    return path
  }

  function segmentPath (d) {
    const dir = (d.d0 || 'r') + (d.d1 || 'r');
    if (d.source && d.source === d.target) {
      return selfLink(d)
    }
    if (dir === 'rl') {
      return fbLink(d)
    }
    if (dir === 'rd') {
      return fdLink(d)
    }
    if (dir === 'dr') {
      return dfLink(d)
    }
    if (dir === 'lr') {
      return bfLink(d)
    }

    // Minimum thickness 2px
    const h = Math.max(minWidth(d), d.dy) / 2;
    let x0 = d.x0;
    let x1 = d.x1;
    let y0 = d.y0;
    let y1 = d.y1;

    if (x1 < x0) {
      [x0, x1] = [x1, x0];
      [y0, y1] = [y1, y0];
    }

    const f = y1 > y0 ? 1 : -1;
    const fx = 1; // dir === 'll' ? -1 : 1;

    const Rlim = radiusBounds(d);
    const defaultRadius = Math.max(Rlim[0], Math.min(Rlim[1], (x1 - x0) / 3));

    let r0 = Math.max(Rlim[0], Math.min(Rlim[1], (d.r0 || defaultRadius)));
    let r1 = Math.max(Rlim[0], Math.min(Rlim[1], (d.r1 || defaultRadius)));

    const dcx = (x1 - x0);
    const dcy = (y1 - y0) - f * (r0 + r1);
    const D = Math.sqrt(dcx * dcx + dcy * dcy);

    const phi = -f * Math.acos(Math.min(1, (r0 + r1) / D));
    const psi = Math.atan2(dcy, dcx);

    let theta = Math.PI / 2 + f * (psi + phi);

    let hs = h * f * Math.sin(theta);
    let hc = h * Math.cos(theta);
    let x2 = x0 + fx * r0 * Math.sin(Math.abs(theta));
    let x3 = x1 - fx * r1 * Math.sin(Math.abs(theta));
    let y2 = y0 + r0 * f * (1 - Math.cos(theta));
    let y3 = y1 - r1 * f * (1 - Math.cos(theta));

    if (isNaN(theta) || Math.abs(theta) < 1e-3) {
      theta = r0 = r1 = 0;
      x2 = x0;
      x3 = x1;
      y2 = y0;
      y3 = y1;
      hs = 0;
      hc = h;
    }

    function arc (dir, r) {
      const f = (dir * (y1 - y0) > 0) ? 1 : 0;
      let rr = (fx * dir * (y1 - y0) > 0) ? (r + h) : (r - h);
      // straight line
      if (theta === 0) { rr = r; }
      return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
    }

    // if (fx * (x2 - x3) < 0 || Math.abs(y1 - y0) > 4*h) {
    // XXX this causes juddering during transitions

    const path = (
      'M' + [x0, y0 - h] + ' ' +
      arc(+1, r0) + [x2 + hs, y2 - hc] + ' ' +
      'L' + [x3 + hs, y3 - hc] + ' ' +
      arc(-1, r1) + [x1, y1 - h] + ' ' +
      'L' + [x1, y1 + h] + ' ' +
      arc(+1, r1) + [x3 - hs, y3 + hc] + ' ' +
      'L' + [x2 - hs, y2 + hc] + ' ' +
      arc(-1, r0) + [x0, y0 + h] + ' ' +
      'Z'
    );

    if (/NaN/.test(path)) {
      console.error('path NaN', d, path);
    }
    return path
  }

  function selfLink (d) {
    const h = Math.max(minWidth(d), d.dy) / 2;
    const r = h * 1.5;
    const theta = 2 * Math.PI;
    const x0 = d.x0;
    const y0 = d.y0;

    function arc (dir) {
      const f = (dir > 0) ? 1 : 0;
      const rr = (dir > 0) ? (r + h) : (r - h);
      return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 1 ' + f + ' '
    }

    return ('M' + [x0 + 0.1, y0 - h] + ' ' +
            arc(+1) + [x0 - 0.1, y0 - h] + ' ' +
            'L' + [x0 - 0.1, y0 + h] + ' ' +
            arc(-1) + [x0 + 0.1, y0 + h] + ' ' +
            'Z')
  }

  function fbLink (d) {
    // Minimum thickness 2px
    const h = Math.max(minWidth(d), d.dy) / 2;
    const x0 = d.x0;
    const x1 = d.x1;
    const y0 = d.y0;
    const y1 = d.y1;
    const Dx = d.x1 - d.x0;
    const Dy = d.y1 - d.y0;
    // Rlim = radiusBounds(d),
    const defaultRadius = ((d.r0 + d.r1) / 2) || (5 + h); // Math.max(Rlim[0], Math.min(Rlim[1], Dx/3)),
    const r = Math.min(Math.abs(y1 - y0) / 2.1, defaultRadius); // 2*(d.r || defaultRadius),
    const theta = Math.atan2(Dy - 2 * r, Dx);
    const f = d.y1 > d.y0 ? 1 : -1;
    const hs = h * Math.sin(theta);
    const hc = h * Math.cos(theta);
    const x2 = d.x0 + r * Math.sin(Math.abs(theta));
    const x3 = d.x1 + r * Math.sin(Math.abs(theta));
    const y2 = d.y0 + r * f * (1 - Math.cos(theta));
    const y3 = d.y1 - r * f * (1 - Math.cos(theta));

    function arc (dir) {
      const f = (dir * theta > 0) ? 1 : 0;
      let rr = (dir * theta > 0) ? (r + h) : (r - h);
      // straight line
      if (theta === 0) { rr = r; }
      return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
    }

    return ('M' + [x0, y0 - h] + ' ' +
            arc(+1) + [x2 + hs, y2 - hc] + ' ' +
            'L' + [x3 + hs, y3 - hc] + ' ' +
            arc(+1) + [x1, y1 + h] + ' ' +
            'L' + [x1, y1 - h] + ' ' +
            arc(-1) + [x3 - hs, y3 + hc] + ' ' +
            'L' + [x2 - hs, y2 + hc] + ' ' +
            arc(-1) + [x0, y0 + h] + ' ' +
            'Z')
  }

  function fdLink (d) {
    // Minimum thickness 2px
    const h = Math.max(minWidth(d), d.dy) / 2;
    const x0 = d.x0;
    const x1 = d.x1;
    const y0 = d.y0;
    const y1 = d.y1;
    const theta = Math.PI / 2;
    const r = Math.max(0, x1 - x0);
    const y2 = y0 + r;

    function arc (dir) {
      const f = (dir * theta > 0) ? 1 : 0;
      let rr = (dir * theta > 0) ? (r + h) : (r - h);
      // straight line
      if (theta === 0) { rr = r; }
      return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
    }

    return ('M' + [x0, y0 - h] + ' ' +
            arc(+1) + [x1 + h, y2] + ' ' +
            'L' + [x1 + h, y1] + ' ' +
            '' + [x1 - h, y1] + ' ' +
            '' + [x1 - h, y2] + ' ' +
            arc(-1) + [x0, y0 + h] + ' ' +
            'Z')
  }

  function dfLink (d) {
    // Minimum thickness 2px
    const h = Math.max(minWidth(d), d.dy) / 2;
    const x0 = d.x0;
    const x1 = d.x1;
    const y0 = d.y0;
    const y1 = d.y1;
    const theta = Math.PI / 2;
    const r = Math.max(0, x1 - x0);
    const y2 = y1 - r;

    function arc (dir) {
      const f = (dir * theta > 0) ? 1 : 0;
      let rr = (dir * theta > 0) ? (r + h) : (r - h);
      // straight line
      if (theta === 0) { rr = r; }
      return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
    }

    return ('M' + [x0 - h, y0] + ' ' +
            'L' + [x0 + h, y0] + ' ' +
            '' + [x0 + h, y2] + ' ' +
            arc(-1) + [x1, y1 - h] + ' ' +
            'L' + [x1, y1 + h] + ' ' +
            arc(+1) + [x0 - h, y2] + ' ' +
            'Z')
  }

  function bfLink (d) {
    // Minimum thickness 2px
    const h = Math.max(minWidth(d), d.dy) / 2;
    const x0 = d.x0;
    const x1 = d.x1;
    const y0 = d.y0;
    const y1 = d.y1;
    const Dx = d.x1 - d.x0;
    const Dy = d.y1 - d.y0;
    // Rlim = radiusBounds(d),
    const defaultRadius = ((d.r0 + d.r1) / 2) || (5 + h); // Math.max(Rlim[0], Math.min(Rlim[1], Dx/3)),
    const r = Math.min(Math.abs(Dy) / 2.1, defaultRadius); // 2*(d.r || defaultRadius),
    const theta = Math.atan2(Dy - 2 * r, Dx);
    // const l = Math.sqrt(Math.max(0, Dx * Dx + (Dy - 2 * r) * (Dy - 2 * r)))
    const f = d.y1 > d.y0 ? 1 : -1;
    const hs = h * Math.sin(theta);
    const hc = h * Math.cos(theta);
    const x2 = d.x0 - r * Math.sin(Math.abs(theta));
    const x3 = d.x1 - r * Math.sin(Math.abs(theta));
    const y2 = d.y0 + r * f * (1 - Math.cos(theta));
    const y3 = d.y1 - r * f * (1 - Math.cos(theta));

    function arc (dir) {
      const f = (dir * theta > 0) ? 1 : 0;
      let rr = (-dir * theta > 0) ? (r + h) : (r - h);
      // straight line
      if (theta === 0) { rr = r; }
      return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
    }

    return ('M' + [x0, y0 - h] + ' ' +
            arc(-1) + [x2 - hs, y2 - hc] + ' ' +
            'L' + [x3 - hs, y3 - hc] + ' ' +
            arc(-1) + [x1, y1 + h] + ' ' +
            'L' + [x1, y1 - h] + ' ' +
            arc(+1) + [x3 + hs, y3 - hc] + ' ' +
            'L' + [x2 + hs, y2 - hc] + ' ' +
            arc(+1) + [x0, y0 + h] + ' ' +
            'Z')
  }

  function toOrFromElsewherePath (d) {
    const p = d.points[0];
    const h = Math.max(minWidth(d), d.dy) / 2;

    // XXX draw these properly with curves and appropriate radii

    if (p.style === 'down-right') {
      return ('M' + [p.x - 20, p.y - h] + ' ' +
              'L' + [p.x, p.y - h] + ' ' +
              'L' + [p.x, p.y + h] + ' ' +
              'L' + [p.x - 20, p.y + h] + ' ' +
              'Z')
    }
    if (p.style === 'right-down') {
      return ('M' + [p.x, p.y - h] + ' ' +
              'L' + [p.x + 20, p.y - h] + ' ' +
              'L' + [p.x + 20, p.y + h] + ' ' +
              'L' + [p.x, p.y + h] + ' ' +
              'Z')
    }
  }

  link.minWidth = function (x) {
    if (arguments.length) {
      minWidth = required$1(x);
      return link
    }
    return minWidth
  };

  return link
}

function required$1 (f) {
  if (typeof f !== 'function') throw new Error()
  return f
}

function sankeyNode () {
  let nodeTitle = (d) => d.title !== undefined ? d.title : d.id;
  let nodeValue = (d) => null;
  let nodeVisible = (d) => !!nodeTitle(d);

  function sankeyNode (context) {
    const selection = context.selection ? context.selection() : context;

    if (selection.select('text').empty()) {
      selection.append('title');
      selection.append('line')
        .attr('x1', 0)
        .attr('x2', 0);
      selection.append('rect')
        .attr('class', 'node-body');
      selection.append('text')
        .attr('class', 'node-value')
        .attr('dy', '.35em')
        .attr('text-anchor', 'middle');
      selection.append('text')
        .attr('class', 'node-title')
        .attr('dy', '.35em');
      selection.append('rect')
        .attr('class', 'node-click-target')
        .attr('x', -5)
        .attr('y', -5)
        .attr('width', 10)
        .style('fill', 'none')
        .style('visibility', 'hidden')
        .style('pointer-events', 'all');

      selection
        .attr('transform', nodeTransform);
    }

    selection.each(function (d) {
      const title = d3Selection.select(this).select('title');
      const value = d3Selection.select(this).select('.node-value');
      let text = d3Selection.select(this).select('.node-title');
      let line = d3Selection.select(this).select('line');
      let body = d3Selection.select(this).select('.node-body');
      let clickTarget = d3Selection.select(this).select('.node-click-target');

      // Local var for title position of each node
      const layoutData = titlePosition(d);
      layoutData.dy = (d.y0 === d.y1) ? 0 : Math.max(1, d.y1 - d.y0);

      const separateValue = (d.x1 - d.x0) > 2;
      const titleText = nodeTitle(d) + ((!separateValue && nodeValue(d))
        ? ' (' + nodeValue(d) + ')'
        : '');

      // Update un-transitioned
      title
        .text(titleText);

      value
        .text(nodeValue)
        .style('display', separateValue ? 'inline' : 'none');

      text
        .attr('text-anchor', layoutData.right ? 'end' : 'start')
        .text(titleText)
        .each(wrap, 100);

      // Are we in a transition?
      if (context !== selection) {
        text = text.transition(context);
        line = line.transition(context);
        body = body.transition(context);
        clickTarget = clickTarget.transition(context);
      }

      // Update
      context
        .attr('transform', nodeTransform);

      line
        .attr('y1', function (d) { return layoutData.titleAbove ? -5 : 0 })
        .attr('y2', function (d) { return layoutData.dy })
        .style('display', function (d) {
          return (d.y0 === d.y1 || !nodeVisible(d)) ? 'none' : 'inline'
        });

      clickTarget
        .attr('height', function (d) { return layoutData.dy + 5 });

      body
        .attr('width', function (d) { return d.x1 - d.x0 })
        .attr('height', function (d) { return layoutData.dy });

      text
        .attr('transform', textTransform)
        .style('display', function (d) {
          return (d.y0 === d.y1 || !nodeVisible(d)) ? 'none' : 'inline'
        });

      value
        .style('font-size', function (d) { return Math.min(d.x1 - d.x0 - 4, d.y1 - d.y0 - 4) + 'px' })
        .attr('transform', function (d) {
          const dx = d.x1 - d.x0;
          const dy = d.y1 - d.y0;
          const theta = dx > dy ? 0 : -90;
          return 'translate(' + (dx / 2) + ',' + (dy / 2) + ') rotate(' + theta + ')'
        });

      function textTransform (d) {
        const layout = layoutData;
        const y = layout.titleAbove ? -10 : (d.y1 - d.y0) / 2;
        let x;
        if (layout.titleAbove) {
          x = (layout.right ? 4 : -4);
        } else {
          x = (layout.right ? -4 : d.x1 - d.x0 + 4);
        }
        return 'translate(' + x + ',' + y + ')'
      }
    });
  }

  sankeyNode.nodeVisible = function (x) {
    if (arguments.length) {
      nodeVisible = required(x);
      return sankeyNode
    }
    return nodeVisible
  };

  sankeyNode.nodeTitle = function (x) {
    if (arguments.length) {
      nodeTitle = required(x);
      return sankeyNode
    }
    return nodeTitle
  };

  sankeyNode.nodeValue = function (x) {
    if (arguments.length) {
      nodeValue = required(x);
      return sankeyNode
    }
    return nodeValue
  };

  return sankeyNode
}

function nodeTransform (d) {
  return 'translate(' + d.x0 + ',' + d.y0 + ')'
}

function titlePosition (d) {
  let titleAbove = false;
  let right = false;

  // If thin, and there's enough space, put above
  if (d.spaceAbove > 20 && d.style !== 'type') {
    titleAbove = true;
  } else {
    titleAbove = false;
  }

  if (d.incoming.length === 0) {
    right = true;
    titleAbove = false;
  } else if (d.outgoing.length === 0) {
    right = false;
    titleAbove = false;
  }

  return { titleAbove, right }
}

function wrap (d, width) {
  const text = d3Selection.select(this);
  const lines = text.text().split(/\n/);
  const lineHeight = 1.1; // ems
  if (lines.length === 1) return
  text.text(null);
  lines.forEach(function (line, i) {
    text.append('tspan')
      .attr('x', 0)
      .attr('dy', (i === 0 ? 0.7 - lines.length / 2 : 1) * lineHeight + 'em')
      .text(line);
  });
}

function required (f) {
  if (typeof f !== 'function') throw new Error()
  return f
}

function positionGroup (nodes, group) {
  const rect = {
    top: Number.MAX_VALUE,
    left: Number.MAX_VALUE,
    bottom: 0,
    right: 0
  };

  group.nodes.forEach(n => {
    const node = nodes.get(n);
    if (!node) return
    if (node.x0 < rect.left) rect.left = node.x0;
    if (node.x1 > rect.right) rect.right = node.x1;
    if (node.y0 < rect.top) rect.top = node.y0;
    if (node.y1 > rect.bottom) rect.bottom = node.y1;
  });

  group.rect = rect;
  return group
}

// The reusable SVG component for the sliced Sankey diagram


function linkTitleGenerator (nodeTitle, typeTitle, fmt) {
  return function (d) {
    const parts = [];
    const sourceTitle = nodeTitle(d.source);
    const targetTitle = nodeTitle(d.target);
    const matTitle = typeTitle(d);

    parts.push(`${sourceTitle} → ${targetTitle}`);
    if (matTitle) parts.push(matTitle);
    parts.push(fmt(d.value));
    return parts.join('\n')
  }
}

function sankeyDiagram () {
  let margin = { top: 0, right: 0, bottom: 0, left: 0 };
  let selectedEdge = null;

  let groups = [];

  const fmt = d3Format.format('.3s');
  const node = sankeyNode();
  const link = sankeyLink();

  let linkColor = d => null;
  let linkTitle = linkTitleGenerator(node.nodeTitle(), d => d.type, fmt);
  let linkLabel = defaultLinkLabel;
  let linkImportance = defaultLinkImportance;
  let linkImportanceAgg = defaultLinkImportanceAgg;

  const listeners = d3Dispatch.dispatch('selectNode', 'selectGroup', 'selectLink');

  /* Main chart */

  function exports (context) {
    const selection = context.selection ? context.selection() : context;

    selection.each(function (G) {
      // Create the skeleton, if it doesn't already exist
      const svg = d3Selection.select(this);

      let sankey = svg.selectAll('.sankey')
        .data([{ type: 'sankey' }]);

      const sankeyEnter = sankey.enter()
        .append('g')
        .classed('sankey', true);

      sankeyEnter.append('g').classed('groups', true);
      sankeyEnter.append('g').classed('links', true); // Links below nodes
      sankeyEnter.append('g').classed('nodes', true);
      sankeyEnter.append('g').classed('slice-titles', true); // Slice titles

      sankey = sankey.merge(sankeyEnter);

      // Update margins
      sankey
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
        // .select('.slice-titles')
        // .attr('transform', 'translate(' + margin.left + ',0)')

      // Groups of nodes
      const nodeMap = d3Collection.map(G.nodes, n => n.id);
      const groupsPositioned = (groups || []).map(g => positionGroup(nodeMap, g));

      // All links -- including "from elsewhere" and "to elsewhere" ones
      const links = Array.from(G.links);
      G.nodes.forEach(node => {
        Array.prototype.push.apply(links, node.fromElsewhere || []);
        Array.prototype.push.apply(links, node.toElsewhere || []);
      });

      // Render
      updateNodes(sankey, context, G.nodes);
      updateLinks(sankey, context, links);
      updateGroups(svg, groupsPositioned);
      // updateSlices(svg, layout.slices(nodes));

      // Events
      svg.on('click', function () {
        listeners.call('selectNode', this, null);
        listeners.call('selectLink', this, null);
      });
    });
  }

  function updateNodes (sankey, context, nodes) {
    let nodeSel = sankey
      .select('.nodes')
      .selectAll('.node')
      .data(nodes, d => d.id);

    // EXIT
    nodeSel.exit().remove();

    nodeSel = nodeSel.merge(
      nodeSel.enter()
        .append('g')
        .attr('class', 'node')
        .call(node)
        .on('click', selectNode));

    if (context instanceof d3Transition.transition) {
      nodeSel.transition(context)
        .call(node);
    } else {
      nodeSel.call(node);
    }
  }

  function updateLinks (sankey, context, edges) {
    let linkSel = sankey
      .select('.links')
      .selectAll('.link')
      .data(edges, d => d.source.id + '-' + d.target.id + '-' + d.type);

    // EXIT

    linkSel.exit().remove();

    // ENTER

    const linkEnter = linkSel.enter()
      .append('g')
      .attr('class', 'link')
      .on('click', selectLink);

    linkEnter.append('path')
      .attr('d', link)
      .style('fill', 'white')
      .each(function (d) { this._current = d; });

    linkEnter.append('title');

    linkEnter.append('text')
      .attr('class', 'label')
      .attr('dy', '0.35em')
      .attr('x', d => d.points[0].x + 4)
      .attr('y', d => d.points[0].y);

    // UPDATE

    linkSel = linkSel.merge(linkEnter);

    // Calculate group importance for sorting
    calculateGroupImportance(edges);

    // Non-transition updates
    linkSel.classed('selected', (d) => d.id === selectedEdge);
    linkSel.sort(linkOrder);

    // Transition updates, if available
    if (context instanceof d3Transition.transition) {
      linkSel = linkSel.transition(context);
      linkSel
        .select('path')
        .style('fill', linkColor)
        .each(function (d) {
          d3Selection.select(this)
            .transition(context)
            .attrTween('d', interpolateLink);
        });
    } else {
      linkSel
        .select('path')
        .style('fill', linkColor)
        .attr('d', link);
    }

    linkSel.select('title')
      .text(linkTitle);

    linkSel.select('.label')
      .text(linkLabel)
      .attr('x', d => d.points[0].x + 4)
      .attr('y', d => d.points[0].y);
  }

  function calculateGroupImportance (edges) {
    // Group links by source-target pair
    const groups = new Map();

    // Calculate individual importance for each link
    edges.forEach(edge => {
      const groupKey = edge.source.id + '-' + edge.target.id;
      if (!groups.has(groupKey)) {
        groups.set(groupKey, []);
      }
      const importance = linkImportance(edge);
      edge._importance = importance;
      groups.get(groupKey).push({ edge, importance });
    });

    // Calculate aggregated importance for each group and assign to all links in group
    groups.forEach(linkGroup => {
      const importanceValues = linkGroup.map(item => item.importance);
      const groupImportance = linkImportanceAgg(importanceValues);
      linkGroup.forEach(item => {
        item.edge._groupImportance = groupImportance;
      });
    });
  }

  // function updateSlices(svg, slices) {
  //   var slice = svg.select('.slice-titles').selectAll('.slice')
  //         .data(slices, function(d) { return d.id; });

  //   var textWidth = (slices.length > 1 ?
  //                    0.9 * (slices[1].x - slices[0].x) :
  //                    null);

  //   slice.enter().append('g')
  //     .attr('class', 'slice')
  //     .append('foreignObject')
  //     .attr('requiredFeatures',
  //           'http://www.w3.org/TR/SVG11/feature#Extensibility')
  //     .attr('height', margin.top)
  //     .attr('class', 'title')
  //     .append('xhtml:div')
  //     .style('text-align', 'center')
  //     .style('word-wrap', 'break-word');
  //   // .text(pprop('sliceMetadata', 'title'));

  //   slice
  //     .attr('transform', function(d) {
  //       return 'translate(' + (d.x - textWidth / 2) + ',0)'; })
  //     .select('foreignObject')
  //     .attr('width', textWidth)
  //     .select('div');
  //   // .text(pprop('sliceMetadata', 'title'));

  //   slice.exit().remove();
  // }

  function updateGroups (svg, groups) {
    let group = svg.select('.groups').selectAll('.group')
      .data(groups);

    // EXIT
    group.exit().remove();

    // ENTER
    const enter = group.enter().append('g')
      .attr('class', 'group');
    // .on('click', selectGroup);

    enter.append('rect');
    enter.append('text')
      .attr('x', -10)
      .attr('y', -25);

    group = group.merge(enter);

    group
      .style('display', d => d.title ? 'inline' : 'none')
      .attr('transform', d => `translate(${d.rect.left},${d.rect.top})`)
      .select('rect')
      .attr('x', -10)
      .attr('y', -20)
      .attr('width', d => d.rect.right - d.rect.left + 20)
      .attr('height', d => d.rect.bottom - d.rect.top + 30);

    group.select('text')
      .text(d => d.title);
  }

  function interpolateLink (b) {
    // XXX should limit radius better
    b.points.forEach(function (p) {
      if (p.ri > 1e3) p.ri = 1e3;
      if (p.ro > 1e3) p.ro = 1e3;
    });
    const interp = d3Interpolate.interpolate(linkGeom(this._current), b);
    const that = this;
    return function (t) {
      that._current = interp(t);
      return link(that._current)
    }
  }

  function linkGeom (l) {
    return {
      points: l.points,
      dy: l.dy
    }
  }

  function linkOrder (a, b) {
    // Selected links always on top
    if (a.id === selectedEdge) return +1
    if (b.id === selectedEdge) return -1
    // All other sorting based on group importance
    return a._groupImportance - b._groupImportance
  }

  function selectLink (d) {
    d3Selection.event.stopPropagation();
    const el = d3Selection.select(this).node();
    listeners.call('selectLink', el, d);
  }

  function selectNode (d) {
    d3Selection.event.stopPropagation();
    const el = d3Selection.select(this).node();
    listeners.call('selectNode', el, d);
  }

  // function selectGroup(d) {
  //   d3.event.stopPropagation();
  //   var el = d3.select(this)[0][0];
  //   dispatch.selectGroup.call(el, d);
  // }

  exports.margins = function (_x) {
    if (!arguments.length) return margin
    margin = {
      top: _x.top === undefined ? margin.top : _x.top,
      left: _x.left === undefined ? margin.left : _x.left,
      bottom: _x.bottom === undefined ? margin.bottom : _x.bottom,
      right: _x.right === undefined ? margin.right : _x.right
    };
    return this
  };

  exports.groups = function (_x) {
    if (!arguments.length) return groups
    groups = _x;
    return this
  };

  // Node styles and title
  exports.nodeTitle = function (_x) {
    if (!arguments.length) return node.nodeTitle()
    node.nodeTitle(_x);
    linkTitle = linkTitleGenerator(_x, d => d.type, fmt);
    return this
  };

  exports.nodeValue = function (_x) {
    if (!arguments.length) return node.nodeValue()
    node.nodeValue(_x);
    return this
  };

  // Link styles and titles
  exports.linkTitle = function (_x) {
    if (!arguments.length) return linkTitle
    linkTitle = _x;
    return this
  };

  exports.linkLabel = function (_x) {
    if (!arguments.length) return linkLabel
    linkLabel = _x;
    return this
  };

  exports.linkColor = function (_x) {
    if (!arguments.length) return linkColor
    linkColor = _x;
    return this
  };

  exports.linkMinWidth = function (_x) {
    if (!arguments.length) return link.minWidth()
    link.minWidth(_x);
    return this
  };

  exports.linkImportance = function (_x) {
    if (!arguments.length) return linkImportance
    linkImportance = _x;
    return this
  };

  exports.linkImportanceAgg = function (_x) {
    if (!arguments.length) return linkImportanceAgg
    linkImportanceAgg = _x;
    return this
  };

  exports.selectNode = function (_x) {
    return this
  };

  exports.selectLink = function (_x) {
    selectedEdge = _x;
    return this
  };

  exports.on = function () {
    const value = listeners.on.apply(listeners, arguments);
    return value === listeners ? exports : value
  };

  return exports
}

function defaultLinkLabel (d) {
  return null
}

function defaultLinkImportance (d) {
  // Return negative values for special cases to put them at the bottom
  if (!d.source || (d.target && d.target.direction === 'd')) return -2
  if (!d.target || (d.source && d.source.direction === 'd')) return -1
  // Return dy (visual width) for normal links
  return d.dy
}

function defaultLinkImportanceAgg (values) {
  // Sum of importance values for all links in the group
  return values.reduce((a, b) => a + b, 0)
}

exports.sankey = sankeyLayout;
exports.sankeyDiagram = sankeyDiagram;
exports.sankeyLink = sankeyLink;
exports.sankeyLinkTitle = linkTitleGenerator;
exports.sankeyNode = sankeyNode;
exports.sankeyPositionJustified = positionNodesVertically$1;
exports.sankeyPositionRelaxation = positionNodesVertically;