Skip to content

Instantly share code, notes, and snippets.

@trebor
Created December 3, 2017 19:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trebor/2b6354f877d8343354f5560500a0d618 to your computer and use it in GitHub Desktop.
Save trebor/2b6354f877d8343354f5560500a0d618 to your computer and use it in GitHub Desktop.
Sankey with Minard dots for different link types
license: mit
// Function that appends a path to selection that has sankey path data attached
// The path is formatted as dash array, and triangle paths to create arrows along the path
function pathArrows () {
var arrowLength = 10
var gapLength = 50
var arrowHeadSize = 4
var path = null;
function appendArrows (selection) {
let totalDashArrayLength = arrowLength + gapLength
let arrows = selection
.append('path')
.attr('d', path)
.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
if (
(numberOfArrows - 1) * totalDashArrayLength +
(arrowLength + (arrowHeadSize + 1)) >
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 - arrowHeadSize / 2) +
' ' +
'L' +
(d.x + arrowHeadSize) +
',' +
d.y +
' ' +
'L' +
d.x +
',' +
(d.y + arrowHeadSize / 2)
)
})
.attr('class', 'arrow-head')
.attr('transform', function (d) {
return 'rotate(' + d.rotation + ',' + d.x + ',' + d.y + ')'
})
.style('fill', 'black')
})
}
appendArrows.arrowLength = function (value) {
if (!arguments.length) return arrowLength
arrowLength = value
return appendArrows
}
appendArrows.gapLength = function (value) {
if (!arguments.length) return gapLength
gapLength = value
return appendArrows
}
appendArrows.arrowHeadSize = function (value) {
if (!arguments.length) return arrowHeadSize
arrowHeadSize = value
return appendArrows
}
appendArrows.path = function(pathFunction) {
if (!arguments.length) {
return path
}
else{
if (typeof pathFunction === "function") {
path = pathFunction;
return appendArrows
}
else {
path = function() { return pathFunction }
return appendArrows;
}
}
};
return appendArrows;
}
// https://github.com/tomshanley/d3-sankey-circular
// fork of https://github.com/d3/d3-sankey copyright 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'
// For a given link, return the target node's depth
function targetDepth (link) {
return link.target.depth
}
// The depth of a node when the nodeAlign (align) is set to 'left'
function left (node) {
return node.depth
}
// The depth of a node when the nodeAlign (align) is set to 'right'
function right (node, n) {
return n - 1 - node.height
}
// The depth of a node when the nodeAlign (align) is set to 'justify'
function justify (node, n) {
return node.sourceLinks.length ? node.depth : n - 1
}
// The depth of a node when the nodeAlign (align) is set to 'center'
function center (node) {
return node.targetLinks.length
? node.depth
: node.sourceLinks.length
? d3Array.min(node.sourceLinks, targetDepth) - 1
: 0
}
// returns a function, using the parameter given to the sankey setting
function constant (x) {
return function () {
return x
}
}
// sort links' breadth (ie top to bottom in a column), based on their source nodes' breadths
function ascendingSourceBreadth (a, b) {
return ascendingBreadth(a.source, b.source) || a.index - b.index
}
// sort links' breadth (ie top to bottom in a column), based on their target nodes' breadths
function ascendingTargetBreadth (a, b) {
return ascendingBreadth(a.target, b.target) || a.index - b.index
}
// sort nodes' breadth (ie top to bottom in a column)
// if both nodes have circular links, or both don't have circular links, then sort by the top (y0) of the node
// else push nodes that have top circular links to the top, and nodes that have bottom circular links to the bottom
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
}
}
}
// return the value of a node or link
function value (d) {
return d.value
}
// return the vertical center of a node
function nodeCenter (node) {
return (node.y0 + node.y1) / 2
}
// return the vertical center of a link's source node
function linkSourceCenter (link) {
return nodeCenter(link.source)
}
// return the vertical center of a link's target node
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
} */
// Return the default value for ID for node, d.index
function defaultId (d) {
return d.index
}
// Return the default object the graph's nodes, graph.nodes
function defaultNodes (graph) {
return graph.nodes
}
// Return the default object the graph's nodes, graph.links
function defaultLinks (graph) {
return graph.links
}
// Return the node from the collection that matches the provided ID, or throw an error if no match
function find (nodeById, id) {
var node = nodeById.get(id)
if (!node) throw new Error('missing: ' + id)
return node
}
// The main sankey functions
// Some constants for circular link calculations
const verticalMargin = 25;
const baseRadius = 10;
const scale = 0.3; //Possibly let user control this, although anything over 0.5 starts to get too cramped
var sankey = function () {
// Set the default values
var x0 = 0,
y0 = 0,
x1 = 1,
y1 = 1, // extent
dx = 24, // nodeWidth
py, // nodePadding, for vertical postioning
id = defaultId,
align = justify,
nodes = defaultNodes,
links = defaultLinks,
iterations = 32,
circularLinkGap = 2,
paddingRatio,
firstNode = undefined;
function sankey () {
var graph = {
nodes: nodes.apply(null, arguments),
links: links.apply(null, arguments)
}
// Process the graph's nodes and links, setting their positions
// 1. Associate the nodes with their respective links, and vice versa
computeNodeLinks(graph)
// 2. Determine which links result in a circular path in the graph
identifyCircles(graph)
// 3. Determine how the circular links will be drawn,
// either travelling back above the main chart ("top")
// or below the main chart ("bottom")
selectCircularLinkTypes(graph)
// 4. Calculate the nodes' values, based on the values of the incoming and outgoing links
computeNodeValues(graph)
// 5. Calculate the nodes' depth based on the incoming and outgoing links
// Sets the nodes':
// - depth: the depth in the graph
// - column: the depth (0, 1, 2, etc), as is relates to visual position from left to right
// - x0, x1: the x coordinates, as is relates to visual position from left to right
computeNodeDepths(graph)
// 6. Calculate the nodes' and links' vertical position within their respective column
// Also readjusts sankey size if circular links are needed, and node x's
computeNodeBreadths(graph, iterations)
computeLinkBreadths(graph)
// 7. Sort links per node, based on the links' source/target nodes' breadths
// 8. Adjust nodes that overlap links that span 2+ columns
let linkSortingIterations = 4; //Possibly let user control this number, like the iterations over node placement
for (var iteration = 0; iteration < linkSortingIterations; iteration++) {
sortSourceLinks(graph, y1)
sortTargetLinks(graph, y1)
resolveNodeLinkOverlaps(graph, y0, y1)
sortSourceLinks(graph, y1)
sortTargetLinks(graph, y1)
}
// 9. Calculate visually appealling path for the circular paths, and create the "d" string
addCircularPathData(graph, circularLinkGap, y1)
return graph
} // end of sankey function
// TODO - update this function to take into account circular changes
/*sankey.update = function (graph) {
computeLinkBreadths(graph)
return graph
}*/
// Set the sankey parameters
// nodeID, nodeAlign, nodeWidth, nodePadding, nodes, links, size, extent, iterations, nodePaddingRatio, circularLinkGap
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.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.circularLinkGap = function (_) {
return arguments.length
? ((circularLinkGap = +_), sankey)
: circularLinkGap
}
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
}
})
})
}
// Update the x0, y0, x1 and y1 for the sankey, to allow space for any circular links
function scaleSankeySize (graph) {
let totalTopLinksWidth = 0,
totalBottomLinksWidth = 0,
totalRightLinksWidth = 0,
totalLeftLinksWidth = 0
let maxColumn = d3.max(graph.nodes, function (node) {
return node.column
})
graph.links.forEach(function (link) {
if (link.circular) {
if (link.circularLinkType == 'top') {
totalTopLinksWidth = totalTopLinksWidth + link.width
} else {
totalBottomLinksWidth = totalBottomLinksWidth + link.width
}
if (link.target.column == 0) {
totalRightLinksWidth = totalRightLinksWidth + link.width
}
if (link.source.column == maxColumn) {
totalLeftLinksWidth = totalLeftLinksWidth + link.width
}
}
})
//account for radius of curves and padding between links
totalTopLinksWidth = totalTopLinksWidth > 0 ? totalTopLinksWidth + verticalMargin + baseRadius : totalTopLinksWidth;
totalBottomLinksWidth = totalBottomLinksWidth > 0 ? totalBottomLinksWidth + verticalMargin + baseRadius : totalBottomLinksWidth;
totalRightLinksWidth = totalRightLinksWidth > 0 ? totalRightLinksWidth + verticalMargin + baseRadius : totalRightLinksWidth;
totalLeftLinksWidth = totalLeftLinksWidth > 0 ? totalLeftLinksWidth + verticalMargin + baseRadius : totalLeftLinksWidth;
let currentWidth = x1 - x0;
let currentHeight = y1 - y0;
let newWidth = currentWidth + totalRightLinksWidth + totalLeftLinksWidth;
let newHeight = currentHeight + totalTopLinksWidth + totalBottomLinksWidth;
let scaleX = currentWidth / newWidth;
let scaleY = currentHeight / newHeight;
x0 = (x0 * scaleX) + (totalRightLinksWidth);
x1 = x1 * scaleX;
y0 = (y0 * scaleY) + (totalTopLinksWidth);
y1 = y1 * scaleY;
graph.nodes.forEach(function (node) {
node.x0 = x0 + (node.column * (((x1 - x0) / maxColumn) - dx))
node.x1 = node.x0 + dx
})
return scaleY;
}
// Iteratively assign the depth 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)
}
})
})
}
// assign column numbers, and get max value
graph.nodes.forEach(function (node) {
node.column = Math.floor(align.call(null, node, x))
})
}
// Assign nodes' breadths, and then shift nodes that overlap (resolveCollisions)
function computeNodeBreadths (graph) {
var columns = d3Collection
.nest()
.key(function (d) {
return d.column
})
.sortKeys(d3Array.ascending)
.entries(graph.nodes)
.map(function (d) {
return d.values
})
initializeNodeBreadth()
resolveCollisions()
for (var alpha = 1, n = iterations; n > 0; --n) {
relaxLeftAndRight((alpha *= 0.99))
resolveCollisions()
}
function initializeNodeBreadth () {
//override py if nodePadding has been set
if (paddingRatio) {
let padding = Infinity
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)
})
//calculate the widths of the links
ky = ky * scale
graph.links.forEach(function (link) {
link.width = link.value * ky
})
//determine how much to scale down the chart, based on circular links
let ratio = scaleSankeySize(graph);
//re-calculate widths
ky = ky * ratio
graph.links.forEach(function (link) {
link.width = link.value * ky
})
columns.forEach(function (nodes) {
var nodesLength = nodes.length
nodes.forEach(function (node, i) {
if (node.partOfCycle) {
if (numberOfNonSelfLinkingCycles(node) == 0) {
node.y0 = y1 / 2 + i
node.y1 = node.y0 + node.value * ky
} else 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.y0 = (y1 - y0) / 2 - nodesLength / 2 + i
node.y1 = node.y0 + node.value * ky
}
})
})
}
// For each node in each column, check the node's vertical position in relation to its targets and sources vertical position
// and shift up/down to be closer to the vertical middle of those targets and sources
function relaxLeftAndRight (alpha) {
let columnsLength = columns.length
columns.forEach(function (nodes, i) {
let n = nodes.length
let depth = nodes[0].depth
nodes.forEach(function (node) {
// check the node is not an orphan
if (node.sourceLinks.length || node.targetLinks.length) {
if (node.partOfCycle && numberOfNonSelfLinkingCycles(node) > 0) {
// console.log(node.name + " " + node.y0)
} 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
node.y0 += dy
node.y1 += dy
}
}
})
})
}
// For each column, check if nodes are overlapping, and if so, shift up/down
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
}
}
})
}
}
// Assign the links y0 and y1 based on source/target nodes position,
// plus the link's relative position to other links to the same node
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
// portion of code to detect circular links based on Colin Fergus' bl.ock https://gist.github.com/cfergus/3956043
// Identify circles in the link objects
function identifyCircles (graph) {
var addedLinks = []
var circularLinkID = 0
graph.links.forEach(function (link) {
//console.log("========================================================")
//console.log("test: " + link.index + " from: " + link.source.name + " to: " + link.target.name)
if (createsCycle(link.source, link.target, addedLinks)) {
//console.log("CIRCLE")
link.circular = true
link.circularLinkID = circularLinkID
circularLinkID = circularLinkID + 1
} else {
//console.log("NORMAL")
link.circular = false
addedLinks.push(link)
}
})
}
// Assign a circular link type (top or bottom), based on:
// - if the source/target node already has circular links, then use the same type
// - if not, choose the type with fewer links
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'
}
//update the count of links per 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
}
})
}
})
//correct self-linking links to be same direction as node
graph.links.forEach(function (link) {
if (link.circular) {
//if both source and target node are same type, then link should have same type
if (link.source.circularLinkType == link.target.circularLinkType) {
link.circularLinkType = link.source.circularLinkType
}
//if link is selflinking, then link should have same type as node
if (selfLinking(link)) {
link.circularLinkType = link.source.circularLinkType
}
}
})
}
// Checks if link creates a cycle
function createsCycle (originalSource, nodeToCheck, graph) {
// Check for self linking nodes
if (originalSource.name == nodeToCheck.name) {
//console.log("TRUE self linking")
return true
}
if (graph.length == 0) {
//console.log("FALSE 0 length graph")
return false
}
var nextLinks = findLinksOutward(nodeToCheck, graph)
// leaf node check
if (nextLinks.length == 0) {
//console.log("FALSE 0 length outward graph to check: " + nodeToCheck.name)
return false;
}
else {
//console.log(nextLinks)
}
// cycle check
for (var i = 0; i < nextLinks.length; i++) {
var nextLink = nextLinks[i]
if (nextLink.target === originalSource) {
//console.log("TRUE found the original source node in the outward tree: " + nextLink.index)
return true
}
// Recurse
if (createsCycle(originalSource, nextLink.target, graph)) {
//console.log("TRUE found the original source node in the outward tree")
return true
}
}
// Exhausted all links
//console.log("FALSE didn't find the original source node in the outward tree: " + nodeToCheck.name)
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
}
// 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)
}
// Check if two circular links potentially overlap
function circularLinksCross (link1, link2) {
if (link1.source.column < link2.target.column) {
return false
} else if (link1.target.column > link2.source.column) {
return false
} else {
return true
}
}
// Return the number of circular links for node, not including self linking links
function numberOfNonSelfLinkingCycles (node) {
let sourceCount = 0
node.sourceLinks.forEach(function (l) {
sourceCount = l.circular && !selfLinking(l)
? sourceCount + 1
: sourceCount
})
let targetCount = 0
node.targetLinks.forEach(function (l) {
targetCount = l.circular && !selfLinking(l)
? targetCount + 1
: targetCount
})
return sourceCount + targetCount
}
// Check if a circular link is the only circular link for both its source and target node
function onlyCircularLink (link) {
let nodeSourceLinks = link.source.sourceLinks
let sourceCount = 0
nodeSourceLinks.forEach(function (l) {
sourceCount = l.circular ? sourceCount + 1 : sourceCount
})
let nodeTargetLinks = link.target.targetLinks
let targetCount = 0
nodeTargetLinks.forEach(function (l) {
targetCount = l.circular ? targetCount + 1 : targetCount
})
if (sourceCount > 1 || targetCount > 1) {
return false
} else {
return true
}
}
// creates vertical buffer values per set of top/bottom links
function calcVerticalBuffer (links, circularLinkGap) {
links.sort(sortLinkColumnAscending)
links.forEach(function (link, i) {
let buffer = 0
if (selfLinking(link) && onlyCircularLink(link)) {
link.circularPathData.verticalBuffer = buffer + link.width / 2
} else {
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, circularLinkGap, y1) {
//let baseRadius = 10
let buffer = 5
//let verticalMargin = 25
let minY = d3.min(graph.links, function (link) {
return link.source.y0
})
// 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, circularLinkGap)
let bottomLinks = graph.links.filter(function (l) {
return l.circularLinkType == 'bottom'
})
bottomLinks = calcVerticalBuffer(bottomLinks, circularLinkGap)
// add the base data for each link
graph.links.forEach(function (link) {
if (link.circular) {
link.circularPathData.arcRadius = link.width + baseRadius
link.circularPathData.leftNodeBuffer = buffer
link.circularPathData.rightNodeBuffer = buffer
link.circularPathData.sourceWidth = link.source.x1 - link.source.x0
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
// for self linking paths, and that the only circular link in/out of that node
if (selfLinking(link) && onlyCircularLink(link)) {
link.circularPathData.leftSmallArcRadius = baseRadius + link.width / 2
link.circularPathData.leftLargeArcRadius = baseRadius + link.width / 2
link.circularPathData.rightSmallArcRadius = baseRadius + link.width / 2
link.circularPathData.rightLargeArcRadius = baseRadius + link.width / 2
if (link.circularLinkType == 'bottom') {
link.circularPathData.verticalFullExtent = link.source.y1 + verticalMargin + 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 = link.source.y0 - verticalMargin - link.circularPathData.verticalBuffer
link.circularPathData.verticalLeftInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.leftLargeArcRadius
link.circularPathData.verticalRightInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.rightLargeArcRadius
}
} else {
// else calculate normally
// add left extent coordinates, based on links with same source column and circularLink type
let thisColumn = link.source.column
let thisCircularLinkType = link.circularLinkType
let sameColumnLinks = graph.links.filter(function (l) {
return (
l.source.column == thisColumn &&
l.circularLinkType == thisCircularLinkType
)
})
if (link.circularLinkType == 'bottom') {
sameColumnLinks.sort(sortLinkSourceYDescending)
} else {
sameColumnLinks.sort(sortLinkSourceYAscending)
}
let radiusOffset = 0
sameColumnLinks.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 column and circularLink type
thisColumn = link.target.column
sameColumnLinks = graph.links.filter(function (l) {
return (
l.target.column == thisColumn &&
l.circularLinkType == thisCircularLinkType
)
})
if (link.circularLinkType == 'bottom') {
sameColumnLinks.sort(sortLinkTargetYDescending)
} else {
sameColumnLinks.sort(sortLinkTargetYAscending)
}
radiusOffset = 0
sameColumnLinks.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
})
// bottom links
if (link.circularLinkType == 'bottom') {
link.circularPathData.verticalFullExtent = y1 + verticalMargin + 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 - verticalMargin - link.circularPathData.verticalBuffer
link.circularPathData.verticalLeftInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.leftLargeArcRadius
link.circularPathData.verticalRightInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.rightLargeArcRadius
}
}
// 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
}
if (link.circular) {
link.path = createCircularPathString(link)
} 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]
})
link.path = normalPath(link)
}
})
}
// 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 columns
// if the same, then use Y position of the source node
function sortLinkColumnAscending (link1, link2) {
if (linkColumnDistance(link1) == linkColumnDistance(link2)) {
return link1.circularLinkType == 'bottom'
? sortLinkSourceYDescending(link1, link2)
: sortLinkSourceYAscending(link1, link2)
} else {
return linkColumnDistance(link2) - linkColumnDistance(link1)
}
}
// sort ascending links by their source vertical position, y0
function sortLinkSourceYAscending (link1, link2) {
return link1.y0 - link2.y0
}
// sort descending links by their source vertical position, y0
function sortLinkSourceYDescending (link1, link2) {
return link2.y0 - link1.y0
}
// sort ascending links by their target vertical position, y1
function sortLinkTargetYAscending (link1, link2) {
return link1.y1 - link2.y1
}
// sort descending links by their target vertical position, 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' column
function linkColumnDistance (link) {
return link.target.column - link.source.column
}
// 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
}
// 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
function linkPerpendicularYToLinkSource (longerLink, shorterLink) {
// 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
}
// 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
function linkPerpendicularYToLinkTarget (longerLink, shorterLink) {
// 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
}
// Move any nodes that overlap links which span 2+ columns
function resolveNodeLinkOverlaps (graph, y0, y1) {
graph.links.forEach(function (link) {
if (link.circular) {
return
}
if (link.target.column - link.source.column > 1) {
let columnToTest = link.source.column + 1
let maxColumnToTest = link.target.column - 1
let i = 1
let numberOfColumnsToTest = maxColumnToTest - columnToTest + 1
for (
columnToTest, (i = 1);
columnToTest <= maxColumnToTest;
columnToTest++, i++
) {
graph.nodes.forEach(function (node) {
if (node.column == columnToTest) {
let t = i / (numberOfColumnsToTest + 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)
let py_t =
B0_t * link.y0 +
B1_t * link.y0 +
B2_t * link.y1 +
B3_t * link.y1
let linkY0AtColumn = py_t - (link.width / 2)
let linkY1AtColumn = py_t + (link.width / 2)
// If top of link overlaps node, push node up
if (linkY0AtColumn > node.y0 && linkY0AtColumn < node.y1) {
let dy = node.y1 - linkY0AtColumn + 10
dy = node.circularLinkType == 'bottom' ? dy : -dy
node = adjustNodeHeight(node, dy, y0, y1)
// check if other nodes need to move up too
graph.nodes.forEach(function (otherNode) {
// don't need to check itself or nodes at different columns
if (
otherNode.name == node.name ||
otherNode.column != node.column
) {
return
}
if (nodesOverlap(node, otherNode)) {
adjustNodeHeight(otherNode, dy, y0, y1)
}
})
} else if (linkY1AtColumn > node.y0 && linkY1AtColumn < node.y1) {
// If bottom of link overlaps node, push node down
let dy = linkY1AtColumn - node.y0 + 10
node = adjustNodeHeight(node, dy, y0, y1)
// check if other nodes need to move down too
graph.nodes.forEach(function (otherNode) {
// don't need to check itself or nodes at different columns
if (
otherNode.name == node.name ||
otherNode.column != node.column
) {
return
}
if (otherNode.y0 < node.y1 && otherNode.y1 > node.y1) {
adjustNodeHeight(otherNode, dy, y0, y1)
}
})
} else if (linkY0AtColumn < node.y0 && linkY1AtColumn > node.y1) {
// if link completely overlaps node
let dy = linkY1AtColumn - node.y0 + 10
node = adjustNodeHeight(node, dy, y0, y1)
graph.nodes.forEach(function (otherNode) {
// don't need to check itself or nodes at different columns
if (
otherNode.name == node.name ||
otherNode.column != node.column
) {
return
}
if (otherNode.y0 < node.y1 && otherNode.y1 > node.y1) {
adjustNodeHeight(otherNode, dy, y0, y1)
}
})
}
}
})
}
}
})
}
// check if two nodes overlap
function nodesOverlap (nodeA, nodeB) {
// test if nodeA top partially overlaps nodeB
if (nodeA.y0 > nodeB.y0 && nodeA.y0 < nodeB.y1) {
return true
} else if (nodeA.y1 > nodeB.y0 && nodeA.y1 < nodeB.y1) {
// test if nodeA bottom partially overlaps nodeB
return true
} else if (nodeA.y0 < nodeB.y0 && nodeA.y1 > nodeB.y1) {
// test if nodeA covers nodeB
return true
} else {
return false
}
}
// update a node, and its associated links, vertical positions (y0, y1)
function adjustNodeHeight (node, dy, sankeyY0, sankeyY1) {
if ((node.y0 + dy >= sankeyY0) && (node.y1 + dy <= sankeyY1)) {
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
}
// sort and set the links' y0 for each node
function sortSourceLinks (graph, y1) {
graph.nodes.forEach(function (node) {
// move any nodes up which are off the bottom
if (node.y + (node.y1 - node.y0) > y1) {
node.y = node.y - (node.y + (node.y1 - node.y0) - y1)
}
let nodesSourceLinks = graph.links.filter(function (l) {
return l.source.name == node.name
})
let nodeSourceLinksLength = nodesSourceLinks.length
// if more than 1 link then sort
if (nodeSourceLinksLength > 1) {
nodesSourceLinks.sort(function (link1, link2) {
// if both are not circular...
if (!link1.circular && !link2.circular) {
// if the target nodes are the same column, then sort by the link's target y
if (link1.target.column == link2.target.column) {
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.column > link2.target.column) {
let link2Adj = linkPerpendicularYToLinkTarget(link2, link1)
return link1.y1 - link2Adj
}
if (link2.target.column > link1.target.column) {
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 column, then sort by the target's y
if (link1.target.column === link2.target.column) {
return link1.target.y1 - link2.target.y1
} else {
// ...and they connect to different column targets, then sort by how far back they
return link2.target.column - link1.target.column
}
} 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 column, then sort by the target's y
if (link1.target.column === link2.target.column) {
return link2.target.y1 - link1.target.y1
} else {
// ...and they connect to different column targets, then sort by how far back they
return link1.target.column - link2.target.column
}
} 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
})
// correct any circular bottom links so they are at the bottom of the node
nodesSourceLinks.forEach(function (link, i) {
if (link.circularLinkType == 'bottom') {
let j = i + 1
let offsetFromBottom = 0
// sum the widths of any links that are below this link
for (j; j < nodeSourceLinksLength; j++) {
offsetFromBottom = offsetFromBottom + nodesSourceLinks[j].width
}
link.y0 = node.y1 - offsetFromBottom - link.width / 2
}
})
})
}
// sort and set the links' y1 for each node
function sortTargetLinks (graph, y1) {
graph.nodes.forEach(function (node) {
let nodesTargetLinks = graph.links.filter(function (l) {
return l.target.name == node.name
})
let nodesTargetLinksLength = nodesTargetLinks.length
if (nodesTargetLinksLength > 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.column == link2.source.column) {
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 column)
if (link2.source.column < link1.source.column) {
let link2Adj = linkPerpendicularYToLinkSource(link2, link1)
return link1.y0 - link2Adj
}
if (link1.source.column < link2.source.column) {
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 column, then sort by the target's y
if (link1.source.column === link2.source.column) {
return link1.source.y1 - link2.source.y1
} else {
// ...and they connect to different column targets, then sort by how far back they
return link1.source.column - link2.source.column
}
} 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 column, then sort by the target's y
if (link1.source.column === link2.source.column) {
return link1.source.y1 - link2.source.y1
} else {
// ...and they connect to different column targets, then sort by how far back they
return link2.source.column - link1.source.column
}
} 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
})
// correct any circular bottom links so they are at the bottom of the node
nodesTargetLinks.forEach(function (link, i) {
if (link.circularLinkType == 'bottom') {
let j = i + 1
let offsetFromBottom = 0
// sum the widths of any links that are below this link
for (j; j < nodesTargetLinksLength; j++) {
offsetFromBottom = offsetFromBottom + nodesTargetLinks[j].width
}
link.y1 = node.y1 - offsetFromBottom - link.width / 2
}
})
})
}
// test if links both slope up, or both slope down
function sameInclines (link1, link2) {
return incline(link1) == incline(link2)
}
// returns the slope of a link, from source to target
// up => slopes up from source to target
// down => slopes down from source to target
function incline (link) {
return link.y0 - link.y1 > 0 ? 'up' : 'down'
}
// check if link is self linking, ie links a node to the same node
function selfLinking (link) {
return link.source.name == link.target.name
}
/// ////////////////////////////////////////////////////////////////////////////
exports.sankeyCircular = sankey
exports.sankeyCenter = center
exports.sankeyLeft = left
exports.sankeyRight = right
exports.sankeyJustify = justify
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 },
]
}
//https://www.ucl.ac.uk/bartlett/sustainable/news/2017/jun/global-paper-recycling-can-be-improved-according-new-research-ucl
let data4 = {
"nodes": [
{ "name": "Non-fibrous" },
{ "name": "Wood" },
{ "name": "Other fibres" },
{ "name": "Mechanical pulp" },
{ "name": "Chemical pulp" },
{ "name": "Recycled pulp" },
//{ "name": "Paper for recycling" },
{ "name": "Mill waste" },
{ "name": "Newsprint" },
{ "name": "Printing and writing" },
{ "name": "Sanitary and household" },
{ "name": "Packaging" },
{ "name": "Other" },
{ "name": "Use" },
{ "name": "To stock" },
{ "name": "Energy recovery municipal" },
{ "name": "Incineration municipal" },
{ "name": "Landfill" },
{ "name": "Non-energy recovery"},
{ "name": "Energy recovery on site"}
],
"links": [
{ "source": "Non-fibrous", "target": "Newsprint", "value": 4 },
{ "source": "Non-fibrous", "target": "Printing and writing", "value": 40 },
{ "source": "Non-fibrous", "target": "Packaging", "value": 20 },
{ "source": "Non-fibrous", "target": "Other", "value": 4 },
{ "source": "Non-fibrous", "target": "Recycled pulp", "value": 2 },
{ "source": "Wood", "target": "Mechanical pulp", "value": 35 },
{ "source": "Wood", "target": "Chemical pulp", "value": 279 },
{ "source": "Other fibres", "target": "Chemical pulp", "value": 4 },
{ "source": "Mechanical pulp", "target": "Newsprint", "value": 3 },
{ "source": "Mechanical pulp", "target": "Packaging", "value": 23 },
{ "source": "Mechanical pulp", "target": "Recycled pulp", "value": 2 },
{ "source": "Mechanical pulp", "target": "Mill waste", "value": 3 },
{ "source": "Chemical pulp", "target": "Printing and writing", "value": 50 },
{ "source": "Chemical pulp", "target": "Sanitary and household", "value": 20 },
{ "source": "Chemical pulp", "target": "Packaging", "value": 40 },
{ "source": "Chemical pulp", "target": "Other", "value": 9 },
{ "source": "Chemical pulp", "target": "Recycled pulp", "value": 3 },
{ "source": "Chemical pulp", "target": "Mill waste", "value": 162 },
{ "source": "Recycled pulp", "target": "Newsprint", "value": 25 },
{ "source": "Recycled pulp", "target": "Printing and writing", "value": 5 },
{ "source": "Recycled pulp", "target": "Sanitary and household", "value": 5 },
{ "source": "Recycled pulp", "target": "Packaging", "value": 100 },
{ "source": "Recycled pulp", "target": "Other", "value": 5 },
{ "source": "Recycled pulp", "target": "Mill waste", "value": 41 },
//{ "source": "Recycled pulp", "target": "Paper for recycling", "value": 3 },
{ "source": "Newsprint", "target": "Use", "value": 31 },
{ "source": "Printing and writing", "target": "Use", "value": 106 },
{ "source": "Sanitary and household", "target": "Use", "value": 30 },
{ "source": "Packaging", "target": "Use", "value": 214 },
{ "source": "Other", "target": "Use", "value": 18 },
{ "source": "Use", "target": "To stock", "value": 36 },
{ "source": "Use", "target": "Energy recovery municipal", "value": 20 },
{ "source": "Use", "target": "Incineration municipal", "value": 14 },
{ "source": "Use", "target": "Landfill", "value": 132 },
{ "source": "Use", "target": "Non-energy recovery", "value": 3 },
{ "source": "Use", "target": "Recycled pulp", "value": 194 },
{ "source": "Mill waste", "target": "Landfill", "value": 22 },
{ "source": "Mill waste", "target": "Non-energy recovery", "value": 26 },
{ "source": "Mill waste", "target": "Energy recovery on site", "value": 158 },
//{ "source": "Paper for recycling", "target": "Recycled pulp", "value": 215 },
]
}
//to test self-linking nodes
let data5 = {
"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": 10, "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" },
{ "source": "process11", "target": "process11", "value": 5, "optimal": "yes" },
{ "source": "finishA", "target": "finishA", "value": 15, "optimal": "yes" },
{ "source": "finishB", "target": "finishB", "value": 15, "optimal": "yes" },
{ "source": "process5", "target": "process5", "value": 10, "optimal": "yes" },
{ "source": "finishB", "target": "process14", "value": 5, "optimal": "yes" }
]
};
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-sankey-circular.js"></script>
<script src="d3-path-arrows.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="example-data.js"></script>
<link href="https://fonts.googleapis.com/css?family=Quintessential" rel="stylesheet">
<title>Sankey with circular links</title>
<style>
body {
font-family: 'Quintessential', sans-serif;
background: #E0E4D5
}
rect {
shape-rendering: crispEdges;
}
text {
font-size: 12px;
font-family: Quintessential;
}
.link {
fill: none;
}
</style>
</head>
<body>
<h1>Sankey with circular links</h1>
<div id="chart"></div>
<script>
var margin = { top: 10, right: 10, bottom: 10, left: 120 };
var width = 1000;
var height = 700;
let data = data5;
const nodePadding = 40;
const circularLinkGap = 2;
var sankey = d3.sankeyCircular()
.nodeWidth(0)
.nodePadding(nodePadding)
.nodePaddingRatio(0.1)
.size([width, height])
.nodeId(function (d) {
return d.name;
})
.nodeAlign(d3.sankeyRight)
.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")
.selectAll("path");
var linkLabels = g.append("g")
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 nodeColour = d3.scaleSequential(d3.interpolateCool)
.domain([0,width]);
//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", "#C89776")
//.style("fill", "black")
//.style("stroke", "black")
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("g")
link.append("path")
.attr("class", "sankey-link")
.attr("d", function(link){
return link.path;
})
.style("stroke-width", function (d) { return d.width; })
.style("opacity", 1)
.style("stroke", "#9f987d")
.style("stroke-dasharray", function(d){
return d.circular ? "1,4" : ""
})
link.append("path")
.attr("class", "sankey-link")
.attr("d", function(link){
return link.path;
})
.style("stroke-width", function (d) { return d.width - 2; })
.style("opacity", 1)
.style("stroke", function(d){
return d.circular ? "DFBD80" : "#BAB5A1"
})
.each(addMinardLabels);
let arrows = pathArrows()
.arrowLength(10)
.gapLength(80)
.arrowHeadSize(4)
.path(function(link){ return link.path })
var arrowsG = linkG.data(sankeyLinks)
.enter()
.append("g")
.attr("class", "g-arrow")
.call(arrows)
link.append("title")
.text(function (d) {
return d.source.name + " → " + d.target.name + "\n Index: " + (d.index);
});
function addMinardLabels (link) {
const gap = 90;
let label = link.value
var linkLength = this.getTotalLength();
let n = Math.floor(linkLength/gap);
for (var i = 0; i < n; i++) {
let thisLength = (i * gap) + 45
let position = this.getPointAtLength(thisLength)
let positionPlueOne = this.getPointAtLength(thisLength + 2);
let adj = positionPlueOne.x - position.x;
let opp = position.y - positionPlueOne.y;
let angle = Math.atan(opp/adj) * (180 / Math.PI );
let rotation = 270 - angle;
linkLabels.append("text")
.attr("class", "link-label")
.text(label)
.attr("x", position.x)
.attr("y", position.y)
.attr("dy", "0.35em")
.style("fill", "black")
.style("text-anchor", "middle")
.attr("transform", "rotate(" + rotation + ","+ position.x + "," + position.y + ")")
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment