|
// https://github.com/tomshanley/d3-sankey-circular |
|
// fork of https://github.com/d3/d3-sankey copyright Mike Bostock |
|
;(function (global, factory) { |
|
typeof exports === 'object' && typeof module !== 'undefined' |
|
? factory( |
|
exports, |
|
require('d3-array'), |
|
require('d3-collection'), |
|
require('d3-shape') |
|
) |
|
: typeof define === 'function' && define.amd |
|
? define(['exports', 'd3-array', 'd3-collection', 'd3-shape'], factory) |
|
: factory( |
|
(global.d3 = global.d3 || {}), |
|
global.d3, |
|
global.d3, |
|
global.d3 |
|
) |
|
})(this, function (exports, d3Array, d3Collection, d3Shape) { |
|
'use strict' |
|
// For a given link, return the target node's depth |
|
function targetDepth (link) { |
|
return link.target.depth |
|
} |
|
|
|
// The depth of a node when the nodeAlign (align) is set to 'left' |
|
function left (node) { |
|
return node.depth |
|
} |
|
|
|
// The depth of a node when the nodeAlign (align) is set to 'right' |
|
function right (node, n) { |
|
return n - 1 - node.height |
|
} |
|
|
|
// The depth of a node when the nodeAlign (align) is set to 'justify' |
|
function justify (node, n) { |
|
return node.sourceLinks.length ? node.depth : n - 1 |
|
} |
|
|
|
// The depth of a node when the nodeAlign (align) is set to 'center' |
|
function center (node) { |
|
return node.targetLinks.length |
|
? node.depth |
|
: node.sourceLinks.length |
|
? d3Array.min(node.sourceLinks, targetDepth) - 1 |
|
: 0 |
|
} |
|
|
|
// returns a function, using the parameter given to the sankey setting |
|
function constant (x) { |
|
return function () { |
|
return x |
|
} |
|
} |
|
|
|
// sort links' breadth (ie top to bottom in a column), based on their source nodes' breadths |
|
function ascendingSourceBreadth (a, b) { |
|
return ascendingBreadth(a.source, b.source) || a.index - b.index |
|
} |
|
|
|
// sort links' breadth (ie top to bottom in a column), based on their target nodes' breadths |
|
function ascendingTargetBreadth (a, b) { |
|
return ascendingBreadth(a.target, b.target) || a.index - b.index |
|
} |
|
|
|
// sort nodes' breadth (ie top to bottom in a column) |
|
// if both nodes have circular links, or both don't have circular links, then sort by the top (y0) of the node |
|
// else push nodes that have top circular links to the top, and nodes that have bottom circular links to the bottom |
|
function ascendingBreadth (a, b) { |
|
if (a.partOfCycle === b.partOfCycle) { |
|
return a.y0 - b.y0 |
|
} else { |
|
if (a.circularLinkType === 'top' || b.circularLinkType === 'bottom') { |
|
return -1 |
|
} else { |
|
return 1 |
|
} |
|
} |
|
} |
|
|
|
// return the value of a node or link |
|
function value (d) { |
|
return d.value |
|
} |
|
|
|
// return the vertical center of a node |
|
function nodeCenter (node) { |
|
return (node.y0 + node.y1) / 2 |
|
} |
|
|
|
// return the vertical center of a link's source node |
|
function linkSourceCenter (link) { |
|
return nodeCenter(link.source) |
|
} |
|
|
|
// return the vertical center of a link's target node |
|
function linkTargetCenter (link) { |
|
return nodeCenter(link.target) |
|
} |
|
|
|
/* function weightedSource (link) { |
|
return nodeCenter(link.source) * link.value |
|
} */ |
|
|
|
/* function weightedTarget (link) { |
|
return nodeCenter(link.target) * link.value |
|
} */ |
|
|
|
// Return the default value for ID for node, d.index |
|
function defaultId (d) { |
|
return d.index |
|
} |
|
|
|
// Return the default object the graph's nodes, graph.nodes |
|
function defaultNodes (graph) { |
|
return graph.nodes |
|
} |
|
|
|
// Return the default object the graph's nodes, graph.links |
|
function defaultLinks (graph) { |
|
return graph.links |
|
} |
|
|
|
// Return the node from the collection that matches the provided ID, or throw an error if no match |
|
function find (nodeById, id) { |
|
var node = nodeById.get(id) |
|
if (!node) throw new Error('missing: ' + id) |
|
return node |
|
} |
|
|
|
// The main sankey functions |
|
|
|
// Some constants for circular link calculations |
|
const verticalMargin = 25; |
|
const baseRadius = 10; |
|
const scale = 0.3; //Possibly let user control this, although anything over 0.5 starts to get too cramped |
|
|
|
var sankey = function () { |
|
// Set the default values |
|
var x0 = 0, |
|
y0 = 0, |
|
x1 = 1, |
|
y1 = 1, // extent |
|
dx = 24, // nodeWidth |
|
py, // nodePadding, for vertical postioning |
|
id = defaultId, |
|
align = justify, |
|
nodes = defaultNodes, |
|
links = defaultLinks, |
|
iterations = 32, |
|
circularLinkGap = 2, |
|
paddingRatio |
|
|
|
function sankey () { |
|
var graph = { |
|
nodes: nodes.apply(null, arguments), |
|
links: links.apply(null, arguments) |
|
} |
|
|
|
// Process the graph's nodes and links, setting their positions |
|
|
|
// 1. Associate the nodes with their respective links, and vice versa |
|
computeNodeLinks(graph) |
|
|
|
// 2. Determine which links result in a circular path in the graph |
|
identifyCircles(graph) |
|
|
|
// 3. Calculate the nodes' values, based on the values of the incoming and outgoing links |
|
computeNodeValues(graph) |
|
|
|
// 4. 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) |
|
|
|
// 5. Determine how the circular links will be drawn, |
|
// either travelling back above the main chart ("top") |
|
// or below the main chart ("bottom") |
|
selectCircularLinkTypes(graph) |
|
|
|
// 6. Calculate the nodes' and links' vertical position within their respective column |
|
// Also readjusts sankey size if circular links are needed, and node x's |
|
computeNodeBreadths(graph, iterations) |
|
computeLinkBreadths(graph) |
|
|
|
// 7. Sort links per node, based on the links' source/target nodes' breadths |
|
// 8. Adjust nodes that overlap links that span 2+ columns |
|
|
|
let linkSortingIterations = 4; //Possibly let user control this number, like the iterations over node placement |
|
for (var iteration = 0; iteration < linkSortingIterations; iteration++) { |
|
|
|
sortSourceLinks(graph, y1) |
|
sortTargetLinks(graph, y1) |
|
resolveNodeLinkOverlaps(graph, y0, y1) |
|
sortSourceLinks(graph, y1) |
|
sortTargetLinks(graph, y1) |
|
|
|
} |
|
|
|
// 9. Calculate visually appealling path for the circular paths, and create the "d" string |
|
addCircularPathData(graph, circularLinkGap, y1) |
|
|
|
return graph |
|
} // end of sankey function |
|
|
|
// TODO - update this function to take into account circular changes |
|
/*sankey.update = function (graph) { |
|
computeLinkBreadths(graph) |
|
return graph |
|
}*/ |
|
|
|
// Set the sankey parameters |
|
// nodeID, nodeAlign, nodeWidth, nodePadding, nodes, links, size, extent, iterations, nodePaddingRatio, circularLinkGap |
|
sankey.nodeId = function (_) { |
|
return arguments.length |
|
? ((id = typeof _ === 'function' ? _ : constant(_)), sankey) |
|
: id |
|
} |
|
|
|
sankey.nodeAlign = function (_) { |
|
return arguments.length |
|
? ((align = typeof _ === 'function' ? _ : constant(_)), sankey) |
|
: align |
|
} |
|
|
|
sankey.nodeWidth = function (_) { |
|
return arguments.length ? ((dx = +_), sankey) : dx |
|
} |
|
|
|
sankey.nodePadding = function (_) { |
|
return arguments.length ? ((py = +_), sankey) : py |
|
} |
|
|
|
sankey.nodes = function (_) { |
|
return arguments.length |
|
? ((nodes = typeof _ === 'function' ? _ : constant(_)), sankey) |
|
: nodes |
|
} |
|
|
|
sankey.links = function (_) { |
|
return arguments.length |
|
? ((links = typeof _ === 'function' ? _ : constant(_)), sankey) |
|
: links |
|
} |
|
|
|
sankey.size = function (_) { |
|
return arguments.length |
|
? ((x0 = y0 = 0), (x1 = +_[0]), (y1 = +_[1]), sankey) |
|
: [x1 - x0, y1 - y0] |
|
} |
|
|
|
sankey.extent = function (_) { |
|
return arguments.length |
|
? ((x0 = +_[0][0]), (x1 = +_[1][0]), (y0 = +_[0][1]), (y1 = +_[1][ |
|
1 |
|
]), sankey) |
|
: [[x0, y0], [x1, y1]] |
|
} |
|
|
|
sankey.iterations = function (_) { |
|
return arguments.length ? ((iterations = +_), sankey) : iterations |
|
} |
|
|
|
sankey.circularLinkGap = function (_) { |
|
return arguments.length |
|
? ((circularLinkGap = +_), sankey) |
|
: circularLinkGap |
|
} |
|
|
|
sankey.nodePaddingRatio = function (_) { |
|
return arguments.length ? ((paddingRatio = +_), sankey) : paddingRatio |
|
} |
|
|
|
// Populate the sourceLinks and targetLinks for each node. |
|
// Also, if the source and target are not objects, assume they are indices. |
|
function computeNodeLinks (graph) { |
|
graph.nodes.forEach(function (node, i) { |
|
node.index = i |
|
node.sourceLinks = [] |
|
node.targetLinks = [] |
|
}) |
|
var nodeById = d3Collection.map(graph.nodes, id) |
|
graph.links.forEach(function (link, i) { |
|
link.index = i |
|
var source = link.source |
|
var target = link.target |
|
if (typeof source !== 'object') { |
|
source = link.source = find(nodeById, source) |
|
} |
|
if (typeof target !== 'object') { |
|
target = link.target = find(nodeById, target) |
|
} |
|
source.sourceLinks.push(link) |
|
target.targetLinks.push(link) |
|
}) |
|
} |
|
|
|
// Compute the value (size) and cycleness of each node by summing the associated links. |
|
function computeNodeValues (graph) { |
|
graph.nodes.forEach(function (node) { |
|
node.partOfCycle = false |
|
node.value = Math.max( |
|
d3Array.sum(node.sourceLinks, value), |
|
d3Array.sum(node.targetLinks, value) |
|
) |
|
node.sourceLinks.forEach(function (link) { |
|
if (link.circular) { |
|
node.partOfCycle = true |
|
node.circularLinkType = link.circularLinkType |
|
} |
|
}) |
|
node.targetLinks.forEach(function (link) { |
|
if (link.circular) { |
|
node.partOfCycle = true |
|
node.circularLinkType = link.circularLinkType |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
// Update the x0, y0, x1 and y1 for the sankey, to allow space for any circular links |
|
function scaleSankeySize (graph) { |
|
let totalTopLinksWidth = 0, |
|
totalBottomLinksWidth = 0, |
|
totalRightLinksWidth = 0, |
|
totalLeftLinksWidth = 0 |
|
|
|
let maxColumn = d3.max(graph.nodes, function (node) { |
|
return node.column |
|
}) |
|
|
|
graph.links.forEach(function (link) { |
|
if (link.circular) { |
|
if (link.circularLinkType == 'top') { |
|
totalTopLinksWidth = totalTopLinksWidth + link.width |
|
} else { |
|
totalBottomLinksWidth = totalBottomLinksWidth + link.width |
|
} |
|
|
|
if (link.target.column == 0) { |
|
totalRightLinksWidth = totalRightLinksWidth + link.width |
|
} |
|
|
|
if (link.source.column == maxColumn) { |
|
totalLeftLinksWidth = totalLeftLinksWidth + link.width |
|
} |
|
} |
|
}) |
|
|
|
//account for radius of curves and padding between links |
|
totalTopLinksWidth = totalTopLinksWidth > 0 ? totalTopLinksWidth + verticalMargin + baseRadius : totalTopLinksWidth; |
|
totalBottomLinksWidth = totalBottomLinksWidth > 0 ? totalBottomLinksWidth + verticalMargin + baseRadius : totalBottomLinksWidth; |
|
totalRightLinksWidth = totalRightLinksWidth > 0 ? totalRightLinksWidth + verticalMargin + baseRadius : totalRightLinksWidth; |
|
totalLeftLinksWidth = totalLeftLinksWidth > 0 ? totalLeftLinksWidth + verticalMargin + baseRadius : totalLeftLinksWidth; |
|
|
|
let currentWidth = x1 - x0; |
|
let currentHeight = y1 - y0; |
|
|
|
let newWidth = currentWidth + totalRightLinksWidth + totalLeftLinksWidth; |
|
let newHeight = currentHeight + totalTopLinksWidth + totalBottomLinksWidth; |
|
|
|
let scaleX = currentWidth / newWidth; |
|
let scaleY = currentHeight / newHeight; |
|
|
|
x0 = (x0 * scaleX) + (totalRightLinksWidth); |
|
x1 = x1 * scaleX; |
|
y0 = (y0 * scaleY) + (totalTopLinksWidth); |
|
y1 = y1 * scaleY; |
|
|
|
graph.nodes.forEach(function (node) { |
|
node.x0 = x0 + (node.column * (((x1 - x0) / maxColumn) - dx)) |
|
node.x1 = node.x0 + dx |
|
}) |
|
|
|
return scaleY; |
|
|
|
} |
|
|
|
// Iteratively assign the depth for each node. |
|
// Nodes are assigned the maximum depth of incoming neighbors plus one; |
|
// nodes with no incoming links are assigned depth zero, while |
|
// nodes with no outgoing links are assigned the maximum depth. |
|
function computeNodeDepths (graph) { |
|
var nodes, next, x |
|
|
|
for ( |
|
(nodes = graph.nodes), (next = []), (x = 0); |
|
nodes.length; |
|
++x, (nodes = next), (next = []) |
|
) { |
|
nodes.forEach(function (node) { |
|
node.depth = x |
|
node.sourceLinks.forEach(function (link) { |
|
if (next.indexOf(link.target) < 0 && !link.circular) { |
|
next.push(link.target) |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
for ( |
|
(nodes = graph.nodes), (next = []), (x = 0); |
|
nodes.length; |
|
++x, (nodes = next), (next = []) |
|
) { |
|
nodes.forEach(function (node) { |
|
node.height = x |
|
node.targetLinks.forEach(function (link) { |
|
if (next.indexOf(link.source) < 0 && !link.circular) { |
|
next.push(link.source) |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
// assign column numbers, and get max value |
|
graph.nodes.forEach(function (node) { |
|
node.column = Math.floor(align.call(null, node, x)) |
|
}) |
|
|
|
|
|
} |
|
|
|
// Assign nodes' breadths, and then shift nodes that overlap (resolveCollisions) |
|
function computeNodeBreadths (graph) { |
|
var columns = d3Collection |
|
.nest() |
|
.key(function (d) { |
|
return d.column |
|
}) |
|
.sortKeys(d3Array.ascending) |
|
.entries(graph.nodes) |
|
.map(function (d) { |
|
return d.values |
|
}) |
|
|
|
initializeNodeBreadth() |
|
//resolveCollisions() |
|
|
|
for (var alpha = 1, n = iterations; n > 0; --n) { |
|
relaxLeftAndRight((alpha *= 0.99)) |
|
resolveCollisions() |
|
} |
|
|
|
function initializeNodeBreadth () { |
|
|
|
//override py if nodePadding has been set |
|
if (paddingRatio) { |
|
let padding = Infinity |
|
columns.forEach(function (nodes) { |
|
let thisPadding = y1 * paddingRatio / (nodes.length + 1) |
|
padding = thisPadding < padding ? thisPadding : padding |
|
}) |
|
py = padding |
|
} |
|
|
|
var ky = d3Array.min(columns, function (nodes) { |
|
return (y1 - y0 - (nodes.length - 1) * py) / d3Array.sum(nodes, value) |
|
}) |
|
|
|
//calculate the widths of the links |
|
ky = ky * scale |
|
|
|
graph.links.forEach(function (link) { |
|
link.width = link.value * ky |
|
}) |
|
|
|
//determine how much to scale down the chart, based on circular links |
|
let ratio = scaleSankeySize(graph); |
|
|
|
//re-calculate widths |
|
ky = ky * ratio |
|
|
|
graph.links.forEach(function (link) { |
|
link.width = link.value * ky |
|
}) |
|
|
|
columns.forEach(function (nodes) { |
|
var nodesLength = nodes.length |
|
nodes.forEach(function (node, i) { |
|
if (node.depth == 3 && nodesLength == 1) { |
|
node.y0 = y1 / 2 - (node.value * ky) |
|
node.y1 = node.y0 + node.value * ky |
|
} else if (node.partOfCycle) { |
|
if (numberOfNonSelfLinkingCycles(node) == 0) { |
|
node.y0 = y1 / 2 + i |
|
node.y1 = node.y0 + node.value * ky |
|
} else if (node.circularLinkType == 'top') { |
|
node.y0 = y0 + i |
|
node.y1 = node.y0 + node.value * ky |
|
} else { |
|
node.y0 = y1 - node.value * ky - i |
|
node.y1 = node.y0 + node.value * ky |
|
} |
|
} else { |
|
node.y0 = (y1 - y0) / 2 - nodesLength / 2 + i |
|
node.y1 = node.y0 + node.value * ky |
|
} |
|
}) |
|
}) |
|
|
|
|
|
} |
|
|
|
// For each node in each column, check the node's vertical position in relation to its targets and sources vertical position |
|
// and shift up/down to be closer to the vertical middle of those targets and sources |
|
function relaxLeftAndRight (alpha) { |
|
let columnsLength = columns.length |
|
|
|
columns.forEach(function (nodes, i) { |
|
|
|
let n = nodes.length |
|
let depth = nodes[0].depth; |
|
|
|
nodes.forEach(function (node) { |
|
console.log(node.name); |
|
console.log(depth + " " + (columnsLength-1)); |
|
// check the node is not an orphan |
|
if (node.sourceLinks.length || node.targetLinks.length) { |
|
|
|
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) { |
|
console.log("s here") |
|
|
|
let nodeHeight = node.y1 - node.y0 |
|
node.y0 = y1 / 2 - nodeHeight / 2 |
|
node.y1 = y1 / 2 + nodeHeight / 2 |
|
} else if (node.partOfCycle && numberOfNonSelfLinkingCycles(node) > 0) { |
|
// console.log(node.name + " " + node.y0) |
|
} 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) { |
|
|
|
if (nodes.length == 1) { |
|
return |
|
} else { |
|
var node, dy, y = y0, n = nodes.length, i |
|
|
|
// Push any overlapping nodes down. |
|
nodes.sort(ascendingBreadth) |
|
|
|
for (i = 0; i < n; ++i) { |
|
node = nodes[i] |
|
dy = y - node.y0 |
|
|
|
if (dy > 0) { |
|
node.y0 += dy |
|
node.y1 += dy |
|
} |
|
y = node.y1 + py |
|
} |
|
|
|
// If the bottommost node goes outside the bounds, push it back up. |
|
dy = y - py - y1 |
|
if (dy > 0) { |
|
;(y = node.y0 -= dy), (node.y1 -= dy) |
|
|
|
// Push any overlapping nodes back up. |
|
for (i = n - 2; i >= 0; --i) { |
|
node = nodes[i] |
|
dy = node.y1 + py - y |
|
if (dy > 0) (node.y0 -= dy), (node.y1 -= dy) |
|
y = node.y0 |
|
} |
|
} |
|
} |
|
|
|
}) |
|
} |
|
} |
|
|
|
// Assign the links y0 and y1 based on source/target nodes position, |
|
// plus the link's relative position to other links to the same node |
|
function computeLinkBreadths (graph) { |
|
graph.nodes.forEach(function (node) { |
|
node.sourceLinks.sort(ascendingTargetBreadth) |
|
node.targetLinks.sort(ascendingSourceBreadth) |
|
}) |
|
graph.nodes.forEach(function (node) { |
|
var y0 = node.y0 |
|
var y1 = y0 |
|
|
|
// start from the bottom of the node for cycle links |
|
var y0cycle = node.y1 |
|
var y1cycle = y0cycle |
|
|
|
node.sourceLinks.forEach(function (link) { |
|
if (link.circular) { |
|
link.y0 = y0cycle - link.width / 2 |
|
y0cycle = y0cycle - link.width |
|
} else { |
|
link.y0 = y0 + link.width / 2 |
|
y0 += link.width |
|
} |
|
}) |
|
node.targetLinks.forEach(function (link) { |
|
if (link.circular) { |
|
link.y1 = y1cycle - link.width / 2 |
|
y1cycle = y1cycle - link.width |
|
} else { |
|
link.y1 = y1 + link.width / 2 |
|
y1 += link.width |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
return sankey |
|
} |
|
|
|
/// ///////////////////////////////////////////////////////////////////////////////// |
|
// Cycle functions |
|
// portion of code to detect circular links based on Colin Fergus' bl.ock https://gist.github.com/cfergus/3956043 |
|
|
|
// Identify circles in the link objects |
|
function identifyCircles (graph) { |
|
var addedLinks = [] |
|
var circularLinkID = 0 |
|
graph.links.forEach(function (link) { |
|
if (createsCycle(link.source, link.target, addedLinks)) { |
|
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) { |
|
let numberOfTops = 0 |
|
let numberOfBottoms = 0 |
|
|
|
|
|
let maxColumn = d3.max(graph.nodes, function (node) { |
|
return node.column |
|
}) |
|
|
|
graph.links.forEach(function (link) { |
|
let updateNode = true; |
|
|
|
if (link.circular) { |
|
//if the link is from end column to the first |
|
if (link.source.column == maxColumn && link.target.column == 0) { |
|
|
|
link.circularLinkType = numberOfTops <= numberOfBottoms |
|
? 'top' |
|
: 'bottom'; |
|
updateNode = false |
|
// if either souce or target has type already use that |
|
} else if (link.source.circularLinkType || link.target.circularLinkType) { |
|
// default to source type if available |
|
link.circularLinkType = link.source.circularLinkType |
|
? link.source.circularLinkType |
|
: link.target.circularLinkType |
|
} else |
|
{ |
|
link.circularLinkType = numberOfTops < numberOfBottoms |
|
? 'top' |
|
: 'bottom' |
|
} |
|
|
|
if (link.circularLinkType == 'top') { |
|
numberOfTops = numberOfTops + 1 |
|
} else { |
|
numberOfBottoms = numberOfBottoms + 1 |
|
} |
|
|
|
if (updateNode) { |
|
|
|
graph.nodes.forEach(function (node) { |
|
if (node.name == link.source.name || node.name == link.target.name) { |
|
node.circularLinkType = link.circularLinkType |
|
} |
|
}) |
|
|
|
} |
|
|
|
} |
|
}) |
|
|
|
//correct self-linking links to be same direction as node |
|
graph.links.forEach(function (link) { |
|
if (link.circular) { |
|
//if both source and target node are same type, then link should have same type |
|
if (link.source.circularLinkType == link.target.circularLinkType) { |
|
link.circularLinkType = link.source.circularLinkType |
|
} |
|
//if link is selflinking, then link should have same type as node |
|
if (selfLinking(link)) { |
|
link.circularLinkType = link.source.circularLinkType |
|
} |
|
} |
|
}) |
|
|
|
} |
|
|
|
// Checks if link creates a cycle |
|
function createsCycle (originalSource, nodeToCheck, graph) { |
|
|
|
// Check for self linking nodes |
|
if (originalSource.name == nodeToCheck.name) { |
|
return true |
|
} |
|
|
|
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 |
|
} |
|
|
|
// Return the angle between a straight line between the source and target of the link, and the vertical plane of the node |
|
function linkAngle (link) { |
|
let adjacent = Math.abs(link.y1 - link.y0) |
|
let opposite = Math.abs(link.target.x0 - link.source.x1) |
|
|
|
return Math.atan(opposite / adjacent) |
|
} |
|
|
|
// Check if two circular links potentially overlap |
|
function circularLinksCross (link1, link2) { |
|
if (link1.source.column < link2.target.column) { |
|
return false |
|
} else if (link1.target.column > link2.source.column) { |
|
return false |
|
} else { |
|
return true |
|
} |
|
} |
|
|
|
// Return the number of circular links for node, not including self linking links |
|
function numberOfNonSelfLinkingCycles (node) { |
|
let sourceCount = 0 |
|
node.sourceLinks.forEach(function (l) { |
|
sourceCount = l.circular && !selfLinking(l) |
|
? sourceCount + 1 |
|
: sourceCount |
|
}) |
|
|
|
let targetCount = 0 |
|
node.targetLinks.forEach(function (l) { |
|
targetCount = l.circular && !selfLinking(l) |
|
? targetCount + 1 |
|
: targetCount |
|
}) |
|
|
|
return sourceCount + targetCount |
|
} |
|
|
|
// Check if a circular link is the only circular link for both its source and target node |
|
function onlyCircularLink (link) { |
|
let nodeSourceLinks = link.source.sourceLinks |
|
let sourceCount = 0 |
|
nodeSourceLinks.forEach(function (l) { |
|
sourceCount = l.circular ? sourceCount + 1 : sourceCount |
|
}) |
|
|
|
let nodeTargetLinks = link.target.targetLinks |
|
let targetCount = 0 |
|
nodeTargetLinks.forEach(function (l) { |
|
targetCount = l.circular ? targetCount + 1 : targetCount |
|
}) |
|
|
|
if (sourceCount > 1 || targetCount > 1) { |
|
return false |
|
} else { |
|
return true |
|
} |
|
} |
|
|
|
// creates vertical buffer values per set of top/bottom links |
|
function calcVerticalBuffer (links, circularLinkGap) { |
|
links.sort(sortLinkColumnAscending) |
|
links.forEach(function (link, i) { |
|
let buffer = 0 |
|
|
|
if (selfLinking(link) && onlyCircularLink(link)) { |
|
link.circularPathData.verticalBuffer = buffer + link.width / 2 |
|
} else { |
|
let j = 0 |
|
for (j; j < i; j++) { |
|
if (circularLinksCross(links[i], links[j])) { |
|
let bufferOverThisLink = |
|
links[j].circularPathData.verticalBuffer + |
|
links[j].width / 2 + |
|
circularLinkGap |
|
buffer = bufferOverThisLink > buffer ? bufferOverThisLink : buffer |
|
} |
|
} |
|
|
|
link.circularPathData.verticalBuffer = buffer + link.width / 2 |
|
} |
|
}) |
|
|
|
return links |
|
} |
|
|
|
// calculate the optimum path for a link to reduce overlaps |
|
function addCircularPathData (graph, circularLinkGap, y1) { |
|
//let baseRadius = 10 |
|
let buffer = 5 |
|
//let verticalMargin = 25 |
|
|
|
let minY = d3.min(graph.links, function (link) { |
|
return link.source.y0 |
|
}) |
|
|
|
// create object for circular Path Data |
|
graph.links.forEach(function (link) { |
|
if (link.circular) { |
|
link.circularPathData = {} |
|
} |
|
}) |
|
|
|
// calc vertical offsets per top/bottom links |
|
let topLinks = graph.links.filter(function (l) { |
|
return l.circularLinkType == 'top' |
|
}) |
|
topLinks = calcVerticalBuffer(topLinks, circularLinkGap) |
|
|
|
let bottomLinks = graph.links.filter(function (l) { |
|
return l.circularLinkType == 'bottom' |
|
}) |
|
bottomLinks = calcVerticalBuffer(bottomLinks, circularLinkGap) |
|
|
|
// add the base data for each link |
|
graph.links.forEach(function (link) { |
|
if (link.circular) { |
|
link.circularPathData.arcRadius = link.width + baseRadius |
|
link.circularPathData.leftNodeBuffer = buffer |
|
link.circularPathData.rightNodeBuffer = buffer |
|
link.circularPathData.sourceWidth = link.source.x1 - link.source.x0 |
|
link.circularPathData.sourceX = link.source.x0 + link.circularPathData.sourceWidth |
|
link.circularPathData.targetX = link.target.x0 |
|
link.circularPathData.sourceY = link.y0 |
|
link.circularPathData.targetY = link.y1 |
|
|
|
// for self linking paths, and that the only circular link in/out of that node |
|
if (selfLinking(link) && onlyCircularLink(link)) { |
|
link.circularPathData.leftSmallArcRadius = baseRadius + link.width / 2 |
|
link.circularPathData.leftLargeArcRadius = baseRadius + link.width / 2 |
|
link.circularPathData.rightSmallArcRadius = baseRadius + link.width / 2 |
|
link.circularPathData.rightLargeArcRadius = baseRadius + link.width / 2 |
|
|
|
if (link.circularLinkType == 'bottom') { |
|
link.circularPathData.verticalFullExtent = link.source.y1 + verticalMargin + link.circularPathData.verticalBuffer |
|
link.circularPathData.verticalLeftInnerExtent = link.circularPathData.verticalFullExtent - link.circularPathData.leftLargeArcRadius |
|
link.circularPathData.verticalRightInnerExtent = link.circularPathData.verticalFullExtent - link.circularPathData.rightLargeArcRadius |
|
} else { |
|
// top links |
|
link.circularPathData.verticalFullExtent = link.source.y0 - verticalMargin - link.circularPathData.verticalBuffer |
|
link.circularPathData.verticalLeftInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.leftLargeArcRadius |
|
link.circularPathData.verticalRightInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.rightLargeArcRadius |
|
} |
|
} else { |
|
// else calculate normally |
|
// add left extent coordinates, based on links with same source 'column' and circularLink type |
|
let thisColumn = link.source.column |
|
let thisCircularLinkType = link.circularLinkType |
|
let sameColumnLinks = graph.links.filter(function (l) { |
|
return ( |
|
l.source.column == thisColumn && |
|
l.circularLinkType == thisCircularLinkType |
|
) |
|
}) |
|
|
|
if (link.circularLinkType == 'bottom') { |
|
sameColumnLinks.sort(sortLinkSourceYDescending) |
|
} else { |
|
sameColumnLinks.sort(sortLinkSourceYAscending) |
|
} |
|
|
|
let radiusOffset = 0 |
|
sameColumnLinks.forEach(function (l, i) { |
|
if (l.circularLinkID == link.circularLinkID) { |
|
link.circularPathData.leftSmallArcRadius = baseRadius + link.width / 2 + radiusOffset |
|
link.circularPathData.leftLargeArcRadius = baseRadius + link.width / 2 + i * circularLinkGap + radiusOffset |
|
} |
|
radiusOffset = radiusOffset + l.width |
|
}) |
|
|
|
// add right extent coordinates, based on links with same target column and circularLink type |
|
thisColumn = link.target.column |
|
sameColumnLinks = graph.links.filter(function (l) { |
|
return ( |
|
l.target.column == thisColumn && |
|
l.circularLinkType == thisCircularLinkType |
|
) |
|
}) |
|
if (link.circularLinkType == 'bottom') { |
|
sameColumnLinks.sort(sortLinkTargetYDescending) |
|
} else { |
|
sameColumnLinks.sort(sortLinkTargetYAscending) |
|
} |
|
|
|
radiusOffset = 0 |
|
sameColumnLinks.forEach(function (l, i) { |
|
if (l.circularLinkID == link.circularLinkID) { |
|
link.circularPathData.rightSmallArcRadius = baseRadius + link.width / 2 + radiusOffset |
|
link.circularPathData.rightLargeArcRadius = baseRadius + link.width / 2 + i * circularLinkGap + radiusOffset |
|
} |
|
radiusOffset = radiusOffset + l.width |
|
}) |
|
|
|
// bottom links |
|
if (link.circularLinkType == 'bottom') { |
|
link.circularPathData.verticalFullExtent = y1 + verticalMargin + link.circularPathData.verticalBuffer |
|
link.circularPathData.verticalLeftInnerExtent = link.circularPathData.verticalFullExtent - link.circularPathData.leftLargeArcRadius |
|
link.circularPathData.verticalRightInnerExtent = link.circularPathData.verticalFullExtent - link.circularPathData.rightLargeArcRadius |
|
} else { |
|
// top links |
|
link.circularPathData.verticalFullExtent = minY - verticalMargin - link.circularPathData.verticalBuffer |
|
link.circularPathData.verticalLeftInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.leftLargeArcRadius |
|
link.circularPathData.verticalRightInnerExtent = link.circularPathData.verticalFullExtent + link.circularPathData.rightLargeArcRadius |
|
} |
|
} |
|
|
|
// all links |
|
link.circularPathData.leftInnerExtent = link.circularPathData.sourceX + link.circularPathData.leftNodeBuffer |
|
link.circularPathData.rightInnerExtent = link.circularPathData.targetX - link.circularPathData.rightNodeBuffer |
|
link.circularPathData.leftFullExtent = link.circularPathData.sourceX + link.circularPathData.leftLargeArcRadius + link.circularPathData.leftNodeBuffer |
|
link.circularPathData.rightFullExtent = link.circularPathData.targetX - link.circularPathData.rightLargeArcRadius - link.circularPathData.rightNodeBuffer |
|
|
|
|
|
} |
|
|
|
if (link.circular) { |
|
link.path = createCircularPathString(link) |
|
} else { |
|
var normalPath = d3.linkHorizontal() |
|
.source(function (d) { |
|
let x = d.source.x0 + (d.source.x1 - d.source.x0) |
|
let y = d.y0 |
|
return [x, y] |
|
}) |
|
.target(function (d) { |
|
let x = d.target.x0 |
|
let y = d.y1 |
|
return [x, y] |
|
}) |
|
link.path = normalPath(link) |
|
} |
|
|
|
|
|
}) |
|
} |
|
|
|
// create a d path using the addCircularPathData |
|
function createCircularPathString (link) { |
|
let pathString = '' |
|
let pathData = {} |
|
|
|
if (link.circularLinkType == 'top') { |
|
pathString = |
|
// start at the right of the source node |
|
'M' + |
|
link.circularPathData.sourceX + |
|
' ' + |
|
link.circularPathData.sourceY + |
|
' ' + |
|
// line right to buffer point |
|
'L' + |
|
link.circularPathData.leftInnerExtent + |
|
' ' + |
|
link.circularPathData.sourceY + |
|
' ' + |
|
// Arc around: Centre of arc X and //Centre of arc Y |
|
'A' + |
|
link.circularPathData.leftLargeArcRadius + |
|
' ' + |
|
link.circularPathData.leftSmallArcRadius + |
|
' 0 0 0 ' + |
|
// End of arc X //End of arc Y |
|
link.circularPathData.leftFullExtent + |
|
' ' + |
|
(link.circularPathData.sourceY - |
|
link.circularPathData.leftSmallArcRadius) + |
|
' ' + // End of arc X |
|
// line up to buffer point |
|
'L' + |
|
link.circularPathData.leftFullExtent + |
|
' ' + |
|
link.circularPathData.verticalLeftInnerExtent + |
|
' ' + |
|
// Arc around: Centre of arc X and //Centre of arc Y |
|
'A' + |
|
link.circularPathData.leftLargeArcRadius + |
|
' ' + |
|
link.circularPathData.leftLargeArcRadius + |
|
' 0 0 0 ' + |
|
// End of arc X //End of arc Y |
|
link.circularPathData.leftInnerExtent + |
|
' ' + |
|
link.circularPathData.verticalFullExtent + |
|
' ' + // End of arc X |
|
// line left to buffer point |
|
'L' + |
|
link.circularPathData.rightInnerExtent + |
|
' ' + |
|
link.circularPathData.verticalFullExtent + |
|
' ' + |
|
// Arc around: Centre of arc X and //Centre of arc Y |
|
'A' + |
|
link.circularPathData.rightLargeArcRadius + |
|
' ' + |
|
link.circularPathData.rightLargeArcRadius + |
|
' 0 0 0 ' + |
|
// End of arc X //End of arc Y |
|
link.circularPathData.rightFullExtent + |
|
' ' + |
|
link.circularPathData.verticalRightInnerExtent + |
|
' ' + // End of arc X |
|
// line down |
|
'L' + |
|
link.circularPathData.rightFullExtent + |
|
' ' + |
|
(link.circularPathData.targetY - |
|
link.circularPathData.rightSmallArcRadius) + |
|
' ' + |
|
// Arc around: Centre of arc X and //Centre of arc Y |
|
'A' + |
|
link.circularPathData.rightLargeArcRadius + |
|
' ' + |
|
link.circularPathData.rightSmallArcRadius + |
|
' 0 0 0 ' + |
|
// End of arc X //End of arc Y |
|
link.circularPathData.rightInnerExtent + |
|
' ' + |
|
link.circularPathData.targetY + |
|
' ' + // End of arc X |
|
// line to end |
|
'L' + |
|
link.circularPathData.targetX + |
|
' ' + |
|
link.circularPathData.targetY |
|
} else { |
|
// bottom path |
|
pathString = |
|
// start at the right of the source node |
|
'M' + |
|
link.circularPathData.sourceX + |
|
' ' + |
|
link.circularPathData.sourceY + |
|
' ' + |
|
// line right to buffer point |
|
'L' + |
|
link.circularPathData.leftInnerExtent + |
|
' ' + |
|
link.circularPathData.sourceY + |
|
' ' + |
|
// Arc around: Centre of arc X and //Centre of arc Y |
|
'A' + |
|
link.circularPathData.leftLargeArcRadius + |
|
' ' + |
|
link.circularPathData.leftSmallArcRadius + |
|
' 0 0 1 ' + |
|
// End of arc X //End of arc Y |
|
link.circularPathData.leftFullExtent + |
|
' ' + |
|
(link.circularPathData.sourceY + |
|
link.circularPathData.leftSmallArcRadius) + |
|
' ' + // End of arc X |
|
// line down to buffer point |
|
'L' + |
|
link.circularPathData.leftFullExtent + |
|
' ' + |
|
link.circularPathData.verticalLeftInnerExtent + |
|
' ' + |
|
// Arc around: Centre of arc X and //Centre of arc Y |
|
'A' + |
|
link.circularPathData.leftLargeArcRadius + |
|
' ' + |
|
link.circularPathData.leftLargeArcRadius + |
|
' 0 0 1 ' + |
|
// End of arc X //End of arc Y |
|
link.circularPathData.leftInnerExtent + |
|
' ' + |
|
link.circularPathData.verticalFullExtent + |
|
' ' + // End of arc X |
|
// line left to buffer point |
|
'L' + |
|
link.circularPathData.rightInnerExtent + |
|
' ' + |
|
link.circularPathData.verticalFullExtent + |
|
' ' + |
|
// Arc around: Centre of arc X and //Centre of arc Y |
|
'A' + |
|
link.circularPathData.rightLargeArcRadius + |
|
' ' + |
|
link.circularPathData.rightLargeArcRadius + |
|
' 0 0 1 ' + |
|
// End of arc X //End of arc Y |
|
link.circularPathData.rightFullExtent + |
|
' ' + |
|
link.circularPathData.verticalRightInnerExtent + |
|
' ' + // End of arc X |
|
// line up |
|
'L' + |
|
link.circularPathData.rightFullExtent + |
|
' ' + |
|
(link.circularPathData.targetY + |
|
link.circularPathData.rightSmallArcRadius) + |
|
' ' + |
|
// Arc around: Centre of arc X and //Centre of arc Y |
|
'A' + |
|
link.circularPathData.rightLargeArcRadius + |
|
' ' + |
|
link.circularPathData.rightSmallArcRadius + |
|
' 0 0 1 ' + |
|
// End of arc X //End of arc Y |
|
link.circularPathData.rightInnerExtent + |
|
' ' + |
|
link.circularPathData.targetY + |
|
' ' + // End of arc X |
|
// line to end |
|
'L' + |
|
link.circularPathData.targetX + |
|
' ' + |
|
link.circularPathData.targetY |
|
} |
|
|
|
return pathString |
|
} |
|
|
|
// sort links based on the distance between the source and tartget node columns |
|
// if the same, then use Y position of the source node |
|
function sortLinkColumnAscending (link1, link2) { |
|
if (linkColumnDistance(link1) == linkColumnDistance(link2)) { |
|
return link1.circularLinkType == 'bottom' |
|
? sortLinkSourceYDescending(link1, link2) |
|
: sortLinkSourceYAscending(link1, link2) |
|
} else { |
|
return linkColumnDistance(link2) - linkColumnDistance(link1) |
|
} |
|
} |
|
|
|
// sort ascending links by their source vertical position, y0 |
|
function sortLinkSourceYAscending (link1, link2) { |
|
return link1.y0 - link2.y0 |
|
} |
|
|
|
// sort descending links by their source vertical position, y0 |
|
function sortLinkSourceYDescending (link1, link2) { |
|
return link2.y0 - link1.y0 |
|
} |
|
|
|
// sort ascending links by their target vertical position, y1 |
|
function sortLinkTargetYAscending (link1, link2) { |
|
return link1.y1 - link2.y1 |
|
} |
|
|
|
// sort descending links by their target vertical position, y1 |
|
function sortLinkTargetYDescending (link1, link2) { |
|
return link2.y1 - link1.y1 |
|
} |
|
|
|
// return the distance between the link's target and source node, in terms of the nodes' column |
|
function linkColumnDistance (link) { |
|
return link.target.column - link.source.column |
|
} |
|
|
|
// return the distance between the link's target and source node, in terms of the nodes' X coordinate |
|
function linkXLength (link) { |
|
return link.target.x0 - link.source.x1 |
|
} |
|
|
|
// Return the Y coordinate on the longerLink path * which is perpendicular shorterLink's source. |
|
// * approx, based on a straight line from target to source, when in fact the path is a bezier |
|
function linkPerpendicularYToLinkSource (longerLink, shorterLink) { |
|
// get the angle for the longer link |
|
let angle = linkAngle(longerLink) |
|
|
|
// get the adjacent length to the other link's x position |
|
let heightFromY1ToPependicular = linkXLength(shorterLink) / Math.tan(angle) |
|
|
|
// add or subtract from longer link1's original y1, depending on the slope |
|
let yPerpendicular = incline(longerLink) == 'up' |
|
? longerLink.y1 + heightFromY1ToPependicular |
|
: longerLink.y1 - heightFromY1ToPependicular |
|
|
|
return yPerpendicular |
|
} |
|
|
|
// Return the Y coordinate on the longerLink path * which is perpendicular shorterLink's source. |
|
// * approx, based on a straight line from target to source, when in fact the path is a bezier |
|
function linkPerpendicularYToLinkTarget (longerLink, shorterLink) { |
|
// get the angle for the longer link |
|
let angle = linkAngle(longerLink) |
|
|
|
// get the adjacent length to the other link's x position |
|
let heightFromY1ToPependicular = linkXLength(shorterLink) / Math.tan(angle) |
|
|
|
// add or subtract from longer link's original y1, depending on the slope |
|
let yPerpendicular = incline(longerLink) == 'up' |
|
? longerLink.y1 - heightFromY1ToPependicular |
|
: longerLink.y1 + heightFromY1ToPependicular |
|
|
|
return yPerpendicular |
|
} |
|
|
|
// Move any nodes that overlap links which span 2+ columns |
|
function resolveNodeLinkOverlaps (graph, y0, y1) { |
|
graph.links.forEach(function (link) { |
|
if (link.circular) { |
|
return |
|
} |
|
|
|
if (link.target.column - link.source.column > 1) { |
|
let columnToTest = link.source.column + 1 |
|
let maxColumnToTest = link.target.column - 1 |
|
|
|
let i = 1 |
|
let numberOfColumnsToTest = maxColumnToTest - columnToTest + 1 |
|
|
|
for ( |
|
columnToTest, (i = 1); |
|
columnToTest <= maxColumnToTest; |
|
columnToTest++, i++ |
|
) { |
|
graph.nodes.forEach(function (node) { |
|
if (node.column == columnToTest) { |
|
let t = i / (numberOfColumnsToTest + 1) |
|
|
|
// Find all the points of a cubic bezier curve in javascript |
|
// https://stackoverflow.com/questions/15397596/find-all-the-points-of-a-cubic-bezier-curve-in-javascript |
|
|
|
let B0_t = Math.pow(1 - t, 3) |
|
let B1_t = 3 * t * Math.pow(1 - t, 2) |
|
let B2_t = 3 * Math.pow(t, 2) * (1 - t) |
|
let B3_t = Math.pow(t, 3) |
|
|
|
let py_t = |
|
B0_t * link.y0 + |
|
B1_t * link.y0 + |
|
B2_t * link.y1 + |
|
B3_t * link.y1 |
|
|
|
let linkY0AtColumn = py_t - (link.width / 2) |
|
let linkY1AtColumn = py_t + (link.width / 2) |
|
|
|
|
|
|
|
// If top of link overlaps node, push node up |
|
if (linkY0AtColumn > node.y0 && linkY0AtColumn < node.y1) { |
|
|
|
let dy = node.y1 - linkY0AtColumn + 10 |
|
dy = node.circularLinkType == 'bottom' ? dy : -dy |
|
|
|
node = adjustNodeHeight(node, dy, y0, y1) |
|
|
|
// check if other nodes need to move up too |
|
graph.nodes.forEach(function (otherNode) { |
|
// don't need to check itself or nodes at different columns |
|
if ( |
|
otherNode.name == node.name || |
|
otherNode.column != node.column |
|
) { |
|
return |
|
} |
|
if (nodesOverlap(node, otherNode)) { |
|
adjustNodeHeight(otherNode, dy, y0, y1) |
|
} |
|
}) |
|
} else if (linkY1AtColumn > node.y0 && linkY1AtColumn < node.y1) { |
|
// If bottom of link overlaps node, push node down |
|
let dy = linkY1AtColumn - node.y0 + 10 |
|
|
|
node = adjustNodeHeight(node, dy, y0, y1) |
|
|
|
// check if other nodes need to move down too |
|
graph.nodes.forEach(function (otherNode) { |
|
// don't need to check itself or nodes at different columns |
|
if ( |
|
otherNode.name == node.name || |
|
otherNode.column != node.column |
|
) { |
|
return |
|
} |
|
if (otherNode.y0 < node.y1 && otherNode.y1 > node.y1) { |
|
adjustNodeHeight(otherNode, dy, y0, y1) |
|
} |
|
}) |
|
} else if (linkY0AtColumn < node.y0 && linkY1AtColumn > node.y1) { |
|
// if link completely overlaps node |
|
let dy = linkY1AtColumn - node.y0 + 10 |
|
|
|
node = adjustNodeHeight(node, dy, y0, y1) |
|
|
|
graph.nodes.forEach(function (otherNode) { |
|
// don't need to check itself or nodes at different columns |
|
if ( |
|
otherNode.name == node.name || |
|
otherNode.column != node.column |
|
) { |
|
return |
|
} |
|
if (otherNode.y0 < node.y1 && otherNode.y1 > node.y1) { |
|
adjustNodeHeight(otherNode, dy, y0, y1) |
|
} |
|
}) |
|
} |
|
} |
|
}) |
|
} |
|
} |
|
}) |
|
} |
|
|
|
// check if two nodes overlap |
|
function nodesOverlap (nodeA, nodeB) { |
|
// test if nodeA top partially overlaps nodeB |
|
if (nodeA.y0 > nodeB.y0 && nodeA.y0 < nodeB.y1) { |
|
return true |
|
} else if (nodeA.y1 > nodeB.y0 && nodeA.y1 < nodeB.y1) { |
|
// test if nodeA bottom partially overlaps nodeB |
|
return true |
|
} else if (nodeA.y0 < nodeB.y0 && nodeA.y1 > nodeB.y1) { |
|
// test if nodeA covers nodeB |
|
return true |
|
} else { |
|
return false |
|
} |
|
} |
|
|
|
// update a node, and its associated links, vertical positions (y0, y1) |
|
function adjustNodeHeight (node, dy, sankeyY0, sankeyY1) { |
|
if ((node.y0 + dy >= sankeyY0) && (node.y1 + dy <= sankeyY1)) { |
|
node.y0 = node.y0 + dy |
|
node.y1 = node.y1 + dy |
|
|
|
node.targetLinks.forEach(function (l) { |
|
l.y1 = l.y1 + dy |
|
}) |
|
|
|
node.sourceLinks.forEach(function (l) { |
|
l.y0 = l.y0 + dy |
|
}) |
|
} |
|
return node |
|
} |
|
|
|
// sort and set the links' y0 for each node |
|
function sortSourceLinks (graph, y1) { |
|
graph.nodes.forEach(function (node) { |
|
// move any nodes up which are off the bottom |
|
if (node.y + (node.y1 - node.y0) > y1) { |
|
node.y = node.y - (node.y + (node.y1 - node.y0) - y1) |
|
} |
|
|
|
let nodesSourceLinks = graph.links.filter(function (l) { |
|
return l.source.name == node.name |
|
}) |
|
|
|
let nodeSourceLinksLength = nodesSourceLinks.length |
|
|
|
// if more than 1 link then sort |
|
if (nodeSourceLinksLength > 1) { |
|
nodesSourceLinks.sort(function (link1, link2) { |
|
// if both are not circular... |
|
if (!link1.circular && !link2.circular) { |
|
// if the target nodes are the same column, then sort by the link's target y |
|
if (link1.target.column == link2.target.column) { |
|
return link1.y1 - link2.y1 |
|
} else if (!sameInclines(link1, link2)) { |
|
// if the links slope in different directions, then sort by the link's target y |
|
return link1.y1 - link2.y1 |
|
|
|
// if the links slope in same directions, then sort by any overlap |
|
} else { |
|
if (link1.target.column > link2.target.column) { |
|
let link2Adj = linkPerpendicularYToLinkTarget(link2, link1) |
|
return link1.y1 - link2Adj |
|
} |
|
if (link2.target.column > link1.target.column) { |
|
let link1Adj = linkPerpendicularYToLinkTarget(link1, link2) |
|
return link1Adj - link2.y1 |
|
} |
|
} |
|
} |
|
|
|
// if only one is circular, the move top links up, or bottom links down |
|
if (link1.circular && !link2.circular) { |
|
return link1.circularLinkType == 'top' ? -1 : 1 |
|
} else if (link2.circular && !link1.circular) { |
|
return link2.circularLinkType == 'top' ? 1 : -1 |
|
} |
|
|
|
// if both links are circular... |
|
if (link1.circular && link2.circular) { |
|
// ...and they both loop the same way (both top) |
|
if ( |
|
link1.circularLinkType === link2.circularLinkType && |
|
link1.circularLinkType == 'top' |
|
) { |
|
// ...and they both connect to a target with same column, then sort by the target's y |
|
if (link1.target.column === link2.target.column) { |
|
return link1.target.y1 - link2.target.y1 |
|
} else { |
|
// ...and they connect to different column targets, then sort by how far back they |
|
return link2.target.column - link1.target.column |
|
} |
|
} else if ( |
|
link1.circularLinkType === link2.circularLinkType && |
|
link1.circularLinkType == 'bottom' |
|
) { |
|
// ...and they both loop the same way (both bottom) |
|
// ...and they both connect to a target with same column, then sort by the target's y |
|
if (link1.target.column === link2.target.column) { |
|
return link2.target.y1 - link1.target.y1 |
|
} else { |
|
// ...and they connect to different column targets, then sort by how far back they |
|
return link1.target.column - link2.target.column |
|
} |
|
} else { |
|
// ...and they loop around different ways, the move top up and bottom down |
|
return link1.circularLinkType == 'top' ? -1 : 1 |
|
} |
|
} |
|
}) |
|
} |
|
|
|
// update y0 for links |
|
let ySourceOffset = node.y0 |
|
|
|
nodesSourceLinks.forEach(function (link) { |
|
link.y0 = ySourceOffset + link.width / 2 |
|
ySourceOffset = ySourceOffset + link.width |
|
}) |
|
|
|
// correct any circular bottom links so they are at the bottom of the node |
|
nodesSourceLinks.forEach(function (link, i) { |
|
if (link.circularLinkType == 'bottom') { |
|
let j = i + 1 |
|
let offsetFromBottom = 0 |
|
// sum the widths of any links that are below this link |
|
for (j; j < nodeSourceLinksLength; j++) { |
|
offsetFromBottom = offsetFromBottom + nodesSourceLinks[j].width |
|
} |
|
link.y0 = node.y1 - offsetFromBottom - link.width / 2 |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
// sort and set the links' y1 for each node |
|
function sortTargetLinks (graph, y1) { |
|
graph.nodes.forEach(function (node) { |
|
let nodesTargetLinks = graph.links.filter(function (l) { |
|
return l.target.name == node.name |
|
}) |
|
|
|
let nodesTargetLinksLength = nodesTargetLinks.length |
|
|
|
if (nodesTargetLinksLength > 1) { |
|
nodesTargetLinks.sort(function (link1, link2) { |
|
// if both are not circular, the base on the source y position |
|
if (!link1.circular && !link2.circular) { |
|
if (link1.source.column == link2.source.column) { |
|
return link1.y0 - link2.y0 |
|
} else if (!sameInclines(link1, link2)) { |
|
return link1.y0 - link2.y0 |
|
} else { |
|
// get the angle of the link to the further source node (ie the smaller column) |
|
if (link2.source.column < link1.source.column) { |
|
let link2Adj = linkPerpendicularYToLinkSource(link2, link1) |
|
|
|
return link1.y0 - link2Adj |
|
} |
|
if (link1.source.column < link2.source.column) { |
|
let link1Adj = linkPerpendicularYToLinkSource(link1, link2) |
|
|
|
return link1Adj - link2.y0 |
|
} |
|
} |
|
} |
|
|
|
// if only one is circular, the move top links up, or bottom links down |
|
if (link1.circular && !link2.circular) { |
|
return link1.circularLinkType == 'top' ? -1 : 1 |
|
} else if (link2.circular && !link1.circular) { |
|
return link2.circularLinkType == 'top' ? 1 : -1 |
|
} |
|
|
|
// if both links are circular... |
|
if (link1.circular && link2.circular) { |
|
// ...and they both loop the same way (both top) |
|
if ( |
|
link1.circularLinkType === link2.circularLinkType && |
|
link1.circularLinkType == 'top' |
|
) { |
|
// ...and they both connect to a target with same column, then sort by the target's y |
|
if (link1.source.column === link2.source.column) { |
|
return link1.source.y1 - link2.source.y1 |
|
} else { |
|
// ...and they connect to different column targets, then sort by how far back they |
|
return link1.source.column - link2.source.column |
|
} |
|
} else if ( |
|
link1.circularLinkType === link2.circularLinkType && |
|
link1.circularLinkType == 'bottom' |
|
) { |
|
// ...and they both loop the same way (both bottom) |
|
// ...and they both connect to a target with same column, then sort by the target's y |
|
if (link1.source.column === link2.source.column) { |
|
return link1.source.y1 - link2.source.y1 |
|
} else { |
|
// ...and they connect to different column targets, then sort by how far back they |
|
return link2.source.column - link1.source.column |
|
} |
|
} else { |
|
// ...and they loop around different ways, the move top up and bottom down |
|
return link1.circularLinkType == 'top' ? -1 : 1 |
|
} |
|
} |
|
}) |
|
} |
|
|
|
// update y1 for links |
|
let yTargetOffset = node.y0 |
|
|
|
nodesTargetLinks.forEach(function (link) { |
|
link.y1 = yTargetOffset + link.width / 2 |
|
yTargetOffset = yTargetOffset + link.width |
|
}) |
|
|
|
// correct any circular bottom links so they are at the bottom of the node |
|
nodesTargetLinks.forEach(function (link, i) { |
|
if (link.circularLinkType == 'bottom') { |
|
let j = i + 1 |
|
let offsetFromBottom = 0 |
|
// sum the widths of any links that are below this link |
|
for (j; j < nodesTargetLinksLength; j++) { |
|
offsetFromBottom = offsetFromBottom + nodesTargetLinks[j].width |
|
} |
|
link.y1 = node.y1 - offsetFromBottom - link.width / 2 |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
// test if links both slope up, or both slope down |
|
function sameInclines (link1, link2) { |
|
return incline(link1) == incline(link2) |
|
} |
|
|
|
// returns the slope of a link, from source to target |
|
// up => slopes up from source to target |
|
// down => slopes down from source to target |
|
function incline (link) { |
|
return link.y0 - link.y1 > 0 ? 'up' : 'down' |
|
} |
|
|
|
// check if link is self linking, ie links a node to the same node |
|
function selfLinking (link) { |
|
return link.source.name == link.target.name |
|
} |
|
|
|
/// //////////////////////////////////////////////////////////////////////////// |
|
|
|
exports.sankeyCircular = sankey |
|
exports.sankeyCenter = center |
|
exports.sankeyLeft = left |
|
exports.sankeyRight = right |
|
exports.sankeyJustify = justify |
|
|
|
Object.defineProperty(exports, '__esModule', { value: true }) |
|
}) |
|
|
|
|
|
|
|
|