Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active January 13, 2024 20:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tomshanley/87c05949f0b1994bfa71eccbf1f30d09 to your computer and use it in GitHub Desktop.
Save tomshanley/87c05949f0b1994bfa71eccbf1f30d09 to your computer and use it in GitHub Desktop.
Sankey in the style of 1960s river freight chart
license: mit
height: 800
border: yes

Further iteration of the Sankey with circular links, this time with arrows to indicate direction.

The arrows are made by appending another path on top of the link part, and which is styled using a stroke-dasharray, with triangles appended that rotated based on two close points on the path.

The Sankey has further improvements to reduce collisions of links that span more than one depth and any nodes.

Also, I've changes how the circular paths are calculates in terms of their vertical height/depth, so they use less space.

Update 4/9/2017: Fixed the function that sorted nodes by breadth.

Built with blockbuilder.org

// https://github.com/d3/d3-sankey Version 0.7.1. Copyright 2017 Mike Bostock.
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(
exports,
require('d3-array'),
require('d3-collection'),
require('d3-shape')
)
: typeof define === 'function' && define.amd
? define(['exports', 'd3-array', 'd3-collection', 'd3-shape'], factory)
: factory(
(global.d3 = global.d3 || {}),
global.d3,
global.d3,
global.d3
)
})(this, function (exports, d3Array, d3Collection, d3Shape) {
'use strict'
function targetDepth (d) {
return d.target.depth
}
function left (node) {
return node.depth
}
function right (node, n) {
return n - 1 - node.height
}
function justify (node, n) {
return node.sourceLinks.length ? node.depth : n - 1
}
function center (node) {
return node.targetLinks.length
? node.depth
: node.sourceLinks.length
? d3Array.min(node.sourceLinks, targetDepth) - 1
: 0
}
function constant (x) {
return function () {
return x
}
}
function ascendingSourceBreadth (a, b) {
return ascendingBreadth(a.source, b.source) || a.index - b.index
}
function ascendingTargetBreadth (a, b) {
return ascendingBreadth(a.target, b.target) || a.index - b.index
}
function ascendingBreadth (a, b) {
if (a.partOfCycle === b.partOfCycle) {
return a.y0 - b.y0
} else {
if (a.circularLinkType === 'top' || b.circularLinkType === 'bottom') {
return -1
} else {
return 1
}
}
}
function value (d) {
return d.value
}
function nodeCenter (node) {
return (node.y0 + node.y1) / 2
}
function linkSourceCenter (link) {
return nodeCenter(link.source)
}
function linkTargetCenter (link) {
return nodeCenter(link.target)
}
function weightedSource (link) {
return nodeCenter(link.source) * link.value
}
function weightedTarget (link) {
return nodeCenter(link.target) * link.value
}
function defaultId (d) {
return d.index
}
function defaultNodes (graph) {
return graph.nodes
}
function defaultLinks (graph) {
return graph.links
}
function find (nodeById, id) {
var node = nodeById.get(id)
if (!node) throw new Error('missing: ' + id)
return node
}
var sankey = function () {
var x0 = 0,
y0 = 0,
x1 = 1,
y1 = 1, // extent
dx = 24, // nodeWidth
py, // nodePadding
scale = 1,
id = defaultId,
align = justify,
nodes = defaultNodes,
links = defaultLinks,
iterations = 32
var padding = Infinity
var paddingRatio = 0.1
function sankey () {
var graph = {
nodes: nodes.apply(null, arguments),
links: links.apply(null, arguments)
}
computeNodeLinks(graph)
identifyCircles(graph)
selectCircularLinkTypes(graph)
computeNodeValues(graph)
computeNodeDepths(graph)
computeNodeBreadths(graph, iterations)
computeLinkBreadths(graph)
//sort links per node, based on the links' source/target positions
sortSourceLinks(graph)
sortTargetLinks(graph)
//adjust nodes that overlap links that span 2+ depths
resolveNodeLinkOverlaps(graph)
//sort links per node, based on the links' source/target positions
sortSourceLinks(graph)
sortTargetLinks(graph)
//add d string for circular paths
addCircularPathData(graph);
return graph
}
sankey.update = function (graph) {
computeLinkBreadths(graph)
return graph
}
sankey.nodeId = function (_) {
return arguments.length
? ((id = typeof _ === 'function' ? _ : constant(_)), sankey)
: id
}
sankey.nodeAlign = function (_) {
return arguments.length
? ((align = typeof _ === 'function' ? _ : constant(_)), sankey)
: align
}
sankey.nodeWidth = function (_) {
return arguments.length ? ((dx = +_), sankey) : dx
}
sankey.nodePadding = function (_) {
return arguments.length ? ((py = +_), sankey) : py
}
sankey.scale = function (_) {
return arguments.length ? ((scale = +_), sankey) : scale
}
sankey.nodes = function (_) {
return arguments.length
? ((nodes = typeof _ === 'function' ? _ : constant(_)), sankey)
: nodes
}
sankey.links = function (_) {
return arguments.length
? ((links = typeof _ === 'function' ? _ : constant(_)), sankey)
: links
}
sankey.size = function (_) {
return arguments.length
? ((x0 = y0 = 0), (x1 = +_[0]), (y1 = +_[1]), sankey)
: [x1 - x0, y1 - y0]
}
sankey.extent = function (_) {
return arguments.length
? ((x0 = +_[0][0]), (x1 = +_[1][0]), (y0 = +_[0][1]), (y1 = +_[1][
1
]), sankey)
: [[x0, y0], [x1, y1]]
}
sankey.iterations = function (_) {
return arguments.length ? ((iterations = +_), sankey) : iterations
}
sankey.nodePaddingRatio = function (_) {
return arguments.length ? ((paddingRatio = +_), sankey) : paddingRatio
}
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks (graph) {
graph.nodes.forEach(function (node, i) {
node.index = i
node.sourceLinks = []
node.targetLinks = []
})
var nodeById = d3Collection.map(graph.nodes, id)
graph.links.forEach(function (link, i) {
link.index = i
var source = link.source
var target = link.target
if (typeof source !== 'object') {
source = link.source = find(nodeById, source)
}
if (typeof target !== 'object') {
target = link.target = find(nodeById, target)
}
source.sourceLinks.push(link)
target.targetLinks.push(link)
})
}
// Compute the value (size) and cycleness of each node by summing the associated links.
function computeNodeValues (graph) {
graph.nodes.forEach(function (node) {
node.partOfCycle = false
node.value = Math.max(
d3Array.sum(node.sourceLinks, value),
d3Array.sum(node.targetLinks, value)
)
node.sourceLinks.forEach(function (link) {
if (link.circular) {
node.partOfCycle = true
node.circularLinkType = link.circularLinkType
}
})
node.targetLinks.forEach(function (link) {
if (link.circular) {
node.partOfCycle = true
node.circularLinkType = link.circularLinkType
}
})
})
}
// Iteratively assign the depth (x-position) for each node.
// Nodes are assigned the maximum depth of incoming neighbors plus one;
// nodes with no incoming links are assigned depth zero, while
// nodes with no outgoing links are assigned the maximum depth.
function computeNodeDepths (graph) {
var nodes, next, x
for (
(nodes = graph.nodes), (next = []), (x = 0);
nodes.length;
++x, (nodes = next), (next = [])
) {
nodes.forEach(function (node) {
node.depth = x
node.sourceLinks.forEach(function (link) {
if (next.indexOf(link.target) < 0 && !link.circular) {
next.push(link.target)
}
})
})
}
for (
(nodes = graph.nodes), (next = []), (x = 0);
nodes.length;
++x, (nodes = next), (next = [])
) {
nodes.forEach(function (node) {
node.height = x
node.targetLinks.forEach(function (link) {
if (next.indexOf(link.source) < 0 && !link.circular) {
next.push(link.source)
}
})
})
}
var kx = (x1 - x0 - dx) / (x - 1)
graph.nodes.forEach(function (node) {
node.x1 =
(node.x0 =
x0 +
Math.max(
0,
Math.min(x - 1, Math.floor(align.call(null, node, x)))
) *
kx) + dx
})
}
function computeNodeBreadths (graph) {
var columns = d3Collection
.nest()
.key(function (d) {
return d.x0
})
.sortKeys(d3Array.ascending)
.entries(graph.nodes)
.map(function (d) {
return d.values
})
initializeNodeBreadth()
resolveCollisions()
for (var alpha = 1, n = iterations; n > 0; --n) {
// relaxRightToLeft((alpha *= 0.99))
// resolveCollisions()
// relaxLeftToRight((alpha *= 0.99))
// resolveCollisions()
relaxLeftAndRight((alpha *= 0.99))
resolveCollisions()
}
function initializeNodeBreadth () {
columns.forEach(function (nodes) {
let thisPadding = y1 * paddingRatio / (nodes.length + 1)
padding = thisPadding < padding ? thisPadding : padding
})
py = padding
var ky = d3Array.min(columns, function (nodes) {
return (y1 - y0 - (nodes.length - 1) * py) / d3Array.sum(nodes, value)
})
ky = ky * scale
columns.forEach(function (nodes) {
var nodesLength = nodes.length
nodes.forEach(function (node, i) {
if (node.partOfCycle) {
if (node.circularLinkType == 'top') {
node.y0 = y0 + i
node.y1 = node.y0 + node.value * ky
} else {
node.y0 = y1 - (node.value * ky) - i
node.y1 = node.y0 + node.value * ky
}
} else {
// node.y1 = (node.y0 = i) + node.value * ky
node.y0 = (y1 - y0) / 2 - nodesLength / 2 + i
node.y1 = node.y0 + node.value * ky
}
})
})
graph.links.forEach(function (link) {
link.width = link.value * ky
})
}
function relaxLeftAndRight (alpha) {
let columnsLength = columns.length
// console.log("cols: " + columnsLength);
columns.forEach(function (nodes, i) {
let n = nodes.length
let depth = nodes[0].depth
// console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
// console.log(depth + ': ' + n)
nodes.forEach(function (node) {
// check the node is not an orphan
if (node.sourceLinks.length || node.targetLinks.length) {
if (node.partOfCycle /*&& n > 1 /* && depth != 0 */) {
// console.log('do nothing')
} else if (depth == 0 && n == 1) {
let nodeHeight = node.y1 - node.y0
node.y0 = y1 / 2 - nodeHeight / 2
node.y1 = y1 / 2 + nodeHeight / 2
} else if (depth == columnsLength - 1 && n == 1) {
let nodeHeight = node.y1 - node.y0
node.y0 = y1 / 2 - nodeHeight / 2
node.y1 = y1 / 2 + nodeHeight / 2
} else {
let avg = 0
let avgTargetY = d3Array.mean(
node.sourceLinks,
linkTargetCenter
)
let avgSourceY = d3Array.mean(
node.targetLinks,
linkSourceCenter
)
if (avgTargetY && avgSourceY) {
avg = (avgTargetY + avgSourceY) / 2
} else {
avg = avgTargetY || avgSourceY
}
let dy = (avg - nodeCenter(node)) * alpha
// positive if it node needs to move down
// let dy = (nodeCenter(node.sourceLinks[0].target) - nodeCenter(node.sourceLinks[0].source) / 2) * alpha;
node.y0 += dy
node.y1 += dy
// console.log('after: ' + node.y0 + ' ' + node.y1)
}
}
})
})
}
function resolveCollisions () {
columns.forEach(function (nodes) {
var node, dy, y = y0, n = nodes.length, i
// Push any overlapping nodes down.
nodes.sort(ascendingBreadth)
for (i = 0; i < n; ++i) {
node = nodes[i]
dy = y - node.y0
if (dy > 0) {
node.y0 += dy
node.y1 += dy
}
y = node.y1 + py
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y - py - y1
if (dy > 0) {
;(y = node.y0 -= dy), (node.y1 -= dy)
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i]
dy = node.y1 + py - y
if (dy > 0) (node.y0 -= dy), (node.y1 -= dy)
y = node.y0
}
}
})
}
}
function computeLinkBreadths (graph) {
graph.nodes.forEach(function (node) {
node.sourceLinks.sort(ascendingTargetBreadth)
node.targetLinks.sort(ascendingSourceBreadth)
})
graph.nodes.forEach(function (node) {
var y0 = node.y0
var y1 = y0
// start from the bottom of the node for cycle links
var y0cycle = node.y1
var y1cycle = y0cycle
node.sourceLinks.forEach(function (link) {
if (link.circular) {
link.y0 = y0cycle - link.width / 2
y0cycle = y0cycle - link.width
} else {
link.y0 = y0 + link.width / 2
y0 += link.width
}
})
node.targetLinks.forEach(function (link) {
if (link.circular) {
link.y1 = y1cycle - link.width / 2
y1cycle = y1cycle - link.width
} else {
link.y1 = y1 + link.width / 2
y1 += link.width
}
})
})
}
return sankey
}
/// /////////////////////////////////////////////////////////////////////////////////
// Cycle functions
// Identify circles in the link objects
function identifyCircles (graph) {
var addedLinks = []
var circularLinkID = 0
graph.links.forEach(function (link) {
if (createsCycle(link.source, link.target, addedLinks)) {
link.circular = true
link.circularLinkID = circularLinkID
circularLinkID = circularLinkID + 1
} else {
link.circular = false
addedLinks.push(link)
}
})
}
function selectCircularLinkTypes (graph) {
let numberOfTops = 0
let numberOfBottoms = 0
graph.links.forEach(function (link) {
if (link.circular) {
// if either souce or target has type already use that
if (link.source.circularLinkType || link.target.circularLinkType) {
// default to source type if available
link.circularLinkType = link.source.circularLinkType
? link.source.circularLinkType
: link.target.circularLinkType
} else {
link.circularLinkType = numberOfTops < numberOfBottoms
? 'top'
: 'bottom'
}
if (link.circularLinkType == 'top') {
numberOfTops = numberOfTops + 1
} else {
numberOfBottoms = numberOfBottoms + 1
}
graph.nodes.forEach(function (node) {
if (node.name == link.source.name || node.name == link.target.name) {
node.circularLinkType = link.circularLinkType
}
})
}
})
}
// Checks if link creates a cycle
function createsCycle (originalSource, nodeToCheck, graph) {
if (graph.length == 0) {
return false
}
var nextLinks = findLinksOutward(nodeToCheck, graph)
// leaf node check
if (nextLinks.length == 0) {
return false
}
// cycle check
for (var i = 0; i < nextLinks.length; i++) {
var nextLink = nextLinks[i]
if (nextLink.target === originalSource) {
return true
}
// Recurse
if (createsCycle(originalSource, nextLink.target, graph)) {
return true
}
}
// Exhausted all links
return false
}
/* Given a node, find all links for which this is a source
in the current 'known' graph */
function findLinksOutward (node, graph) {
var children = []
for (var i = 0; i < graph.length; i++) {
if (node == graph[i].source) {
children.push(graph[i])
}
}
return children
}
// Create a normal curve or circular curve
//var curveSankeyForceLink =
// Return the angle between a straight line between the source and target of the link, and the vertical plane of the node
function linkAngle (link) {
let adjacent = Math.abs(link.y1 - link.y0)
let opposite = Math.abs(link.target.x0 - link.source.x1)
return Math.atan(opposite / adjacent)
}
function circularLinksCross (link1, link2) {
if (link1.source.depth < link2.target.depth) {
return false
} else if (link1.target.depth > link2.source.depth) {
return false
} else {
return true
}
}
function calcVerticalBuffer (links) {
links.sort(sortLinkDepthAscending)
links.forEach(function (link, i) {
let buffer = 0
let j = 0
for (j; j < i; j++) {
if (circularLinksCross(links[i], links[j])) {
let bufferOverThisLink =
links[j].circularPathData.verticalBuffer +
links[j].width / 2 +
circularLinkGap
buffer = bufferOverThisLink > buffer ? bufferOverThisLink : buffer
}
}
link.circularPathData.verticalBuffer = buffer + link.width / 2
})
return links
}
// calculate the optimum path for a link to reduce overlaps
function addCircularPathData (graph) {
let maxLinkWidth = d3.max(graph.links, function (link) {
return link.width
})
let minRadius = maxLinkWidth
let maxNodeDepth = d3.max(graph.links, function (link) {
return link.target.depth
})
let minY = d3.min(graph.links, function (link) {
return link.source.y0
})
let baseRadius = 10
// create object for circular Path Data
graph.links.forEach(function (link) {
if (link.circular) {
link.circularPathData = {}
}
})
// calc vertical offsets per top/bottom links
let topLinks = graph.links.filter(function (l) {
return l.circularLinkType == 'top'
})
topLinks = calcVerticalBuffer(topLinks)
let bottomLinks = graph.links.filter(function (l) {
return l.circularLinkType == 'bottom'
})
bottomLinks = calcVerticalBuffer(bottomLinks)
// add the base data for each link
graph.links.forEach(function (link) {
if (link.circular) {
link.circularPathData.arcRadius = link.width + baseRadius
link.circularPathData.leftNodeBuffer = 10
link.circularPathData.rightNodeBuffer = 10
link.circularPathData.sourceWidth = link.source.x1 - link.source.x0
link.circularPathData.targetWidth = link.target.x1 - link.target.x0 // probably won't use
link.circularPathData.sourceX =
link.source.x0 + link.circularPathData.sourceWidth
link.circularPathData.targetX = link.target.x0
link.circularPathData.sourceY = link.y0
link.circularPathData.targetY = link.y1
// add left extent coordinates, based on links with same source depth and circularLink type
let thisDepth = link.source.depth
let thisCircularLinkType = link.circularLinkType
let sameDepthLinks = graph.links.filter(function (l) {
return (
l.source.depth == thisDepth &&
l.circularLinkType == thisCircularLinkType
)
})
if (link.circularLinkType == 'bottom') {
sameDepthLinks.sort(sortLinkSourceYDescending)
} else {
sameDepthLinks.sort(sortLinkSourceYAscending)
}
let radiusOffset = 0
sameDepthLinks.forEach(function (l, i) {
if (l.circularLinkID == link.circularLinkID) {
link.circularPathData.leftSmallArcRadius =
baseRadius + link.width / 2 + radiusOffset
link.circularPathData.leftLargeArcRadius =
baseRadius + link.width / 2 + i * circularLinkGap + radiusOffset
}
radiusOffset = radiusOffset + l.width
})
// add right extent coordinates, based on links with same target depth and circularLink type
thisDepth = link.target.depth
sameDepthLinks = graph.links.filter(function (l) {
return (
l.target.depth == thisDepth &&
l.circularLinkType == thisCircularLinkType
)
})
if (link.circularLinkType == 'bottom') {
sameDepthLinks.sort(sortLinkTargetYDescending)
} else {
sameDepthLinks.sort(sortLinkTargetYAscending)
}
radiusOffset = 0
sameDepthLinks.forEach(function (l, i) {
if (l.circularLinkID == link.circularLinkID) {
link.circularPathData.rightSmallArcRadius =
baseRadius + link.width / 2 + radiusOffset
link.circularPathData.rightLargeArcRadius =
baseRadius + link.width / 2 + i * circularLinkGap + radiusOffset
}
radiusOffset = radiusOffset + l.width
})
// all links
link.circularPathData.leftInnerExtent =
link.circularPathData.sourceX + link.circularPathData.leftNodeBuffer
link.circularPathData.rightInnerExtent =
link.circularPathData.targetX - link.circularPathData.rightNodeBuffer
link.circularPathData.leftFullExtent =
link.circularPathData.sourceX +
link.circularPathData.leftLargeArcRadius +
link.circularPathData.leftNodeBuffer
link.circularPathData.rightFullExtent =
link.circularPathData.targetX -
link.circularPathData.rightLargeArcRadius -
link.circularPathData.rightNodeBuffer
// bottom links
if (link.circularLinkType == 'bottom') {
link.circularPathData.verticalFullExtent =
height + 25 + link.circularPathData.verticalBuffer
link.circularPathData.verticalLeftInnerExtent =
link.circularPathData.verticalFullExtent -
link.circularPathData.leftLargeArcRadius
link.circularPathData.verticalRightInnerExtent =
link.circularPathData.verticalFullExtent -
link.circularPathData.rightLargeArcRadius
} else {
// top links
link.circularPathData.verticalFullExtent =
minY - 25 - link.circularPathData.verticalBuffer
link.circularPathData.verticalLeftInnerExtent =
link.circularPathData.verticalFullExtent +
link.circularPathData.leftLargeArcRadius
link.circularPathData.verticalRightInnerExtent =
link.circularPathData.verticalFullExtent +
link.circularPathData.rightLargeArcRadius
}
link.circularPathData.path = createCircularPathString(link)
}
})
//return links
}
// create a d path using the addCircularPathData
function createCircularPathString (link) {
let pathString = ''
let pathData = {}
if (link.circularLinkType == 'top') {
pathString =
// start at the right of the source node
'M' +
link.circularPathData.sourceX +
' ' +
link.circularPathData.sourceY +
' ' +
// line right to buffer point
'L' +
link.circularPathData.leftInnerExtent +
' ' +
link.circularPathData.sourceY +
' ' +
// Arc around: Centre of arc X and //Centre of arc Y
'A' +
link.circularPathData.leftLargeArcRadius +
' ' +
link.circularPathData.leftSmallArcRadius +
' 0 0 0 ' +
// End of arc X //End of arc Y
link.circularPathData.leftFullExtent +
' ' +
(link.circularPathData.sourceY -
link.circularPathData.leftSmallArcRadius) +
' ' + // End of arc X
// line up to buffer point
'L' +
link.circularPathData.leftFullExtent +
' ' +
link.circularPathData.verticalLeftInnerExtent +
' ' +
// Arc around: Centre of arc X and //Centre of arc Y
'A' +
link.circularPathData.leftLargeArcRadius +
' ' +
link.circularPathData.leftLargeArcRadius +
' 0 0 0 ' +
// End of arc X //End of arc Y
link.circularPathData.leftInnerExtent +
' ' +
link.circularPathData.verticalFullExtent +
' ' + // End of arc X
// line left to buffer point
'L' +
link.circularPathData.rightInnerExtent +
' ' +
link.circularPathData.verticalFullExtent +
' ' +
// Arc around: Centre of arc X and //Centre of arc Y
'A' +
link.circularPathData.rightLargeArcRadius +
' ' +
link.circularPathData.rightLargeArcRadius +
' 0 0 0 ' +
// End of arc X //End of arc Y
link.circularPathData.rightFullExtent +
' ' +
link.circularPathData.verticalRightInnerExtent +
' ' + // End of arc X
// line down
'L' +
link.circularPathData.rightFullExtent +
' ' +
(link.circularPathData.targetY -
link.circularPathData.rightSmallArcRadius) +
' ' +
// Arc around: Centre of arc X and //Centre of arc Y
'A' +
link.circularPathData.rightLargeArcRadius +
' ' +
link.circularPathData.rightSmallArcRadius +
' 0 0 0 ' +
// End of arc X //End of arc Y
link.circularPathData.rightInnerExtent +
' ' +
link.circularPathData.targetY +
' ' + // End of arc X
// line to end
'L' +
link.circularPathData.targetX +
' ' +
link.circularPathData.targetY
} else {
// bottom path
pathString =
// start at the right of the source node
'M' +
link.circularPathData.sourceX +
' ' +
link.circularPathData.sourceY +
' ' +
// line right to buffer point
'L' +
link.circularPathData.leftInnerExtent +
' ' +
link.circularPathData.sourceY +
' ' +
// Arc around: Centre of arc X and //Centre of arc Y
'A' +
link.circularPathData.leftLargeArcRadius +
' ' +
link.circularPathData.leftSmallArcRadius +
' 0 0 1 ' +
// End of arc X //End of arc Y
link.circularPathData.leftFullExtent +
' ' +
(link.circularPathData.sourceY +
link.circularPathData.leftSmallArcRadius) +
' ' + // End of arc X
// line down to buffer point
'L' +
link.circularPathData.leftFullExtent +
' ' +
link.circularPathData.verticalLeftInnerExtent +
' ' +
// Arc around: Centre of arc X and //Centre of arc Y
'A' +
link.circularPathData.leftLargeArcRadius +
' ' +
link.circularPathData.leftLargeArcRadius +
' 0 0 1 ' +
// End of arc X //End of arc Y
link.circularPathData.leftInnerExtent +
' ' +
link.circularPathData.verticalFullExtent +
' ' + // End of arc X
// line left to buffer point
'L' +
link.circularPathData.rightInnerExtent +
' ' +
link.circularPathData.verticalFullExtent +
' ' +
// Arc around: Centre of arc X and //Centre of arc Y
'A' +
link.circularPathData.rightLargeArcRadius +
' ' +
link.circularPathData.rightLargeArcRadius +
' 0 0 1 ' +
// End of arc X //End of arc Y
link.circularPathData.rightFullExtent +
' ' +
link.circularPathData.verticalRightInnerExtent +
' ' + // End of arc X
// line up
'L' +
link.circularPathData.rightFullExtent +
' ' +
(link.circularPathData.targetY +
link.circularPathData.rightSmallArcRadius) +
' ' +
// Arc around: Centre of arc X and //Centre of arc Y
'A' +
link.circularPathData.rightLargeArcRadius +
' ' +
link.circularPathData.rightSmallArcRadius +
' 0 0 1 ' +
// End of arc X //End of arc Y
link.circularPathData.rightInnerExtent +
' ' +
link.circularPathData.targetY +
' ' + // End of arc X
// line to end
'L' +
link.circularPathData.targetX +
' ' +
link.circularPathData.targetY
}
return pathString
}
// sort links based on the distance between the source and tartget node depths
// if the same, then use Y position of the source node
function sortLinkDepthAscending (link1, link2) {
if (linkDepthDistance(link1) == linkDepthDistance(link2)) {
return link1.circularLinkType == 'bottom'
? sortLinkSourceYDescending(link1, link2)
: sortLinkSourceYAscending(link1, link2)
} else {
// return linkDepthDistance(link1) - linkDepthDistance(link2);
return linkDepthDistance(link2) - linkDepthDistance(link1)
}
}
function sortLinkSourceYAscending (link1, link2) {
return link1.y0 - link2.y0
}
function sortLinkSourceYDescending (link1, link2) {
return link2.y0 - link1.y0
}
function sortLinkTargetYAscending (link1, link2) {
return link1.y1 - link2.y1
}
function sortLinkTargetYDescending (link1, link2) {
return link2.y1 - link1.y1
}
// return the distance between the link's target and source node, in terms of the nodes' depth
function linkDepthDistance (link) {
return link.target.depth - link.source.depth
}
// return the distance between the link's target and source node, in terms of the nodes' X coordinate
function linkXLength (link) {
return link.target.x0 - link.source.x1
}
function linkPerpendicularYToLinkSource (longerLink, shorterLink) {
// Return the Y coordinate on the longerLink path * which is perpendicular shorterLink's source.
// * approx, based on a straight line from target to source, when in fact the path is a bezier
// get the angle for the longer link
let angle = linkAngle(longerLink)
// get the adjacent length to the other link's x position
let heightFromY1ToPependicular = linkXLength(shorterLink) / Math.tan(angle)
// add or subtract from longer link1's original y1, depending on the slope
let yPerpendicular = incline(longerLink) == 'up'
? longerLink.y1 + heightFromY1ToPependicular
: longerLink.y1 - heightFromY1ToPependicular
return yPerpendicular
}
function linkPerpendicularYToLinkTarget (longerLink, shorterLink) {
// Return the Y coordinate on the longerLink path * which is perpendicular shorterLink's source.
// * approx, based on a straight line from target to source, when in fact the path is a bezier
// get the angle for the longer link
let angle = linkAngle(longerLink)
// get the adjacent length to the other link's x position
let heightFromY1ToPependicular = linkXLength(shorterLink) / Math.tan(angle)
// add or subtract from longer link's original y1, depending on the slope
let yPerpendicular = incline(longerLink) == 'up'
? longerLink.y1 - heightFromY1ToPependicular
: longerLink.y1 + heightFromY1ToPependicular
return yPerpendicular
}
function resolveNodeLinkOverlaps (graph) {
graph.links.forEach(function (link) {
if (link.circular) {
return
}
if (link.target.depth - link.source.depth > 1) {
let depthToTest = link.source.depth + 1
let maxDepthToTest = link.target.depth - 1
let i = 1
let numberOfDepthsToTest = maxDepthToTest - depthToTest + 1
for (
depthToTest, (i = 1);
depthToTest <= maxDepthToTest;
depthToTest++, i++
) {
graph.nodes.forEach(function (node) {
if (node.depth == depthToTest) {
let t = i / (numberOfDepthsToTest + 1)
// Find all the points of a cubic bezier curve in javascript
// https://stackoverflow.com/questions/15397596/find-all-the-points-of-a-cubic-bezier-curve-in-javascript
let B0_t = Math.pow(1 - t, 3)
let B1_t = 3 * t * Math.pow(1 - t, 2)
let B2_t = 3 * Math.pow(t, 2) * (1 - t)
let B3_t = Math.pow(t, 3)
// if you need x coord
// let controlX = (link.source.x1 + link.target.x0)/2;
// let px_t = (B0_t * link.source.x1) + (B1_t * controlX) + (B2_t * controlX) + (B3_t * link.target.x0)
let py_t =
B0_t * link.y0 +
B1_t * link.y0 +
B2_t * link.y1 +
B3_t * link.y1
let linkY0AtDepth = py_t - link.width / 2
let linkY1AtDepth = py_t + link.width / 2
// If top of link overlaps node, push node up
if (linkY0AtDepth > node.y0 && linkY0AtDepth < node.y1) {
// console.log("OVERLAP!")
let dy = -(node.y1 - linkY0AtDepth + 10)
node = adjustNodeHeight(node, dy);
//check if other nodes need to move up too
graph.nodes.forEach(function(otherNode) {
//don't need to check itself or nodes at different depths
if ((otherNode.name == node.name) || (otherNode.depth != node.depth)) { return }
if (nodesOverlap(node, otherNode)) {
adjustNodeHeight(otherNode, dy)
}
})
} else if (linkY1AtDepth > node.y0 && linkY1AtDepth < node.y1) {
// If bottom of link overlaps node, push node down
let dy = linkY1AtDepth - node.y0 + 10
node = adjustNodeHeight(node, dy);
//check if other nodes need to move down too
graph.nodes.forEach(function(otherNode) {
//don't need to check itself or nodes at different depths
if ((otherNode.name == node.name) || (otherNode.depth != node.depth)) { return }
if (otherNode.y0 < node.y1 && otherNode.y1 > node.y1) {
adjustNodeHeight(otherNode, dy)
}
})
} else if (linkY0AtDepth < node.y0 && linkY1AtDepth > node.y1) {
// if link completely overlaps node
let dy = linkY1AtDepth - node.y0 + 10
node = adjustNodeHeight(node, dy);
graph.nodes.forEach(function(otherNode) {
//don't need to check itself or nodes at different depths
if ((otherNode.name == node.name) || (otherNode.depth != node.depth)) { return }
if (otherNode.y0 < node.y1 && otherNode.y1 > node.y1) {
adjustNodeHeight(otherNode, dy)
}
})
}
}
})
}
}
})
}
function nodesOverlap(nodeA, nodeB) {
//test if nodeA top partially overlaps nodeB
if (nodeA.y0 > nodeB.y0 && nodeA.y0 < nodeB.y1) {
return true
}
//test if nodeA bottom partially overlaps nodeB
else if (nodeA.y1 > nodeB.y0 && nodeA.y1 < nodeB.y1) {
return true
}
//test if nodeA covers nodeB
else if (nodeA.y0 < nodeB.y0 && nodeA.y1 > nodeB.y1) {
return true
}
else {
return false;
}
}
function adjustNodeHeight (node, dy) {
node.y0 = node.y0 + dy
node.y1 = node.y1 + dy
node.targetLinks.forEach(function (l) {
l.y1 = l.y1 + dy
})
node.sourceLinks.forEach(function (l) {
l.y0 = l.y0 + dy
})
return node;
}
function sortSourceLinks (graph) {
graph.nodes.forEach(function (node) {
// move any nodes up which are off the bottom
if (node.y + (node.y1 - node.y0) > height) {
node.y = node.y - (node.y + (node.y1 - node.y0) - height)
}
let nodesSourceLinks = graph.links.filter(function (l) {
return l.source.name == node.name
})
// if more than 1 link then sort
if (nodesSourceLinks.length > 1) {
nodesSourceLinks.sort(function (link1, link2) {
// if both are not circular...
if (!link1.circular && !link2.circular) {
// if the target nodes are the same depth, then sort by the link's target y
if (link1.target.depth == link2.target.depth) {
return link1.y1 - link2.y1
} else if (!sameInclines(link1, link2)) {
// if the links slope in different directions, then sort by the link's target y
return link1.y1 - link2.y1
// if the links slope in same directions, then sort by any overlap
} else {
if (link1.target.depth > link2.target.depth) {
// if (node.name == "process10") {console.log("here")}
/* let link2Angle = linkAngleFromSource(link2);
let link2AdjToLink1Y = linkXLength(link1) / Math.tan(link2Angle);
let link2Adj = incline(link2) == "up"
? link2.y0 - link2AdjToLink1Y
: link2.y0 + link2AdjToLink1Y; */
let link2Adj = linkPerpendicularYToLinkTarget(link2, link1)
return link1.y1 - link2Adj
}
if (link2.target.depth > link1.target.depth) {
/* let link1Angle = linkAngleFromSource(link1);
let link1AdjToLink2Y = linkXLength(link2) / Math.tan(link1Angle);
let link1Adj = incline(link1) == "up"
? link1.y0 - link1AdjToLink2Y
: link1.y0 + link1AdjToLink2Y; */
let link1Adj = linkPerpendicularYToLinkTarget(link1, link2)
return link1Adj - link2.y1
}
}
}
// if only one is circular, the move top links up, or bottom links down
if (link1.circular && !link2.circular) {
return link1.circularLinkType == 'top' ? -1 : 1
} else if (link2.circular && !link1.circular) {
return link2.circularLinkType == 'top' ? 1 : -1
}
// if both links are circular...
if (link1.circular && link2.circular) {
// ...and they both loop the same way (both top)
if (
link1.circularLinkType === link2.circularLinkType &&
link1.circularLinkType == 'top'
) {
// ...and they both connect to a target with same depth, then sort by the target's y
if (link1.target.depth === link2.target.depth) {
return link1.target.y1 - link2.target.y1
} else {
// ...and they connect to different depth targets, then sort by how far back they
return link2.target.depth - link1.target.depth
}
} else if (
link1.circularLinkType === link2.circularLinkType &&
link1.circularLinkType == 'bottom'
) {
// ...and they both loop the same way (both bottom)
// ...and they both connect to a target with same depth, then sort by the target's y
if (link1.target.depth === link2.target.depth) {
return link2.target.y1 - link1.target.y1
} else {
// ...and they connect to different depth targets, then sort by how far back they
return link1.target.depth - link2.target.depth
}
} else {
// ...and they loop around different ways, the move top up and bottom down
return link1.circularLinkType == 'top' ? -1 : 1
}
}
})
}
// update y0 for links
let ySourceOffset = node.y0
nodesSourceLinks.forEach(function (link) {
link.y0 = ySourceOffset + link.width / 2
ySourceOffset = ySourceOffset + link.width
})
})
}
function sortTargetLinks (graph) {
graph.nodes.forEach(function (node) {
let nodesTargetLinks = graph.links.filter(function (l) {
return l.target.name == node.name
})
if (nodesTargetLinks.length > 1) {
nodesTargetLinks.sort(function (link1, link2) {
// if both are not circular, the base on the source y position
if (!link1.circular && !link2.circular) {
if (link1.source.depth == link2.source.depth) {
return link1.y0 - link2.y0
} else if (!sameInclines(link1, link2)) {
return link1.y0 - link2.y0
} else {
// get the angle of the link to the further source node (ie the smaller depth)
if (link2.source.depth < link1.source.depth) {
let link2Adj = linkPerpendicularYToLinkSource(link2, link1)
return link1.y0 - link2Adj
}
if (link1.source.depth < link2.source.depth) {
let link1Adj = linkPerpendicularYToLinkSource(link1, link2)
return link1Adj - link2.y0
}
}
}
// if only one is circular, the move top links up, or bottom links down
if (link1.circular && !link2.circular) {
return link1.circularLinkType == 'top' ? -1 : 1
} else if (link2.circular && !link1.circular) {
return link2.circularLinkType == 'top' ? 1 : -1
}
// if both links are circular...
if (link1.circular && link2.circular) {
// ...and they both loop the same way (both top)
if (
link1.circularLinkType === link2.circularLinkType &&
link1.circularLinkType == 'top'
) {
// ...and they both connect to a target with same depth, then sort by the target's y
if (link1.source.depth === link2.source.depth) {
return link1.source.y1 - link2.source.y1
} else {
// ...and they connect to different depth targets, then sort by how far back they
return link1.source.depth - link2.source.depth
}
} else if (
link1.circularLinkType === link2.circularLinkType &&
link1.circularLinkType == 'bottom'
) {
// ...and they both loop the same way (both bottom)
// ...and they both connect to a target with same depth, then sort by the target's y
if (link1.source.depth === link2.source.depth) {
return link1.source.y1 - link2.source.y1
} else {
// ...and they connect to different depth targets, then sort by how far back they
return link2.source.depth - link1.source.depth
}
} else {
// ...and they loop around different ways, the move top up and bottom down
return link1.circularLinkType == 'top' ? -1 : 1
}
}
})
}
// update y1 for links
let yTargetOffset = node.y0
nodesTargetLinks.forEach(function (link) {
link.y1 = yTargetOffset + link.width / 2
yTargetOffset = yTargetOffset + link.width
})
})
}
function sameInclines (link1, link2) {
return incline(link1) == incline(link2)
}
function incline (link) {
// positive = slopes up from source to target
// negative = slopes down from source to target
return link.y0 - link.y1 > 0 ? 'up' : 'down'
}
///////////////////////////////////////////////////////////////////////////////
exports.sankey = sankey
exports.sankeyCenter = center
exports.sankeyLeft = left
exports.sankeyRight = right
exports.sankeyJustify = justify
// exports.sankeyLinkHorizontal = sankeyLinkHorizontal
// exports.curveSankeyForceLink = curveSankeyForceLink
Object.defineProperty(exports, '__esModule', { value: true })
})
let data2 = {
"nodes": [
{ "name": "startA" },
{ "name": "startB" },
{ "name": "process1" },
{ "name": "process2" },
{ "name": "process3" },
{ "name": "process4" },
{ "name": "process5" },
{ "name": "process6" },
{ "name": "process7" },
{ "name": "process8" },
{ "name": "process9" },
{ "name": "process10" },
{ "name": "process11" },
{ "name": "process12" },
{ "name": "process13" },
{ "name": "process14" },
{ "name": "process15" },
{ "name": "process16" },
{ "name": "finishA" },
{ "name": "finishB" }
],
"links": [
{ "source": "startA", "target": "process8", "value": 20, "optimal": "yes" },
{ "source": "startA", "target": "process5", "value": 20, "optimal": "yes" },
{ "source": "startA", "target": "process6", "value": 20, "optimal": "yes" },
{ "source": "startB", "target": "process1", "value": 15, "optimal": "yes" },
{ "source": "startB", "target": "process5", "value": 15, "optimal": "yes" },
{ "source": "process1", "target": "process4", "value": 30, "optimal": "yes" },
{ "source": "process4", "target": "process1", "value": 10, "optimal": "yes" },
{ "source": "process2", "target": "process7", "value": 35, "optimal": "yes" },
{ "source": "process1", "target": "process3", "value": 20, "optimal": "yes" },
{ "source": "process5", "target": "process1", "value": 20, "optimal": "yes" },
{ "source": "process6", "target": "startA", "value": 5, "optimal": "yes" },
{ "source": "process4", "target": "process2", "value": 5, "optimal": "yes" },
{ "source": "process6", "target": "process8", "value": 15, "optimal": "yes" },
{ "source": "process4", "target": "startB", "value": 5, "optimal": "yes" },
{ "source": "process3", "target": "process2", "value": 15, "optimal": "yes" },
{ "source": "process3", "target": "startB", "value": 5, "optimal": "yes" },
{ "source": "process15", "target": "process13", "value": 10, "optimal": "yes" },
{ "source": "process13", "target": "process9", "value": 10, "optimal": "yes" },
{ "source": "process7", "target": "startB", "value": 20, "optimal": "yes" },
{ "source": "process8", "target": "process1", "value": 10, "optimal": "yes" },
{ "source": "process8", "target": "process16", "value": 10, "optimal": "yes" },
{ "source": "process16", "target": "process9", "value": 10, "optimal": "yes" },
{ "source": "process8", "target": "process11", "value": 25, "optimal": "yes" },
{ "source": "process11", "target": "process10", "value": 20, "optimal": "yes" },
{ "source": "process4", "target": "process12", "value": 10, "optimal": "yes" },
{ "source": "process12", "target": "process11", "value": 10, "optimal": "yes" },
{ "source": "process7", "target": "process15", "value": 15, "optimal": "yes" },
{ "source": "process15", "target": "process14", "value": 10, "optimal": "yes" },
{ "source": "process10", "target": "process13", "value": 10, "optimal": "yes" },
{ "source": "process10", "target": "process16", "value": 10, "optimal": "yes" },
{ "source": "process14", "target": "finishB", "value": 10, "optimal": "yes" },
{ "source": "process9", "target": "finishA", "value": 10, "optimal": "yes" },
{ "source": "process16", "target": "process8", "value": 10, "optimal": "yes" },
{ "source": "process9", "target": "finishB", "value": 10, "optimal": "yes" },
{ "source": "process15", "target": "finishB", "value": 10, "optimal": "yes" },
{ "source": "process15", "target": "finishA", "value": 10, "optimal": "yes" },
{ "source": "process11", "target": "process15", "value": 25, "optimal": "yes" }
]
};
var data1 = {
"nodes": [
{ "name": "start" },
{ "name": "process0-0" },
{ "name": "process0-1" },
{ "name": "process0-2" },
{ "name": "process0-3" },
{ "name": "process0-4" },
{ "name": "process0-5" },
{ "name": "process0-6" },
{ "name": "process0-7" },
{ "name": "process0-8" },
{ "name": "process0-9" },
{ "name": "process1-0" },
{ "name": "process1-1" },
{ "name": "process1-2" },
{ "name": "process1-3" },
{ "name": "process1-4" },
{ "name": "process1-5" },
{ "name": "process1-6" },
{ "name": "process1-7" },
{ "name": "process1-8" },
{ "name": "process1-9" },
{ "name": "process2-0" },
{ "name": "process2-1" },
{ "name": "process2-2" },
{ "name": "process2-3" },
{ "name": "process2-4" },
{ "name": "process2-5" },
{ "name": "process2-6" },
{ "name": "process2-7" },
{ "name": "process2-8" },
{ "name": "process2-9" },
{ "name": "process3-0" },
{ "name": "process3-1" },
{ "name": "process3-2" },
{ "name": "process3-3" },
{ "name": "process3-4" },
{ "name": "process3-5" },
{ "name": "process3-6" },
{ "name": "process3-7" },
{ "name": "process3-8" },
{ "name": "process3-9" },
{ "name": "process4-0" },
{ "name": "process4-1" },
{ "name": "process4-2" },
{ "name": "process4-3" },
{ "name": "process4-4" },
{ "name": "process4-5" },
{ "name": "process4-6" },
{ "name": "process4-7" },
{ "name": "process4-8" },
{ "name": "process4-9" },
{ "name": "process5-0" },
{ "name": "process5-1" },
{ "name": "process5-2" },
{ "name": "process5-3" },
{ "name": "process5-4" },
{ "name": "process5-5" },
{ "name": "process5-6" },
{ "name": "process5-7" },
{ "name": "process5-8" },
{ "name": "process5-9" },
{ "name": "finish" }
],
"links": [
{ "source": "start", "target": "process0-0", "value": 3 },
{ "source": "start", "target": "process0-1", "value": 1 },
{ "source": "start", "target": "process0-2", "value": 3 },
{ "source": "start", "target": "process0-3", "value": 5 },
{ "source": "start", "target": "process0-4", "value": 4 },
{ "source": "start", "target": "process0-5", "value": 2 },
{ "source": "start", "target": "process0-6", "value": 5 },
{ "source": "start", "target": "process0-7", "value": 5 },
{ "source": "start", "target": "process0-8", "value": 1 },
{ "source": "start", "target": "process0-9", "value": 1 },
{ "source": "process0-0", "target": "process1-0", "value": 3 },
{ "source": "process0-0", "target": "process1-7", "value": 1 },
{ "source": "process0-0", "target": "process1-3", "value": 5 },
{ "source": "process0-0", "target": "process1-3", "value": 2 },
{ "source": "process0-0", "target": "process1-6", "value": 4 },
{ "source": "process0-1", "target": "process1-5", "value": 4 },
{ "source": "process0-1", "target": "process1-7", "value": 2 },
{ "source": "process0-1", "target": "process1-4", "value": 1 },
{ "source": "process0-1", "target": "process1-3", "value": 4 },
{ "source": "process0-1", "target": "process1-7", "value": 1 },
{ "source": "process0-2", "target": "process1-1", "value": 3 },
{ "source": "process0-2", "target": "process1-0", "value": 4 },
{ "source": "process0-2", "target": "process1-2", "value": 2 },
{ "source": "process0-2", "target": "process1-1", "value": 3 },
{ "source": "process0-2", "target": "process1-8", "value": 1 },
{ "source": "process0-3", "target": "process1-4", "value": 3 },
{ "source": "process0-3", "target": "process1-8", "value": 1 },
{ "source": "process0-3", "target": "process1-5", "value": 4 },
{ "source": "process0-3", "target": "process1-2", "value": 3 },
{ "source": "process0-3", "target": "process1-2", "value": 2 },
{ "source": "process0-4", "target": "process1-6", "value": 4 },
{ "source": "process0-4", "target": "process1-1", "value": 3 },
{ "source": "process0-4", "target": "process1-5", "value": 5 },
{ "source": "process0-4", "target": "process1-2", "value": 5 },
{ "source": "process0-4", "target": "process1-9", "value": 4 },
{ "source": "process0-5", "target": "process1-7", "value": 4 },
{ "source": "process0-5", "target": "process1-9", "value": 4 },
{ "source": "process0-5", "target": "process1-5", "value": 1 },
{ "source": "process0-5", "target": "process1-5", "value": 2 },
{ "source": "process0-5", "target": "process1-3", "value": 4 },
{ "source": "process0-6", "target": "process1-6", "value": 2 },
{ "source": "process0-6", "target": "process1-4", "value": 5 },
{ "source": "process0-6", "target": "process1-0", "value": 2 },
{ "source": "process0-6", "target": "process1-9", "value": 2 },
{ "source": "process0-6", "target": "process1-5", "value": 3 },
{ "source": "process0-7", "target": "process1-7", "value": 1 },
{ "source": "process0-7", "target": "process1-9", "value": 3 },
{ "source": "process0-7", "target": "process1-1", "value": 4 },
{ "source": "process0-7", "target": "process1-2", "value": 5 },
{ "source": "process0-7", "target": "process1-2", "value": 3 },
{ "source": "process0-8", "target": "process1-7", "value": 3 },
{ "source": "process0-8", "target": "process1-7", "value": 3 },
{ "source": "process0-8", "target": "process1-0", "value": 3 },
{ "source": "process0-8", "target": "process1-6", "value": 5 },
{ "source": "process0-8", "target": "process1-0", "value": 1 },
{ "source": "process0-9", "target": "process1-3", "value": 5 },
{ "source": "process0-9", "target": "process1-8", "value": 5 },
{ "source": "process0-9", "target": "process1-2", "value": 5 },
{ "source": "process0-9", "target": "process1-5", "value": 2 },
{ "source": "process0-9", "target": "process1-7", "value": 4 },
{ "source": "process1-0", "target": "process2-9", "value": 3 },
{ "source": "process1-0", "target": "process2-4", "value": 5 },
{ "source": "process1-0", "target": "process2-3", "value": 1 },
{ "source": "process1-0", "target": "process2-0", "value": 4 },
{ "source": "process1-0", "target": "process2-1", "value": 1 },
{ "source": "process1-1", "target": "process2-4", "value": 3 },
{ "source": "process1-1", "target": "process2-0", "value": 3 },
{ "source": "process1-1", "target": "process2-5", "value": 1 },
{ "source": "process1-1", "target": "process2-2", "value": 4 },
{ "source": "process1-1", "target": "process2-9", "value": 5 },
{ "source": "process1-2", "target": "process2-6", "value": 3 },
{ "source": "process1-2", "target": "process2-1", "value": 1 },
{ "source": "process1-2", "target": "process2-4", "value": 4 },
{ "source": "process1-2", "target": "process2-9", "value": 1 },
{ "source": "process1-2", "target": "process2-8", "value": 3 },
{ "source": "process1-3", "target": "process2-5", "value": 4 },
{ "source": "process1-3", "target": "process2-7", "value": 5 },
{ "source": "process1-3", "target": "process2-4", "value": 4 },
{ "source": "process1-3", "target": "process2-7", "value": 5 },
{ "source": "process1-3", "target": "process2-0", "value": 3 },
{ "source": "process1-4", "target": "process2-8", "value": 3 },
{ "source": "process1-4", "target": "process2-7", "value": 3 },
{ "source": "process1-4", "target": "process2-4", "value": 2 },
{ "source": "process1-4", "target": "process2-2", "value": 5 },
{ "source": "process1-4", "target": "process2-9", "value": 3 },
{ "source": "process1-5", "target": "process2-2", "value": 1 },
{ "source": "process1-5", "target": "process2-8", "value": 5 },
{ "source": "process1-5", "target": "process2-3", "value": 3 },
{ "source": "process1-5", "target": "process2-5", "value": 4 },
{ "source": "process1-5", "target": "process2-4", "value": 3 },
{ "source": "process1-6", "target": "process2-6", "value": 5 },
{ "source": "process1-6", "target": "process2-2", "value": 3 },
{ "source": "process1-6", "target": "process2-7", "value": 4 },
{ "source": "process1-6", "target": "process2-6", "value": 5 },
{ "source": "process1-6", "target": "process2-3", "value": 5 },
{ "source": "process1-7", "target": "process2-4", "value": 4 },
{ "source": "process1-7", "target": "process2-8", "value": 3 },
{ "source": "process1-7", "target": "process2-6", "value": 1 },
{ "source": "process1-7", "target": "process2-9", "value": 3 },
{ "source": "process1-7", "target": "process2-0", "value": 5 },
{ "source": "process1-8", "target": "process2-9", "value": 5 },
{ "source": "process1-8", "target": "process2-7", "value": 1 },
{ "source": "process1-8", "target": "process2-4", "value": 1 },
{ "source": "process1-8", "target": "process2-8", "value": 3 },
{ "source": "process1-8", "target": "process2-8", "value": 2 },
{ "source": "process1-9", "target": "process2-0", "value": 2 },
{ "source": "process1-9", "target": "process2-9", "value": 2 },
{ "source": "process1-9", "target": "process2-5", "value": 5 },
{ "source": "process1-9", "target": "process2-6", "value": 4 },
{ "source": "process1-9", "target": "process2-2", "value": 3 },
{ "source": "process2-0", "target": "process3-8", "value": 5 },
{ "source": "process2-0", "target": "process3-2", "value": 4 },
{ "source": "process2-0", "target": "process3-3", "value": 2 },
{ "source": "process2-0", "target": "process3-5", "value": 5 },
{ "source": "process2-0", "target": "process3-2", "value": 1 },
{ "source": "process2-1", "target": "process3-5", "value": 5 },
{ "source": "process2-1", "target": "process3-2", "value": 3 },
{ "source": "process2-1", "target": "process3-7", "value": 2 },
{ "source": "process2-1", "target": "process3-6", "value": 5 },
{ "source": "process2-1", "target": "process3-9", "value": 3 },
{ "source": "process2-2", "target": "process3-2", "value": 4 },
{ "source": "process2-2", "target": "process3-4", "value": 1 },
{ "source": "process2-2", "target": "process3-7", "value": 4 },
{ "source": "process2-2", "target": "process3-2", "value": 3 },
{ "source": "process2-2", "target": "process3-9", "value": 2 },
{ "source": "process2-3", "target": "process3-4", "value": 4 },
{ "source": "process2-3", "target": "process3-3", "value": 2 },
{ "source": "process2-3", "target": "process3-0", "value": 1 },
{ "source": "process2-3", "target": "process3-5", "value": 2 },
{ "source": "process2-3", "target": "process3-8", "value": 4 },
{ "source": "process2-4", "target": "process3-1", "value": 3 },
{ "source": "process2-4", "target": "process3-1", "value": 3 },
{ "source": "process2-4", "target": "process3-1", "value": 3 },
{ "source": "process2-4", "target": "process3-4", "value": 2 },
{ "source": "process2-4", "target": "process3-4", "value": 4 },
{ "source": "process2-5", "target": "process3-8", "value": 4 },
{ "source": "process2-5", "target": "process3-2", "value": 5 },
{ "source": "process2-5", "target": "process3-4", "value": 2 },
{ "source": "process2-5", "target": "process3-1", "value": 5 },
{ "source": "process2-5", "target": "process3-4", "value": 4 },
{ "source": "process2-6", "target": "process3-5", "value": 4 },
{ "source": "process2-6", "target": "process3-6", "value": 4 },
{ "source": "process2-6", "target": "process3-7", "value": 5 },
{ "source": "process2-6", "target": "process3-9", "value": 1 },
{ "source": "process2-6", "target": "process3-9", "value": 4 },
{ "source": "process2-7", "target": "process3-1", "value": 3 },
{ "source": "process2-7", "target": "process3-5", "value": 3 },
{ "source": "process2-7", "target": "process3-8", "value": 1 },
{ "source": "process2-7", "target": "process3-4", "value": 3 },
{ "source": "process2-7", "target": "process3-9", "value": 5 },
{ "source": "process2-8", "target": "process3-7", "value": 2 },
{ "source": "process2-8", "target": "process3-5", "value": 3 },
{ "source": "process2-8", "target": "process3-5", "value": 3 },
{ "source": "process2-8", "target": "process3-2", "value": 2 },
{ "source": "process2-8", "target": "process3-1", "value": 4 },
{ "source": "process2-9", "target": "process3-4", "value": 3 },
{ "source": "process2-9", "target": "process3-5", "value": 2 },
{ "source": "process2-9", "target": "process3-3", "value": 2 },
{ "source": "process2-9", "target": "process3-1", "value": 3 },
{ "source": "process2-9", "target": "process3-7", "value": 3 },
{ "source": "process3-0", "target": "process4-5", "value": 3 },
{ "source": "process3-0", "target": "process4-6", "value": 1 },
{ "source": "process3-0", "target": "process4-4", "value": 1 },
{ "source": "process3-0", "target": "process4-3", "value": 5 },
{ "source": "process3-0", "target": "process4-4", "value": 5 },
{ "source": "process3-1", "target": "process4-0", "value": 4 },
{ "source": "process3-1", "target": "process4-8", "value": 1 },
{ "source": "process3-1", "target": "process4-0", "value": 2 },
{ "source": "process3-1", "target": "process4-8", "value": 1 },
{ "source": "process3-1", "target": "process4-7", "value": 5 },
{ "source": "process3-2", "target": "process4-5", "value": 5 },
{ "source": "process3-2", "target": "process4-9", "value": 3 },
{ "source": "process3-2", "target": "process4-5", "value": 2 },
{ "source": "process3-2", "target": "process4-6", "value": 2 },
{ "source": "process3-2", "target": "process4-2", "value": 4 },
{ "source": "process3-3", "target": "process4-6", "value": 2 },
{ "source": "process3-3", "target": "process4-3", "value": 4 },
{ "source": "process3-3", "target": "process4-0", "value": 3 },
{ "source": "process3-3", "target": "process4-3", "value": 4 },
{ "source": "process3-3", "target": "process4-5", "value": 3 },
{ "source": "process3-4", "target": "process4-2", "value": 4 },
{ "source": "process3-4", "target": "process4-4", "value": 4 },
{ "source": "process3-4", "target": "process4-6", "value": 3 },
{ "source": "process3-4", "target": "process4-9", "value": 3 },
{ "source": "process3-4", "target": "process4-1", "value": 5 },
{ "source": "process3-5", "target": "process4-7", "value": 3 },
{ "source": "process3-5", "target": "process4-9", "value": 4 },
{ "source": "process3-5", "target": "process4-8", "value": 4 },
{ "source": "process3-5", "target": "process4-3", "value": 3 },
{ "source": "process3-5", "target": "process4-0", "value": 4 },
{ "source": "process3-6", "target": "process4-8", "value": 5 },
{ "source": "process3-6", "target": "process4-9", "value": 1 },
{ "source": "process3-6", "target": "process4-3", "value": 2 },
{ "source": "process3-6", "target": "process4-7", "value": 4 },
{ "source": "process3-6", "target": "process4-8", "value": 1 },
{ "source": "process3-7", "target": "process4-1", "value": 1 },
{ "source": "process3-7", "target": "process4-2", "value": 3 },
{ "source": "process3-7", "target": "process4-1", "value": 4 },
{ "source": "process3-7", "target": "process4-4", "value": 5 },
{ "source": "process3-7", "target": "process4-2", "value": 4 },
{ "source": "process3-8", "target": "process4-4", "value": 4 },
{ "source": "process3-8", "target": "process4-5", "value": 4 },
{ "source": "process3-8", "target": "process4-7", "value": 2 },
{ "source": "process3-8", "target": "process4-7", "value": 1 },
{ "source": "process3-8", "target": "process4-5", "value": 4 },
{ "source": "process3-9", "target": "process4-8", "value": 4 },
{ "source": "process3-9", "target": "process4-7", "value": 2 },
{ "source": "process3-9", "target": "process4-5", "value": 2 },
{ "source": "process3-9", "target": "process4-0", "value": 2 },
{ "source": "process3-9", "target": "process4-9", "value": 5 },
{ "source": "process4-0", "target": "process5-3", "value": 5 },
{ "source": "process4-0", "target": "process5-6", "value": 3 },
{ "source": "process4-0", "target": "process5-5", "value": 5 },
{ "source": "process4-0", "target": "process5-0", "value": 3 },
{ "source": "process4-0", "target": "process5-8", "value": 4 },
{ "source": "process4-1", "target": "process5-2", "value": 3 },
{ "source": "process4-1", "target": "process5-3", "value": 2 },
{ "source": "process4-1", "target": "process5-7", "value": 5 },
{ "source": "process4-1", "target": "process5-1", "value": 2 },
{ "source": "process4-1", "target": "process5-3", "value": 5 },
{ "source": "process4-2", "target": "process5-0", "value": 1 },
{ "source": "process4-2", "target": "process5-1", "value": 5 },
{ "source": "process4-2", "target": "process5-9", "value": 5 },
{ "source": "process4-2", "target": "process5-3", "value": 1 },
{ "source": "process4-2", "target": "process5-4", "value": 4 },
{ "source": "process4-3", "target": "process5-6", "value": 3 },
{ "source": "process4-3", "target": "process5-7", "value": 3 },
{ "source": "process4-3", "target": "process5-0", "value": 4 },
{ "source": "process4-3", "target": "process5-9", "value": 3 },
{ "source": "process4-3", "target": "process5-9", "value": 1 },
{ "source": "process4-4", "target": "process5-4", "value": 4 },
{ "source": "process4-4", "target": "process5-8", "value": 2 },
{ "source": "process4-4", "target": "process5-4", "value": 2 },
{ "source": "process4-4", "target": "process5-3", "value": 4 },
{ "source": "process4-4", "target": "process5-6", "value": 2 },
{ "source": "process4-5", "target": "process5-5", "value": 1 },
{ "source": "process4-5", "target": "process5-1", "value": 1 },
{ "source": "process4-5", "target": "process5-1", "value": 4 },
{ "source": "process4-5", "target": "process5-6", "value": 3 },
{ "source": "process4-5", "target": "process5-9", "value": 5 },
{ "source": "process4-6", "target": "process5-3", "value": 3 },
{ "source": "process4-6", "target": "process5-2", "value": 4 },
{ "source": "process4-6", "target": "process5-0", "value": 5 },
{ "source": "process4-6", "target": "process5-7", "value": 1 },
{ "source": "process4-6", "target": "process5-2", "value": 5 },
{ "source": "process4-7", "target": "process5-6", "value": 5 },
{ "source": "process4-7", "target": "process5-5", "value": 1 },
{ "source": "process4-7", "target": "process5-8", "value": 1 },
{ "source": "process4-7", "target": "process5-1", "value": 3 },
{ "source": "process4-7", "target": "process5-9", "value": 2 },
{ "source": "process4-8", "target": "process5-3", "value": 5 },
{ "source": "process4-8", "target": "process5-1", "value": 3 },
{ "source": "process4-8", "target": "process5-8", "value": 4 },
{ "source": "process4-8", "target": "process5-4", "value": 5 },
{ "source": "process4-8", "target": "process5-4", "value": 4 },
{ "source": "process4-9", "target": "process5-0", "value": 4 },
{ "source": "process4-9", "target": "process5-0", "value": 2 },
{ "source": "process4-9", "target": "process5-1", "value": 2 },
{ "source": "process4-9", "target": "process5-7", "value": 1 },
{ "source": "process4-9", "target": "process5-7", "value": 4 },
{ "source": "process5-0", "target": "finish", "value": 4 },
{ "source": "process5-1", "target": "finish", "value": 2 },
{ "source": "process5-2", "target": "finish", "value": 5 },
{ "source": "process5-3", "target": "finish", "value": 1 },
{ "source": "process5-4", "target": "finish", "value": 1 },
{ "source": "process5-5", "target": "finish", "value": 3 },
{ "source": "process5-6", "target": "finish", "value": 1 },
{ "source": "process5-7", "target": "finish", "value": 5 },
{ "source": "process5-8", "target": "finish", "value": 4 },
{ "source": "process5-8", "target": "start", "value": 4 },
{ "source": "process5-9", "target": "finish", "value": 4 }
]
}
let data3 = {
"nodes": [
{ "name": "Oceans" },
{ "name": "Evaporation" },
{ "name": "Atmosphere" },
{ "name": "Condensation" },
{ "name": "Precipitation" },
{ "name": "Ice and snow" },
{ "name": "Infiltration" },
{ "name": "Seepage" },
{ "name": "Spring" },
{ "name": "Freshwater" },
// { "name": "Soil moisture" },
{ "name": "Plants and animals" },
{ "name": "Sublimation" },
{ "name": "Groundwater flow" },
{ "name": "Groundwater storage" },
{ "name": "Surface runoff" },
{ "name": "Plant uptake"},
{ "name": "Evapotranspiration"},
],
"links": [
{ "source": "Oceans", "target": "Evaporation", "value": 4 },
{ "source": "Evaporation", "target": "Condensation", "value": 4 },
{ "source": "Condensation", "target": "Atmosphere", "value": 4 },
{ "source": "Atmosphere", "target": "Precipitation", "value": 4 },
{ "source": "Precipitation", "target": "Ice and snow", "value": 4 },
{ "source": "Precipitation", "target": "Oceans", "value": 4 },
{ "source": "Precipitation", "target": "Surface runoff", "value": 4 },
{ "source": "Ice and snow", "target": "Infiltration", "value": 4 },
{ "source": "Ice and snow", "target": "Sublimation", "value": 4 },
{ "source": "Sublimation", "target": "Atmosphere", "value": 4 },
{ "source": "Infiltration", "target": "Groundwater flow", "value": 4 },
{ "source": "Infiltration", "target": "Groundwater storage", "value": 4 },
{ "source": "Groundwater storage", "target": "Oceans", "value": 4 },
{ "source": "Groundwater flow", "target": "Seepage", "value": 4 },
{ "source": "Groundwater flow", "target": "Spring", "value": 4 },
{ "source": "Groundwater flow", "target": "Plant uptake", "value": 4 },
{ "source": "Groundwater flow", "target": "Oceans", "value": 4 },
{ "source": "Groundwater flow", "target": "Freshwater", "value": 4 },
{ "source": "Seepage", "target": "Freshwater", "value": 4 },
{ "source": "Spring", "target": "Freshwater", "value": 4 },
{ "source": "Freshwater", "target": "Evaporation", "value": 4 },
{ "source": "Freshwater", "target": "Plants and animals", "value": 4 },
{ "source": "Freshwater", "target": "Seepage", "value": 4 },
{ "source": "Plant uptake", "target": "Plants and animals", "value": 4 },
{ "source": "Plants and animals", "target": "Freshwater", "value": 4 },
{ "source": "Surface runoff", "target": "Groundwater flow", "value": 4 },
{ "source": "Plants and animals", "target": "Evapotranspiration", "value": 4 },
{ "source": "Evapotranspiration", "target": "Atmosphere", "value": 4 },
{ "source": "Freshwater", "target": "Oceans", "value": 4 },
]
}
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-sankey-circular.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="data.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:100i" rel="stylesheet">
<title>Sankey with circular links</title>
<style>
body {
font-family: 'Roboto', sans-serif;
background: #E3D4C1;
}
rect {
shape-rendering: crispEdges;
}
text {
/*text-shadow: 0 1px 0 #fff;*/
font-size: 12px;
font-family: 'Roboto', sans-serif;
}
.link {
fill: none;
}
</style>
</head>
<body>
<h1>Sankey with circular links</h1>
<p>Colours and arrows influenced by a <a href="https://www.loc.gov/resource/g4042m.ct002283/">1960 chart showing the inland freight on the Mississippi River made by the United States Army Corps of Engineers</a>.</p>
<div id="chart"></div>
<script>
var margin = { top: 200, right: 100, bottom: 120, left: 100 };
var width = 1200;
var height = 400;
let data = data2;
const nodePadding = 40;
const circularLinkGap = 2;
var sankey = d3.sankey()
.nodeWidth(1)
.nodePadding(nodePadding)
.nodePaddingRatio(0.7)
.scale(0.5)
.size([width, height])
.nodeId(function (d) {
return d.name;
})
.nodeAlign(d3.sankeyLeft)
.iterations(32);
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
var linkG = g.append("g")
.attr("class", "links")
.attr("fill", "none")
//.attr("stroke-opacity", 0.2)
.selectAll("path");
var nodeG = g.append("g")
.attr("class", "nodes")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g");
//run the Sankey + circular over the data
let sankeyData = sankey(data);
let sankeyNodes = sankeyData.nodes;
let sankeyLinks = sankeyData.links;
let depthExtent = d3.extent(sankeyNodes, function (d) { return d.depth; });
var colour = d3.scaleSequential(d3.interpolateCool)
.domain(depthExtent);
//Adjust link Y coordinates based on target/source Y positions
var node = nodeG.data(sankeyNodes)
.enter()
.append("g");
node.append("rect")
.attr("x", function (d) { return d.x0; })
.attr("y", function (d) { return d.y0; })
.attr("height", function (d) { return d.y1 - d.y0; })
.attr("width", function (d) { return d.x1 - d.x0; })
//.style("fill", function (d) { return colour(d.depth); })
.style("fill", "black")
.style("opacity", 0.5)
.style("stroke", "black")
.on("mouseover", function (d) {
let thisName = d.name;
node.selectAll("rect")
.style("opacity", function (d) {
return highlightNodes(d, thisName)
})
d3.selectAll(".sankey-link")
.style("opacity", function (l) {
return l.source.name == thisName || l.target.name == thisName ? 1 : 0.3;
})
node.selectAll("text")
.style("opacity", function (d) {
return highlightNodes(d, thisName)
})
})
.on("mouseout", function (d) {
d3.selectAll("rect").style("opacity", 0.5);
d3.selectAll(".sankey-link").style("opacity", 0.7);
d3.selectAll("text").style("opacity", 1);
})
/*node.append("text")
.attr("x", function (d) { return d.x0 - 6; })
.attr("y", function (d) { return d.y0 + ((d.y1 - d.y0) / 2); })
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(function (d) { return d.name; })
.filter(function (d) { return (d.x0 < width / 2) && (d.depth != 0); })
.attr("x", function (d) { return d.x1 + 6; })
.attr("text-anchor", "start")*/
node.append("text")
.attr("x", function (d) { return (d.x0 + d.x1) / 2; })
.attr("y", function (d) { return d.y0 - 12; })
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function (d) { return d.name; });
node.append("title")
.text(function (d) { return d.name + "\n" + (d.value); });
var link = linkG.data(sankeyLinks)
.enter()
.append("path")
.attr("class", "sankey-link")
.attr("d", sankeyPath)
.style("stroke-width", function (d) { return Math.max(1, d.width); })
.style("stroke", function (d) {
return d.circular ? "#988682" : "#988682";
})
.style("opacity", 0.7);
link.append("title")
.text(function (d) {
return d.source.name + " → " + d.target.name + "\n Index: " + (d.index);
});
//ARROWS
var arrowsG = linkG.data(sankeyLinks)
.enter()
.append("g")
.attr("class", "g-arrow")
.call(appendArrows)
function highlightNodes(node, name) {
let opacity = 0.3
if (node.name == name) {
opacity = 1;
}
node.sourceLinks.forEach(function (link) {
if (link.target.name == name) {
opacity = 1;
};
})
node.targetLinks.forEach(function (link) {
if (link.source.name == name) {
opacity = 1;
};
})
return opacity;
}
function sankeyPath(link) {
let path = ''
if (link.circular) {
path = link.circularPathData.path
} else {
var normalPath = d3
.linkHorizontal()
.source(function (d) {
let x = d.source.x0 + (d.source.x1 - d.source.x0)
let y = d.y0
return [x, y]
})
.target(function (d) {
let x = d.target.x0
let y = d.y1
return [x, y]
})
path = normalPath(link)
}
return path
}
function appendArrows(linkG) {
let arrowLength = 20;
let gapLength = 300;
let totalDashArrayLength = arrowLength + gapLength;
arrows = linkG.append("path")
.attr("d", sankeyPath)
.style("stroke-width", 1)
.style("stroke", "black")
.style("stroke-dasharray", arrowLength + "," + gapLength)
arrows.each(function (arrow) {
let thisPath = d3.select(this).node();
let parentG = d3.select(this.parentNode)
let pathLength = thisPath.getTotalLength();
let numberOfArrows = Math.ceil(pathLength / totalDashArrayLength);
//remove the last arrow head if it will overlap the target node
//+4 to take into account arrow head size
if ((((numberOfArrows - 1) * totalDashArrayLength) + (arrowLength + 5)) > pathLength) {
numberOfArrows = numberOfArrows - 1;
}
let arrowHeadData = d3.range(numberOfArrows).map(function (d, i) {
let length = (i * totalDashArrayLength) + arrowLength;
let point = thisPath.getPointAtLength(length);
let previousPoint = thisPath.getPointAtLength(length - 2);
let rotation = 0;
if (point.y == previousPoint.y) {
rotation = (point.x < previousPoint.x) ? 180 : 0;
}
else if (point.x == previousPoint.x) {
rotation = (point.y < previousPoint.y) ? -90 : 90;
}
else {
let adj = Math.abs(point.x - previousPoint.x);
let opp = Math.abs(point.y - previousPoint.y);
let angle = Math.atan(opp / adj) * (180 / Math.PI);
if (point.x < previousPoint.x) {
angle = angle + ((90 - angle) * 2)
}
if (point.y < previousPoint.y) {
rotation = -angle;
}
else {
rotation = angle;
}
};
return { x: point.x, y: point.y, rotation: rotation };
});
let arrowHeads = parentG.selectAll(".arrow-heads")
.data(arrowHeadData)
.enter()
.append("path")
.attr("d", function (d) {
return "M" + (d.x) + "," + (d.y - 2) + " "
+ "L" + (d.x + 4) + "," + (d.y) + " "
+ "L" + d.x + "," + (d.y + 2);
})
.attr("class", "arrow-head")
.attr("transform", function (d) {
return "rotate(" + d.rotation + "," + d.x + "," + d.y + ")";
})
.style("fill", "black")
});
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment