Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active June 7, 2019 22:21
Show Gist options
  • Save tomshanley/be24e5890516516d147f79cbb0fbbda4 to your computer and use it in GitHub Desktop.
Save tomshanley/be24e5890516516d147f79cbb0fbbda4 to your computer and use it in GitHub Desktop.
Sankey with transition
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
}
function getNodeID (node, id) {
return id(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
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, id)
// 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)
// 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, id)
// 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, id)
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, id)
sortTargetLinks(graph, y1, id)
resolveNodeLinkOverlaps(graph, y0, y1, id)
sortSourceLinks(graph, y1, id)
sortTargetLinks(graph, y1, id)
}
// 8.1 Adjust node and link positions back to fill height of chart area if compressed
fillHeight(graph, y0, y1)
// 9. Calculate visually appealling path for the circular paths, and create the "d" string
addCircularPathData(graph, circularLinkGap, y1, id)
return graph
} // end of sankey function
function getNewValue(link, links) {
var newValue = 0
links.forEach(function(l){
if (l.source == link.source.name && l.target == link.target.name) {
newValue = l.value;
return
}
})
return newValue;
}
// TODO - update this function to take into account circular changes
sankey.updateValues = function (currentGraph, newLinks) {
//assume first node exists
var ratio = (currentGraph.nodes[0].y1 - currentGraph.nodes[0].y0) / currentGraph.nodes[0].value;
//Store previous values and match link values to current links
currentGraph.links.forEach(function(link){
link.previousValue = link.value
link.previousWidth = link.width
link.previousPath = link.path
link.value = getNewValue(link, newLinks)
link.width = link.previousValue == link.value ? link.width : link.value * ratio
})
//Sum link values per node
currentGraph.nodes.forEach(function (node) {
node.previousValue = node.value;
node.value = Math.max(
d3Array.sum(node.sourceLinks, value),
d3Array.sum(node.targetLinks, value)
)
//console.log(node.previousValue + " " + node.value)
})
//Update node Y0, Y1, value, height. Use Current value to height ratio should be used for creating the new heights. Shift any over laps slightly, but keep same order top to bottom
currentGraph.nodes.forEach(function (node) {
node.previousY0 = node.y0
node.previousY1 = node.y1
//needs to run for every
//if (node.previousValue != node.value) {
var changeHeight = ratio * (node.value - node.previousValue)
node.y0 = node.y0 - (changeHeight/2)
node.y1 = node.y1 + (changeHeight/2)
//resolve overlaps
//TODO
//Update links' y0 and y1 for each node
node.sourceLinks.sort(function(a, b){
return a.y0 - b.y0
})
var y0offset = 0
node.sourceLinks.forEach(function (link) {
link.y0 = y0offset + node.y0 + (link.width / 2)
y0offset = y0offset + link.width
})
node.targetLinks.sort(function(a, b){
return a.y1 - b.y1
})
var y1offset = 0
node.targetLinks.forEach(function (link) {
link.y1 = y1offset + node.y0 + (link.width / 2)
y1offset = y1offset + link.width
})
//}
})
//Recalculate link paths d
addCircularPathData(currentGraph, circularLinkGap, y1, id)
//Return graph
return currentGraph
}
// 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
}
})
})
}
function getCircleMargins (graph) {
let totalTopLinksWidth = 0,
totalBottomLinksWidth = 0,
totalRightLinksWidth = 0,
totalLeftLinksWidth = 0
let maxColumn = d3Array.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) {
totalLeftLinksWidth = totalLeftLinksWidth + link.width
}
if (link.source.column == maxColumn) {
totalRightLinksWidth = totalRightLinksWidth + 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;
return { "top": totalTopLinksWidth, "bottom": totalBottomLinksWidth, "left": totalLeftLinksWidth, "right": totalRightLinksWidth }
}
// Update the x0, y0, x1 and y1 for the sankey, to allow space for any circular links
function scaleSankeySize (graph, margin) {
let maxColumn = d3Array.max(graph.nodes, function (node) {
return node.column
})
let currentWidth = x1 - x0;
let currentHeight = y1 - y0;
let newWidth = currentWidth + margin.right + margin.left;
let newHeight = currentHeight + margin.top + margin.bottom;
let scaleX = currentWidth / newWidth;
let scaleY = currentHeight / newHeight;
x0 = (x0 * scaleX) + (margin.left);
x1 = margin.right == 0 ? x1 : x1 * scaleX;
y0 = (y0 * scaleY) + (margin.top);
y1 = y1 * scaleY;
graph.nodes.forEach(function (node) {
node.x0 = x0 + (node.column * ((x1 - x0 - dx) / maxColumn))
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, iterations, id) {
var columns = d3Collection
.nest()
.key(function (d) {
return d.column
})
.sortKeys(d3Array.ascending)
.entries(graph.nodes)
.map(function (d) {
return d.values
})
initializeNodeBreadth(id)
resolveCollisions()
for (var alpha = 1, n = iterations; n > 0; --n) {
relaxLeftAndRight((alpha *= 0.99), id)
resolveCollisions()
}
function initializeNodeBreadth (id) {
//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 margin = getCircleMargins(graph)
let ratio = scaleSankeySize(graph, margin);
//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.depth == (columns.length - 1) && nodesLength == 1) {
node.y0 = y1 / 2 - (node.value * ky)
node.y1 = node.y0 + node.value * ky
} else if (node.depth == 0 && nodesLength == 1) {
node.y0 = y1 / 2 - (node.value * ky)
node.y1 = node.y0 + node.value * ky
} else if (node.partOfCycle) {
if (numberOfNonSelfLinkingCycles(node, id) == 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 {
if (margin.top == 0 || margin.bottom == 0) {
node.y0 = ((y1 - y0) / nodesLength) * 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, id) {
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, id) > 0) {
} 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, id) {
var addedLinks = []
var circularLinkID = 0
graph.links.forEach(function (link) {
if (createsCycle(link.source, link.target, addedLinks, id)) {
link.circular = true
link.circularLinkID = circularLinkID
circularLinkID = circularLinkID + 1
} else {
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, id) {
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 (getNodeID(node, id) == getNodeID(link.source, id) || getNodeID(node, id) == getNodeID(link.target, id)) {
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, id)) {
link.circularLinkType = link.source.circularLinkType
}
}
})
}
// Checks if link creates a cycle
function createsCycle (originalSource, nodeToCheck, graph, id) {
// Check for self linking nodes
if (getNodeID(originalSource, id) == getNodeID(nodeToCheck, id)) {
return true
}
if (graph.length == 0) {
return false
}
var nextLinks = findLinksOutward(nodeToCheck, graph)
// leaf node check
if (nextLinks.length == 0) {
return false
}
else {
}
// 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, id)) {
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
}
// 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, id) {
let sourceCount = 0
node.sourceLinks.forEach(function (l) {
sourceCount = l.circular && !selfLinking(l, id)
? sourceCount + 1
: sourceCount
})
let targetCount = 0
node.targetLinks.forEach(function (l) {
targetCount = l.circular && !selfLinking(l, id)
? 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, id) {
links.sort(sortLinkColumnAscending)
links.forEach(function (link, i) {
let buffer = 0
if (selfLinking(link, id) && 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, id) {
//let baseRadius = 10
let buffer = 5
//let verticalMargin = 25
let minY = d3Array.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, id)
let bottomLinks = graph.links.filter(function (l) {
return l.circularLinkType == 'bottom'
})
bottomLinks = calcVerticalBuffer(bottomLinks, circularLinkGap, id)
// 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, id) && 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 = d3Shape.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, id) {
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 (
getNodeID(otherNode, id) == getNodeID(node, id) ||
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 (
getNodeID(otherNode, id) == getNodeID(node, id) ||
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 (
getNodeID(otherNode, id) == getNodeID(node, id) ||
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, id) {
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 getNodeID(l.source, id) == getNodeID(node, id)
})
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, id) {
graph.nodes.forEach(function (node) {
let nodesTargetLinks = graph.links.filter(function (l) {
return getNodeID(l.target, id) == getNodeID(node, id)
})
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, id) {
return getNodeID(link.source, id) == getNodeID(link.target, id)
}
function fillHeight(graph, y0, y1) {
var nodes = graph.nodes
var links = graph.links
var top = false
var bottom = false
links.forEach(function(link){
if (link.circularLinkType == "top") {
top = true
} else if (link.circularLinkType == "bottom") {
bottom = true
}
})
if (top == false || bottom == false) {
var minY0 = d3Array.min(nodes, function(node){ return node.y0 })
var maxY1 = d3Array.max(nodes, function(node){ return node.y1 })
var currentHeight = maxY1 - minY0
var chartHeight = y1 - y0
var ratio = chartHeight/currentHeight
nodes.forEach(function(node){
var nodeHeight = (node.y1 - node.y0) * ratio
node.y0 = (node.y0 - minY0) * ratio
node.y1 = node.y0 + nodeHeight
})
links.forEach(function(link) {
link.y0 = (link.y0 - minY0) * ratio
link.y1 = (link.y1 - minY0) * ratio
link.width = link.width * ratio
})
}
}
/// ////////////////////////////////////////////////////////////////////////////
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>
<title>Sankey with circular links</title>
<style>
body {
font-family: sans-serif;
}
h1 {
color: #8e0152
}
span {
font-weight: bold
}
rect {
shape-rendering: crispEdges;
}
text {
font-size: 12px;
}
path {
fill: none;
opacity: 0.5
}
.lower {
text-anchor: end
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {display:none;}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #8e0152;
}
input:focus + .slider {
box-shadow: 0 0 1px #8e0152;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
</style>
</head>
<body>
<h1>Sankey with circular links</h1>
<p>Switch to new data:</p>
<form id="selectData">
<input type="radio" name="selectData" value="Control" checked> Control group<br>
<input type="radio" name="selectData" value="Test 1"> Test 1<br>
<input type="radio" name="selectData" value="Test 2"> Test 2
</form>
<div id="legend"></div>
<div id="chart"></div>
<script>
var margin = { top: 10, right: 10, bottom: 10, left: 120 };
var width = 1000;
var height = 700;
var extent = [-1, 1]
//colour for updated nodes and links
var colour = d3.scaleSequential(d3.interpolatePiYG)
.domain(extent);
let data = data5;
let originalLinks = [
{ "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" }
]
let newLinks = [
{ "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": 20, "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": "process3", "target": "process2", "value": 15, "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": 5, "optimal": "yes" },
{ "source": "process9", "target": "finishB", "value": 10, "optimal": "yes" },
{ "source": "process15", "target": "finishB", "value": 10, "optimal": "yes" },
{ "source": "process15", "target": "finishA", "value": 25, "optimal": "yes" },
{ "source": "process11", "target": "process15", "value": 5, "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": 5, "optimal": "yes" },
{ "source": "finishB", "target": "process14", "value": 5, "optimal": "yes" }
]
let newLinks2 = [
{ "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": 20, "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": "process3", "target": "process2", "value": 15, "optimal": "yes" },
{ "source": "process15", "target": "process13", "value": 20, "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": 5, "optimal": "yes" },
{ "source": "process8", "target": "process11", "value": 25, "optimal": "yes" },
{ "source": "process11", "target": "process10", "value": 10, "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": 5, "optimal": "yes" },
{ "source": "process10", "target": "process16", "value": 5, "optimal": "yes" },
{ "source": "process14", "target": "finishB", "value": 10, "optimal": "yes" },
{ "source": "process9", "target": "finishA", "value": 10, "optimal": "yes" },
{ "source": "process16", "target": "process8", "value": 5, "optimal": "yes" },
{ "source": "process9", "target": "finishB", "value": 10, "optimal": "yes" },
{ "source": "process15", "target": "finishB", "value": 10, "optimal": "yes" },
{ "source": "process15", "target": "finishA", "value": 25, "optimal": "yes" },
{ "source": "process11", "target": "process15", "value": 15, "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": 5, "optimal": "yes" },
{ "source": "finishB", "target": "process14", "value": 5, "optimal": "yes" }
]
const nodePadding = 40;
const circularLinkGap = 2;
var sankey = d3.sankeyCircular()
.nodeWidth(10)
.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);
var node = nodeG
.data(sankeyData.nodes, function(d){ return d.name })
.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")
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; });
var link = linkG.data(sankeyData.links, function(d) { return d.index})
.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("stroke", "#BAB5A1" )
link.append("title")
.text(function (d) {
return d.source.name + " → " + d.target.name + "\n Index: " + (d.index);
});
//////////////////////////////////////////////////////////////////////
let legendMargin = { top: 0, right: 500, bottom: 0, left: 500 };
let legendWidth = width + (margin.left + margin.right) - (legendMargin.left + legendMargin.right);
let legendHeight = 45;
let legend = d3.select("#legend").append("svg")
.attr("width", legendWidth + legendMargin.left + legendMargin.right)
.attr("height", legendHeight + legendMargin.top + legendMargin.bottom);
let legendG = legend.append("g")
.attr("transform", "translate(" + legendMargin.left + "," + legendMargin.top + ")")
let defs = legend.append("defs")
let legendGradient = defs.append("linearGradient")
.attr("id", "linear-gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "0%");
let noOfSamples = 20;
let dataRange = extent[1] - extent[0];
let stepSize = dataRange / noOfSamples;
for (i = 0; i < noOfSamples; i++) {
legendGradient.append("stop")
.attr("offset", (i / (noOfSamples - 1)))
.attr("stop-color", colour(extent[0] + (i * stepSize)));
}
legendG.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#linear-gradient)");
let textX = 3
let textY = 15
let textYOffset = 12
legendG.append("text")
.attr("class", "legend-label lower after")
.text("")
.attr("x", -textX)
.attr("y", textY)
legendG.append("text")
.attr("class", "legend-label lower")
.text("is lower than")
.attr("x", -textX)
.attr("y", textY + textYOffset)
legendG.append("text")
.attr("class", "legend-label lower before")
.text("")
.attr("x", -textX)
.attr("y", textY + textYOffset + textYOffset)
legendG.append("text")
.attr("class", "legend-label higher after")
.text("")
.attr("x", legendWidth + textX)
.attr("y", textY)
legendG.append("text")
.attr("class", "legend-label higher")
.text("is higher than")
.attr("x", legendWidth + textX)
.attr("y", textY + textYOffset)
legendG.append("text")
.attr("class", "legend-label higher before")
.text("")
.attr("x", legendWidth + textX)
.attr("y", textY + textYOffset + textYOffset)
//////////////////////////////////////////////////////////////////////
var useNewData = true
let selected = "Control"
let previousSelected = "Control"
d3.selectAll("input")
.on("change", function(){
let t = d3.transition().duration(1000)
previousSelected = selected
selected = this.value
d3.selectAll(".before").text(previousSelected)
d3.selectAll(".after").text(selected)
if (selected == "Control") {
sankey.updateValues(sankeyData, originalLinks)
} else if (selected == "Test 1") {
sankey.updateValues(sankeyData, newLinks)
} else {
sankey.updateValues(sankeyData, newLinks2)
}
node.data(sankeyData.nodes, function(d){ return d.name })
link.data(sankeyData.links, function(d){ return d.name })
node.selectAll("rect")
.transition(t)
.attr("y", function (d) { return d.y0; })
.attr("height", function (d) { return d.y1 - d.y0; })
.style("fill", function(d){ return d.value == d.previousValue ? "grey" : colour((d.value/d.previousValue) - 1) })
node.selectAll("text")
.transition(t)
.attr("y", function (d) { return d.y0 - 12; })
link.selectAll("path")
.transition(t)
.attr("d", function(link){
return link.path;
})
.style("stroke-width", function (d) { return d.width})
.style("stroke", function(d){ return d.value == d.previousValue ? "lightgrey" : colour((d.value/d.previousValue) - 1) })
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment