Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active November 23, 2021 21:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tomshanley/b82d9aede85694b1422786ef41536ec5 to your computer and use it in GitHub Desktop.
Save tomshanley/b82d9aede85694b1422786ef41536ec5 to your computer and use it in GitHub Desktop.
Sankey with circular links
license: mit

Testing how a Sankey with circular links may be constructed.

  • This chart detects circular links, and then uses top and bottom 'channels' to layout those links back to the target node. The middle channel is for "forward" links through the process.

  • This needs more work in terms in aligning the nodes better - the nodes which have circular links are pushed to the top or bottom, but perhaps a bit too rigourously.

  • This version of the sankey code works out padding based on a proportion of the height and max values/nodes in each column.

  • Need to consider how circular links could work going down the middle if that makes sense, eg from process4 to process1 in this example. Perhaps if the nodes a close in terms of depth, and the risk of crossing other links is lower?

But ultimately, this needs a load more sankey datasets thrown at it, to break the layout functions

// https://github.com/d3/d3-sankey Version 0.7.1. Copyright 2017 Mike Bostock.
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(
exports,
require('d3-array'),
require('d3-collection'),
require('d3-shape')
)
: typeof define === 'function' && define.amd
? define(['exports', 'd3-array', 'd3-collection', 'd3-shape'], factory)
: factory(
(global.d3 = global.d3 || {}),
global.d3,
global.d3,
global.d3
)
})(this, function (exports, d3Array, d3Collection, d3Shape) {
'use strict'
function targetDepth (d) {
return d.target.depth
}
function left (node) {
return node.depth
}
function right (node, n) {
return n - 1 - node.height
}
function justify (node, n) {
return node.sourceLinks.length ? node.depth : n - 1
}
function center (node) {
return node.targetLinks.length
? node.depth
: node.sourceLinks.length
? d3Array.min(node.sourceLinks, targetDepth) - 1
: 0
}
function constant (x) {
return function () {
return x
}
}
function ascendingSourceBreadth (a, b) {
return ascendingBreadth(a.source, b.source) || a.index - b.index
}
function ascendingTargetBreadth (a, b) {
return ascendingBreadth(a.target, b.target) || a.index - b.index
}
function ascendingBreadth (a, b) {
if (a.partOfCycle === b.partOfCycle) {
return a.y0 - b.y0
} else {
if (a.circularLinkType === 'top' || b.circularLinkType === 'bottom') {
return -1
} else {
return 1
}
}
}
function value (d) {
return d.value
}
function nodeCenter (node) {
return (node.y0 + node.y1) / 2
}
function weightedSource (link) {
return nodeCenter(link.source) * link.value
}
function weightedTarget (link) {
return nodeCenter(link.target) * link.value
}
function defaultId (d) {
return d.index
}
function defaultNodes (graph) {
return graph.nodes
}
function defaultLinks (graph) {
return graph.links
}
function find (nodeById, id) {
var node = nodeById.get(id)
if (!node) throw new Error('missing: ' + id)
return node
}
var sankey = function () {
var x0 = 0,
y0 = 0,
x1 = 1,
y1 = 1, // extent
dx = 24, // nodeWidth
py, // nodePadding
id = defaultId,
align = justify,
nodes = defaultNodes,
links = defaultLinks,
iterations = 32;
var padding = Infinity;
var paddingRatio = 0.1;
function sankey () {
var graph = {
nodes: nodes.apply(null, arguments),
links: links.apply(null, arguments)
}
computeNodeLinks(graph)
identifyCircles(graph)
selectCircularLinkTypes(graph)
computeNodeValues(graph)
computeNodeDepths(graph)
computeNodeBreadths(graph, iterations)
computeLinkBreadths(graph)
return graph
}
sankey.update = function (graph) {
computeLinkBreadths(graph)
return graph
}
sankey.nodeId = function (_) {
return arguments.length
? ((id = typeof _ === 'function' ? _ : constant(_)), sankey)
: id
}
sankey.nodeAlign = function (_) {
return arguments.length
? ((align = typeof _ === 'function' ? _ : constant(_)), sankey)
: align
}
sankey.nodeWidth = function (_) {
return arguments.length ? ((dx = +_), sankey) : dx
}
sankey.nodePadding = function (_) {
return arguments.length ? ((py = +_), sankey) : py
}
sankey.nodes = function (_) {
return arguments.length
? ((nodes = typeof _ === 'function' ? _ : constant(_)), sankey)
: nodes
}
sankey.links = function (_) {
return arguments.length
? ((links = typeof _ === 'function' ? _ : constant(_)), sankey)
: links
}
sankey.size = function (_) {
return arguments.length
? ((x0 = y0 = 0), (x1 = +_[0]), (y1 = +_[1]), sankey)
: [x1 - x0, y1 - y0]
}
sankey.extent = function (_) {
return arguments.length
? ((x0 = +_[0][0]), (x1 = +_[1][0]), (y0 = +_[0][1]), (y1 = +_[1][
1
]), sankey)
: [[x0, y0], [x1, y1]]
}
sankey.iterations = function (_) {
return arguments.length ? ((iterations = +_), sankey) : iterations
}
sankey.nodePaddingRatio = function (_) {
return arguments.length ? ((paddingRatio = +_), sankey) : paddingRatio
}
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks (graph) {
graph.nodes.forEach(function (node, i) {
node.index = i
node.sourceLinks = []
node.targetLinks = []
})
var nodeById = d3Collection.map(graph.nodes, id)
graph.links.forEach(function (link, i) {
link.index = i
var source = link.source
var target = link.target
if (typeof source !== 'object') {
source = link.source = find(nodeById, source)
}
if (typeof target !== 'object') {
target = link.target = find(nodeById, target)
}
source.sourceLinks.push(link)
target.targetLinks.push(link)
})
}
// Compute the value (size) and cycleness of each node by summing the associated links.
function computeNodeValues (graph) {
graph.nodes.forEach(function (node) {
node.partOfCycle = false
node.value = Math.max(
d3Array.sum(node.sourceLinks, value),
d3Array.sum(node.targetLinks, value)
)
node.sourceLinks.forEach(function (link) {
if (link.circular) {
node.partOfCycle = true
node.circularLinkType = link.circularLinkType
}
})
node.targetLinks.forEach(function (link) {
if (link.circular) {
node.partOfCycle = true
node.circularLinkType = link.circularLinkType
}
})
})
}
// Iteratively assign the depth (x-position) for each node.
// Nodes are assigned the maximum depth of incoming neighbors plus one;
// nodes with no incoming links are assigned depth zero, while
// nodes with no outgoing links are assigned the maximum depth.
function computeNodeDepths (graph) {
var nodes, next, x
for (
(nodes = graph.nodes), (next = []), (x = 0);
nodes.length;
++x, (nodes = next), (next = [])
) {
nodes.forEach(function (node) {
node.depth = x
node.sourceLinks.forEach(function (link) {
if (next.indexOf(link.target) < 0 && !link.circular) {
next.push(link.target)
}
})
})
}
for (
(nodes = graph.nodes), (next = []), (x = 0);
nodes.length;
++x, (nodes = next), (next = [])
) {
nodes.forEach(function (node) {
node.height = x
node.targetLinks.forEach(function (link) {
if (next.indexOf(link.source) < 0 && !link.circular) {
next.push(link.source)
}
})
})
}
var kx = (x1 - x0 - dx) / (x - 1)
graph.nodes.forEach(function (node) {
node.x1 =
(node.x0 =
x0 +
Math.max(
0,
Math.min(x - 1, Math.floor(align.call(null, node, x)))
) *
kx) + dx
})
}
function computeNodeBreadths (graph) {
var columns = d3Collection
.nest()
.key(function (d) {
return d.x0
})
.sortKeys(d3Array.ascending)
.entries(graph.nodes)
.map(function (d) {
return d.values
})
initializeNodeBreadth()
resolveCollisions()
for (var alpha = 1, n = iterations; n > 0; --n) {
relaxRightToLeft((alpha *= 0.99))
resolveCollisions()
relaxLeftToRight(alpha)
resolveCollisions()
}
function initializeNodeBreadth () {
console.log(paddingRatio)
columns.forEach(function(nodes){
let thisPadding = (y1 * paddingRatio) / (nodes.length + 1)
padding = thisPadding < padding ? thisPadding : padding;
})
py = padding;
console.log(py)
var ky = d3Array.min(columns, function (nodes) {
return (y1 - y0 - ((nodes.length - 1) * py)) / d3Array.sum(nodes, value)
})
ky = ky / 2
columns.forEach(function (nodes) {
var nodesLength = nodes.length
nodes.forEach(function (node, i) {
if (node.partOfCycle) {
if (node.circularLinkType == 'top') {
node.y0 = y0 + i
node.y1 = node.y0 + node.value * ky
} else {
node.y0 = y1 - node.value - i
node.y1 = node.y0 + node.value * ky
}
} else {
// node.y1 = (node.y0 = i) + node.value * ky
node.y0 = (y1 - y0) / 2 - nodesLength / 2 + i
node.y1 = node.y0 + node.value * ky
}
})
})
graph.links.forEach(function (link) {
link.width = link.value * ky
})
}
function relaxLeftToRight (alpha) {
let columnsLength = columns.length;
columns.forEach(function (nodes, i) {
let n = nodes.length;
nodes.forEach(function (node) {
if (node.targetLinks.length) {
if (node.partOfCycle && (n > 1) /*&& (i > 0) && (i < columnsLength) */) {
//do nothing for now
} else {
var dy =
(d3Array.sum(node.targetLinks, weightedSource) /
d3Array.sum(node.targetLinks, value) -
nodeCenter(node)) *
alpha
node.y0 += dy
node.y1 += dy
}
}
})
})
}
function relaxRightToLeft (alpha) {
let columnsLength = columns.length;
columns.slice().reverse().forEach(function (nodes, i) {
let n = nodes.length;
nodes.forEach(function (node) {
if (node.sourceLinks.length) {
if (node.partOfCycle && (n > 1)/* && (i > 0) && (i < columnsLength) */) {
//do nothing for now
} else {
var dy =
(d3Array.sum(node.sourceLinks, weightedTarget) /
d3Array.sum(node.sourceLinks, value) -
nodeCenter(node)) *
alpha
node.y0 += dy
node.y1 += dy
}
}
})
})
}
function resolveCollisions () {
columns.forEach(function (nodes) {
var node, dy, y = y0, n = nodes.length, i
// Push any overlapping nodes down.
nodes.sort(ascendingBreadth)
for (i = 0; i < n; ++i) {
node = nodes[i]
dy = y - node.y0
if (dy > 0) {
node.y0 += dy
node.y1 += dy
}
y = node.y1 + py
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y - py - y1
if (dy > 0) {
;(y = node.y0 -= dy), (node.y1 -= dy)
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i]
dy = node.y1 + py - y
if (dy > 0) (node.y0 -= dy), (node.y1 -= dy)
y = node.y0
}
}
})
}
}
function computeLinkBreadths (graph) {
graph.nodes.forEach(function (node) {
node.sourceLinks.sort(ascendingTargetBreadth)
node.targetLinks.sort(ascendingSourceBreadth)
})
graph.nodes.forEach(function (node) {
var y0 = node.y0
var y1 = y0
// start from the bottom of the node for cycle links
var y0cycle = node.y1
var y1cycle = y0cycle
node.sourceLinks.forEach(function (link) {
if (link.circular) {
link.y0 = y0cycle - link.width / 2
y0cycle = y0cycle - link.width
} else {
link.y0 = y0 + link.width / 2
y0 += link.width
}
})
node.targetLinks.forEach(function (link) {
if (link.circular) {
link.y1 = y1cycle - link.width / 2
y1cycle = y1cycle - link.width
} else {
link.y1 = y1 + link.width / 2
y1 += link.width
}
})
})
}
return sankey
}
/// /////////////////////////////////////////////////////////////////////////////////
// Cycle functions
// Identify circles in the link objects
function identifyCircles (graph) {
var addedLinks = []
var circularLinkID = 0
graph.links.forEach(function (link) {
if (createsCycle(link.source, link.target, addedLinks)) {
link.circular = true
link.circularLinkID = circularLinkID
circularLinkID = circularLinkID + 1
} else {
link.circular = false
addedLinks.push(link)
}
})
}
function selectCircularLinkTypes (graph) {
graph.links.forEach(function (link) {
console.log(link.target.circularLinkType)
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 = link.circularLinkID % 2 == 0
? 'bottom'
: 'top'
}
graph.nodes.forEach(function (node) {
if (node.name == link.source.name || node.name == link.target.name) {
node.circularLinkType = link.circularLinkType
}
})
}
// if the target and source have the same circularLinkType, use that
/* if (
link.source.circularLinkType &&
link.source.circularLinkType === link.target.circularLinkType
) {
console.log(link.circularLinkID + ' reusing')
return link.source.circularLinkType
} else if (link.source.circularLinkType) {
// if only one of the target/source has a circularLinkType, use that
console.log(link.circularLinkID + ' reusing source')
return link.source.circularLinkType
} else {
// assign random?
console.log(link.circularLinkID + ' random')
return
} */
})
}
// Checks if link creates a cycle
function createsCycle (originalSource, nodeToCheck, graph) {
if (graph.length == 0) {
return false
}
var nextLinks = findLinksOutward(nodeToCheck, graph)
// leaf node check
if (nextLinks.length == 0) {
return false
}
// cycle check
for (var i = 0; i < nextLinks.length; i++) {
var nextLink = nextLinks[i]
if (nextLink.target === originalSource) {
return true
}
// Recurse
if (createsCycle(originalSource, nextLink.target, graph)) {
return true
}
}
// Exhausted all links
return false
}
/* Given a node, find all links for which this is a source
in the current 'known' graph */
function findLinksOutward (node, graph) {
var children = []
for (var i = 0; i < graph.length; i++) {
if (node == graph[i].source) {
children.push(graph[i])
}
}
return children
}
// create a path for circle paths
/* function computeCirclePath (d) {
//distance back to target node
let circularLinkDistance = d.source.depth - d.target.depth;
//distance out from the node
var leftNodeBuffer = 40 - (40 * (d.y0/height));
var rightNodeBuffer = 40 - (40 * (d.y1/height));
//how far below the nodes the path will go, more so for longer distances
var verticalBuffer = 20 * circularLinkDistance;
//radius of the corners of the path
var arcRadius = d.width + 10;
var leftInnerExtent = d.source.x1 + leftNodeBuffer;
var leftFullExtent = d.source.x1 + arcRadius + leftNodeBuffer;
var rightInnerExtent = d.target.x0 - rightNodeBuffer;
var rightFullExtent = d.target.x0 - arcRadius - rightNodeBuffer;
var bottomInnerExtent = height + verticalBuffer;
var bottomFullExtent = height + verticalBuffer + arcRadius;
let path =
// start at the right of the source node
"M" + d.source.x1 + " " + d.y0 + " " +
// line right to buffer point
"L" + leftInnerExtent + " " + d.y0 + " " +
//Arc around: Centre of arc X and //Centre of arc Y
"A" + arcRadius + " " + arcRadius + " 0 0 1 " +
//End of arc X //End of arc Y
leftFullExtent + " " + (d.y0 + arcRadius) + " " + //End of arc X
// line down to buffer point
"L" + leftFullExtent + " " + bottomInnerExtent + " " +
//Arc around: Centre of arc X and //Centre of arc Y
"A" + arcRadius + " " + arcRadius + " 0 0 1 " +
//End of arc X //End of arc Y
leftInnerExtent + " " + bottomFullExtent + " " + //End of arc X
// line left to buffer point
"L" + rightInnerExtent + " " + bottomFullExtent + " " +
//Arc around: Centre of arc X and //Centre of arc Y
"A" + arcRadius + " " + arcRadius + " 0 0 1 " +
//End of arc X //End of arc Y
rightFullExtent + " " + bottomInnerExtent + " " + //End of arc X
// line up
"L" + rightFullExtent + " " + (d.y1 + arcRadius) + " " +
//Arc around: Centre of arc X and //Centre of arc Y
"A" + arcRadius + " " + arcRadius + " 0 0 1 " +
//End of arc X //End of arc Y
rightInnerExtent + " " + d.y1 + " " + //End of arc X
//line to end
"L" + d.target.x0 + " " + d.y1;
return path
} */
/// /////////////////////////////////////////////////////////////////////////////////
/* var curveSankeyLink = function () {
return function (d) {
let path = ''
if (d.circular) {
path = computeCirclePath(d)
} else {
var normalPath = d3Shape
.linkHorizontal()
.source(function (d) {
return [d.source.x1, d.y0]
})
.target(function (d) {
return [d.target.x0, d.y1]
})
path = normalPath(d)
}
return path
}
} */
/// /////////////////////////////////////////////////////////////////////////////////
var sankeyLinkHorizontal = function () {
return d3Shape
.linkHorizontal()
.source(horizontalSource)
.target(horizontalTarget)
}
function horizontalSource (d) {
return [d.source.x1, d.y0]
}
function horizontalTarget (d) {
return [d.target.x0, d.y1]
}
exports.sankey = sankey
exports.sankeyCenter = center
exports.sankeyLeft = left
exports.sankeyRight = right
exports.sankeyJustify = justify
exports.sankeyLinkHorizontal = sankeyLinkHorizontal
// exports.curveSankeyLink = curveSankeyLink
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" },
{ "name": "finishC" }
],
"links": [
{ "source": "startA", "target": "process1", "value": 15 },
{ "source": "startA", "target": "process8", "value": 20 },
{ "source": "startA", "target": "process5", "value": 30 },
{ "source": "startA", "target": "process6", "value": 20 },
{ "source": "startB", "target": "process1", "value": 15 },
{ "source": "startB", "target": "process5", "value": 15 },
{ "source": "process1", "target": "process4", "value": 20 },
{ "source": "process4", "target": "process1", "value": 10 },
{ "source": "process2", "target": "process7", "value": 30 },
{ "source": "process1", "target": "process3", "value": 10 },
{ "source": "process5", "target": "process3", "value": 20 },
{ "source": "process6", "target": "startA", "value": 5 },
{ "source": "process4", "target": "process2", "value": 5 },
{ "source": "process6", "target": "process8", "value": 15 },
{ "source": "process4", "target": "startB", "value": 5 },
{ "source": "process4", "target": "process7", "value": 10 },
{ "source": "process3", "target": "process2", "value": 25 },
{ "source": "process3", "target": "startB", "value": 5 },
{ "source": "process15", "target": "process13", "value": 10 },
{ "source": "process13", "target": "finishC", "value": 10 },
{ "source": "process7", "target": "startB", "value": 20 },
{ "source": "process8", "target": "process1", "value": 10 },
{ "source": "process16", "target": "process9", "value": 10 },
{ "source": "process8", "target": "process11", "value": 35 },
{ "source": "process11", "target": "process10", "value": 25 },
{ "source": "process4", "target": "process12", "value": 10 },
{ "source": "process12", "target": "process11", "value": 5 },
{ "source": "process7", "target": "process15", "value": 20 },
{ "source": "process15", "target": "process14", "value": 10 },
{ "source": "process10", "target": "process9", "value": 10 },
{ "source": "process10", "target": "process16", "value": 20 },
{ "source": "process14", "target": "finishB", "value": 10 },
{ "source": "process9", "target": "finishA", "value": 10 },
{ "source": "process16", "target": "process8", "value": 10 },
{ "source": "process9", "target": "finishB", "value": 10 },
{ "source": "process11", "target": "process14", "value": 25 }
]
};
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 }
]
}
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-sankey-circular.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="data.js"></script>
<title>Sankey with circular links</title>
<style>
#chart {
height: 800px;
}
rect {
fill-opacity: .9;
stroke: white;
stroke-width: 1;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 0 1px 0 #fff;
}
.link {
fill: none;
stroke: #000;
}
</style>
</head>
<body>
<h2>Sankey with circular links</h2>
<div id="chart"></div>
<script>
var margin = { top: 200, right: 200, bottom: 200, left: 200 };
var width = 1200;
var height = 300;
let data = data2;
var nodePadding = 40
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(nodePadding)
.nodePaddingRatio(0.1)
.size([width, height])
.nodeId(function (d) {
return d.name;
})
.nodeAlign(d3.sankeyJustify)
.iterations(32)
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
var linkG = g.append("g")
.attr("class", "links")
.attr("fill", "none")
.attr("stroke-opacity", 0.2)
.selectAll("path");
var nodeG = g.append("g")
.attr("class", "nodes")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g");
//run the Sankey + circular over the data
let sankeyData = sankey(data);
let sankeyNodes = sankeyData.nodes;
let sankeyLinks = sankeyData.links;
let depthExtent = d3.extent(data.nodes, function (d) { return d.depth; });
var colour = d3.scaleSequential(d3.interpolateCool)
.domain(depthExtent);
let linkDistance = width / depthExtent[1];
//Adjust link Y's based on target/source Y positions
sortTargetLinks()
sortSourceLinks()
//create paths for circular links
sankeyLinks = addCircularPathData(sankeyLinks);
//draw everything
var node = nodeG.data(sankeyNodes)
.enter()
.append("g");
node.append("rect")
.attr("x", function (d) { return d.x0; }) //Use original sankey defined positions
.attr("y", function (d) { return d.y0; }) //Use force defined positions
.attr("height", function (d) { return d.y1 - d.y0; })
.attr("width", function (d) { return d.x1 - d.x0; })
.style("fill", function (d) { return d.partOfCycle ? "red" : colour(d.depth); })
.style("fill", function (d) { return colour(d.depth); })
.style("opacity", 0.5)
.style("stroke", "white")
.on("mouseover", function (d) {
d3.select(this).style("opacity", 1);
let thisName = d.name;
d3.selectAll("path")
.style("opacity", function (l) {
return l.source.name == thisName || l.target.name == thisName ? 1 : 0.3;
})
})
.on("mouseout", function (d) {
d3.selectAll("rect").style("opacity", 0.5);
d3.selectAll("path").style("opacity", 0.7);
})
node.append("text")
.attr("x", function (d) { return d.x0 - 6; })
.attr("y", function (d) { return d.y0 + ((d.y1 - d.y0) / 2); })
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(function (d) { return d.name; })
.filter(function (d) { return d.x0 < width / 2; })
.attr("x", function (d) { return d.x1 + 6; })
.attr("text-anchor", "start")
node.append("title")
.text(function (d) { return d.name + "\n" + (d.value); });
var link = linkG.data(sankeyLinks)
.enter()
.append("path")
.attr("d", curveSankeyForceLink)
.style("stroke-width", function (d) { return Math.max(1, d.width); })
.style("stroke", function (d) {
return d.circular ? "red" : "black";
})
.style("opacity", 0.7);
link.append("title")
.text(function (d) {
return d.source.name + " → " + d.target.name + "\n ID: " + (d.index);
});
//Create a normal curve or circular curve
function curveSankeyForceLink(link) {
let path = ''
if (link.circular) {
//path = computeCircleForcePath(d)
path = link.circularPathData.path;
} else {
var normalPath = d3.linkHorizontal()
.source(function (d) {
let x = d.source.x0 + (d.source.x1 - d.source.x0);
let y = d.y0// + (d.source.y - d.source.y0);
return [x, y]
})
.target(function (d) {
let x = d.target.x0;
let y = d.y1// + (d.target.y - d.target.y0);
return [x, y]
})
path = normalPath(link)
}
return path
};
function linkAngle(link) {
let angle = 0;
let opposite = link.y1 - link.y0;
let adjacent = link.target.x0 - link.source.x1;
angle = Math.atan(Math.abs(opposite) / Math.abs(adjacent))
console.log("index: " + link.index + " y1: " + link.y1 + " y0: " + link.y0 + " opp: " + opposite + " adj: " + adjacent + " angle: " + angle)
if (opposite > 0) {
angle = angle + (Math.PI / 2)
}
else {
angle = (Math.PI / 2) - angle
}
return angle;
}
function addCircularPathData(links) {
let maxLinkWidth = d3.max(links, function (link) { return link.width });
let minRadius = maxLinkWidth;
let maxNodeDepth = d3.max(links, function (link) { return link.target.depth; });
let minY = d3.min(links, function (link) { return link.source.y0 });
let baseRadius = 10;
let circularLinkGap = 2;
//add the base data for each link
links.forEach(function (link) {
if (link.circular) {
link.circularPathData = {};
link.circularPathData.arcRadius = link.width + baseRadius;
link.circularPathData.leftNodeBuffer = 10;
link.circularPathData.rightNodeBuffer = 10;
link.circularPathData.sourceWidth = link.source.x1 - link.source.x0;
link.circularPathData.targetWidth = link.target.x1 - link.target.x0; //probably won't use
link.circularPathData.sourceX = link.source.x0 + link.circularPathData.sourceWidth;
link.circularPathData.targetX = link.target.x0;
link.circularPathData.sourceY = linkSourceY(link);
link.circularPathData.targetY = linkTargetY(link);
//add left extent coordinates, based on links with same source depth and circularLink type
let thisDepth = link.source.depth;
let thisCircularLinkType = link.circularLinkType;
let sameDepthLinks = links.filter(function (l) { return ((l.source.depth == thisDepth) && (l.circularLinkType == thisCircularLinkType)); })
if (link.circularLinkType == "bottom") {
sameDepthLinks.sort(sortLinkSourceYDescending);
}
else {
sameDepthLinks.sort(sortLinkSourceYAscending);
}
let radiusOffset = 0;
sameDepthLinks.forEach(function (l, i) {
if (l.circularLinkID == link.circularLinkID) {
link.circularPathData.leftSmallArcRadius = baseRadius + (link.width / 2) + radiusOffset;
link.circularPathData.leftLargeArcRadius = baseRadius + (link.width / 2) + (i * circularLinkGap) + radiusOffset;
}
radiusOffset = radiusOffset + l.width;
})
//add right extent coordinates, based on links with same target depth and circularLink type
thisDepth = link.target.depth;
sameDepthLinks = links.filter(function (l) { return ((l.target.depth == thisDepth) && (l.circularLinkType == thisCircularLinkType)); });
if (link.circularLinkType == "bottom") {
sameDepthLinks.sort(sortLinkTargetYDescending)
}
else {
sameDepthLinks.sort(sortLinkTargetYAscending)
}
radiusOffset = 0;
sameDepthLinks.forEach(function (l, i) {
if (l.circularLinkID == link.circularLinkID) {
link.circularPathData.rightSmallArcRadius = baseRadius + (link.width / 2) + radiusOffset;
link.circularPathData.rightLargeArcRadius = baseRadius + (link.width / 2) + (i * circularLinkGap) + radiusOffset;
}
radiusOffset = radiusOffset + l.width;
})
//add vertical extent coordinates, based on links with same target depth and circularLink type
sameCircularTypeLinks = links.filter(function (l) { return l.circularLinkType == thisCircularLinkType; });
sameCircularTypeLinks.sort(sortLinkDepthAscending);
let verticalOffset = 0;
sameCircularTypeLinks.forEach(function (l, i) {
if (l.circularLinkID == link.circularLinkID) {
link.circularPathData.verticalBuffer = (link.width / 2) + verticalOffset + (i * circularLinkGap);
}
verticalOffset = verticalOffset + l.width;
})
//all links
link.circularPathData.leftInnerExtent = link.circularPathData.sourceX + link.circularPathData.leftNodeBuffer;
link.circularPathData.rightInnerExtent = link.circularPathData.targetX - link.circularPathData.rightNodeBuffer;
link.circularPathData.leftFullExtent = link.circularPathData.sourceX + link.circularPathData.leftLargeArcRadius + link.circularPathData.leftNodeBuffer;
link.circularPathData.rightFullExtent = link.circularPathData.targetX - link.circularPathData.rightLargeArcRadius - link.circularPathData.rightNodeBuffer;
//bottom links
if (link.circularLinkType == "bottom") {
link.circularPathData.verticalFullExtent = (height + 25) + link.circularPathData.verticalBuffer;
link.circularPathData.verticalLeftInnerExtent = link.circularPathData.verticalFullExtent - link.circularPathData.leftLargeArcRadius;
link.circularPathData.verticalRightInnerExtent = link.circularPathData.verticalFullExtent - link.circularPathData.rightLargeArcRadius;
}
//top links
else {
link.circularPathData.verticalFullExtent = minY - 25 - link.circularPathData.verticalBuffer;
link.circularPathData.verticalLeftInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.leftLargeArcRadius;
link.circularPathData.verticalRightInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.rightLargeArcRadius;
};
link.circularPathData.path = createCircularPathString(link);
}
})
return links;
}
function createCircularPathString(link) {
let pathString = "";
let pathData =
console.log(link.circularPathData.sourceX)
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;
}
//bottom path
else {
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;
}
function sortLinkDepthAscending(link1, link2) {
return linkDepthDistance(link1) - linkDepthDistance(link2);
};
function sortLinkSourceYAscending(link1, link2) {
return linkSourceY(link1) - linkSourceY(link2);
};
function sortLinkSourceYDescending(link1, link2) {
return linkSourceY(link2) - linkSourceY(link1);
};
function sortLinkTargetYAscending(link1, link2) {
return linkTargetY(link1) - linkTargetY(link2);
};
function sortLinkTargetYDescending(link1, link2) {
return linkTargetY(link2) - linkTargetY(link1);
};
function linkDepthDistance(link) {
return link.source.depth - link.target.depth;
};
function linkSourceY(link) {
//return link.y0 + (link.source.y - link.source.y0);
return link.y0;
};
function linkTargetY(link) {
//return link.y1 + (link.target.y - link.target.y0);
return link.y1;
};
function sortSourceLinks() {
sankeyNodes.forEach(function (node) {
//move any nodes up which are off the bottom
if ((node.y + (node.y1 - node.y0)) > height) {
console.log("adjusted y for node " + node.name)
node.y = node.y - ((node.y + (node.y1 - node.y0)) - height)
}
let nodesSourceLinks = sankeyLinks.filter(function (l) { return l.source.name == node.name });
//if more than 1 link then sort
if (nodesSourceLinks.length > 1) {
nodesSourceLinks.sort(function (link1, link2) {
//if both are not circular...
if (!link1.circular && !link2.circular) {
console.log("---------------------------------------------------------")
let link1Angle = linkAngle(link1);
let link2Angle = linkAngle(link2);
console.log("node: " + node.name + " link1 " + link1.index + " link1 A " + link1Angle + " link2 " + link2.index + " link2 A " + link2Angle)
console.log("---------------------------------------------------------")
return link1Angle - link2Angle
//return link2Angle - link1Angle
};
//if only one is circular, the move top links up, or bottom links down
if (link1.circular && !link2.circular) {
console.log(link1.circularLinkID)
return link1.circularLinkType == "top" ? -1 : 1;
}
else if (link2.circular && !link1.circular) {
console.log(link2.circularLinkID)
return link2.circularLinkType == "top" ? 1 : -1;
};
//if both links are circular...
if (link1.circular && link2.circular) {
//...and they both loop the same way (both top)
if (link1.circularLinkType === link2.circularLinkType && link1.circularLinkType == "top") {
//...and they both connect to a target with same depth, then sort by the target's y
if (link1.target.depth === link2.target.depth) {
return link1.target.y1 - link2.target.y1
}
//...and they connect to different depth targets, then sort by how far back they
else {
return link1.target.depth - link2.target.depth;
}
}
//...and they both loop the same way (both bottom)
else if (link1.circularLinkType === link2.circularLinkType && link1.circularLinkType == "bottom") {
//...and they both connect to a target with same depth, then sort by the target's y
if (link1.target.depth === link2.target.depth) {
return link2.target.y1 - link1.target.y1;
}
//...and they connect to different depth targets, then sort by how far back they
else {
return link1.target.depth - link2.target.depth;
}
}
//...and they loop around different ways, the move top up and bottom down
else {
return link1.circularLinkType == "top" ? -1 : 1;
}
};
})
}
//update y0 for links
let ySourceOffset = node.y0
console.log(nodesSourceLinks)
nodesSourceLinks.forEach(function (link) {
link.y0 = ySourceOffset + (link.width / 2);
ySourceOffset = ySourceOffset + link.width;
})
})
}
function sortTargetLinks() {
sankeyNodes.forEach(function (node) {
let nodesTargetLinks = sankeyLinks.filter(function (l) { return l.target.name == node.name });
if (nodesTargetLinks.length > 1) {
nodesTargetLinks.sort(function (link1, link2) {
//if both are not circular, the base on the target y position
if (!link1.circular && !link2.circular) {
let link1Angle = linkAngle(link1);
let link2Angle = linkAngle(link2);
return link2Angle - link1Angle;
};
//if only one is circular, the move top links up, or bottom links down
if (link1.circular && !link2.circular) {
console.log(link1.circularLinkID)
return link1.circularLinkType == "top" ? -1 : 1;
}
else if (link2.circular && !link1.circular) {
console.log(link2.circularLinkID)
return link2.circularLinkType == "top" ? 1 : -1;
};
//if both links are circular...
if (link1.circular && link2.circular) {
//...and they both loop the same way (both top)
if (link1.circularLinkType === link2.circularLinkType && link1.circularLinkType == "top") {
//...and they both connect to a target with same depth, then sort by the target's y
if (link1.source.depth === link2.source.depth) {
return link1.source.y1 - link2.source.y1
}
//...and they connect to different depth targets, then sort by how far back they
else {
return link1.source.depth - link2.source.depth;
}
}
//...and they both loop the same way (both bottom)
else if (link1.circularLinkType === link2.circularLinkType && link1.circularLinkType == "bottom") {
//...and they both connect to a target with same depth, then sort by the target's y
if (link1.source.depth === link2.source.depth) {
return link2.source.y1 - link1.source.y1;
}
//...and they connect to different depth targets, then sort by how far back they
else {
return link2.source.depth - link1.source.depth;
}
}
//...and they loop around different ways, the move top up and bottom down
else {
return link1.circularLinkType == "top" ? -1 : 1;
}
};
})
}
//update y1 for links
let yTargetOffset = node.y0;
nodesTargetLinks.forEach(function (link) {
console.log(link.circularLinkID)
link.y1 = yTargetOffset + (link.width / 2);
yTargetOffset = yTargetOffset + link.width;
})
})
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment