// 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' function targetDepth (d) { return d.target.depth } function left (node) { return node.depth } function right (node, n) { return n - 1 - node.height } function justify (node, n) { return node.sourceLinks.length ? node.depth : n - 1 } function center (node) { return node.targetLinks.length ? node.depth : node.sourceLinks.length ? d3Array.min(node.sourceLinks, targetDepth) - 1 : 0 } function constant (x) { return function () { return x } } function ascendingSourceBreadth (a, b) { return ascendingBreadth(a.source, b.source) || a.index - b.index } function ascendingTargetBreadth (a, b) { return ascendingBreadth(a.target, b.target) || a.index - b.index } function ascendingBreadth (a, b) { if (a.partOfCycle === b.partOfCycle) { return a.y0 - b.y0 } else { if (a.circularLinkType === 'top' || b.circularLinkType === 'bottom') { return -1 } else { return 1 } } } function value (d) { return d.value } function nodeCenter (node) { return (node.y0 + node.y1) / 2 } function linkSourceCenter (link) { return nodeCenter(link.source) } function linkTargetCenter (link) { return nodeCenter(link.target) } function weightedSource (link) { return nodeCenter(link.source) * link.value } function weightedTarget (link) { return nodeCenter(link.target) * link.value } function defaultId (d) { return d.index } function defaultNodes (graph) { return graph.nodes } function defaultLinks (graph) { return graph.links } function find (nodeById, id) { var node = nodeById.get(id) if (!node) throw new Error('missing: ' + id) return node } var sankey = function () { var x0 = 0, y0 = 0, x1 = 1, y1 = 1, // extent dx = 24, // nodeWidth py, // nodePadding scale = 1, id = defaultId, align = justify, nodes = defaultNodes, links = defaultLinks, iterations = 32 var padding = Infinity var paddingRatio = 0.1 function sankey () { var graph = { nodes: nodes.apply(null, arguments), links: links.apply(null, arguments) } computeNodeLinks(graph) identifyCircles(graph) selectCircularLinkTypes(graph) computeNodeValues(graph) computeNodeDepths(graph) computeNodeBreadths(graph, iterations) computeLinkBreadths(graph) //sort links per node, based on the links' source/target positions sortSourceLinks(graph) sortTargetLinks(graph) //adjust nodes that overlap links that span 2+ depths resolveNodeLinkOverlaps(graph) //sort links per node, based on the links' source/target positions sortSourceLinks(graph) sortTargetLinks(graph) //add d string for circular paths addCircularPathData(graph); return graph } sankey.update = function (graph) { computeLinkBreadths(graph) return graph } sankey.nodeId = function (_) { return arguments.length ? ((id = typeof _ === 'function' ? _ : constant(_)), sankey) : id } sankey.nodeAlign = function (_) { return arguments.length ? ((align = typeof _ === 'function' ? _ : constant(_)), sankey) : align } sankey.nodeWidth = function (_) { return arguments.length ? ((dx = +_), sankey) : dx } sankey.nodePadding = function (_) { return arguments.length ? ((py = +_), sankey) : py } sankey.scale = function (_) { return arguments.length ? ((scale = +_), sankey) : scale } sankey.nodes = function (_) { return arguments.length ? ((nodes = typeof _ === 'function' ? _ : constant(_)), sankey) : nodes } sankey.links = function (_) { return arguments.length ? ((links = typeof _ === 'function' ? _ : constant(_)), sankey) : links } sankey.size = function (_) { return arguments.length ? ((x0 = y0 = 0), (x1 = +_[0]), (y1 = +_[1]), sankey) : [x1 - x0, y1 - y0] } sankey.extent = function (_) { return arguments.length ? ((x0 = +_[0][0]), (x1 = +_[1][0]), (y0 = +_[0][1]), (y1 = +_[1][ 1 ]), sankey) : [[x0, y0], [x1, y1]] } sankey.iterations = function (_) { return arguments.length ? ((iterations = +_), sankey) : iterations } sankey.nodePaddingRatio = function (_) { return arguments.length ? ((paddingRatio = +_), sankey) : paddingRatio } // Populate the sourceLinks and targetLinks for each node. // Also, if the source and target are not objects, assume they are indices. function computeNodeLinks (graph) { graph.nodes.forEach(function (node, i) { node.index = i node.sourceLinks = [] node.targetLinks = [] }) var nodeById = d3Collection.map(graph.nodes, id) graph.links.forEach(function (link, i) { link.index = i var source = link.source var target = link.target if (typeof source !== 'object') { source = link.source = find(nodeById, source) } if (typeof target !== 'object') { target = link.target = find(nodeById, target) } source.sourceLinks.push(link) target.targetLinks.push(link) }) } // Compute the value (size) and cycleness of each node by summing the associated links. function computeNodeValues (graph) { graph.nodes.forEach(function (node) { node.partOfCycle = false node.value = Math.max( d3Array.sum(node.sourceLinks, value), d3Array.sum(node.targetLinks, value) ) node.sourceLinks.forEach(function (link) { if (link.circular) { node.partOfCycle = true node.circularLinkType = link.circularLinkType } }) node.targetLinks.forEach(function (link) { if (link.circular) { node.partOfCycle = true node.circularLinkType = link.circularLinkType } }) }) } // Iteratively assign the depth (x-position) for each node. // Nodes are assigned the maximum depth of incoming neighbors plus one; // nodes with no incoming links are assigned depth zero, while // nodes with no outgoing links are assigned the maximum depth. function computeNodeDepths (graph) { var nodes, next, x for ( (nodes = graph.nodes), (next = []), (x = 0); nodes.length; ++x, (nodes = next), (next = []) ) { nodes.forEach(function (node) { node.depth = x node.sourceLinks.forEach(function (link) { if (next.indexOf(link.target) < 0 && !link.circular) { next.push(link.target) } }) }) } for ( (nodes = graph.nodes), (next = []), (x = 0); nodes.length; ++x, (nodes = next), (next = []) ) { nodes.forEach(function (node) { node.height = x node.targetLinks.forEach(function (link) { if (next.indexOf(link.source) < 0 && !link.circular) { next.push(link.source) } }) }) } var kx = (x1 - x0 - dx) / (x - 1) graph.nodes.forEach(function (node) { node.x1 = (node.x0 = x0 + Math.max( 0, Math.min(x - 1, Math.floor(align.call(null, node, x))) ) * kx) + dx }) } function computeNodeBreadths (graph) { var columns = d3Collection .nest() .key(function (d) { return d.x0 }) .sortKeys(d3Array.ascending) .entries(graph.nodes) .map(function (d) { return d.values }) initializeNodeBreadth() resolveCollisions() for (var alpha = 1, n = iterations; n > 0; --n) { // relaxRightToLeft((alpha *= 0.99)) // resolveCollisions() // relaxLeftToRight((alpha *= 0.99)) // resolveCollisions() relaxLeftAndRight((alpha *= 0.99)) resolveCollisions() } function initializeNodeBreadth () { columns.forEach(function (nodes) { let thisPadding = y1 * paddingRatio / (nodes.length + 1) padding = thisPadding < padding ? thisPadding : padding }) py = padding var ky = d3Array.min(columns, function (nodes) { return (y1 - y0 - (nodes.length - 1) * py) / d3Array.sum(nodes, value) }) ky = ky * scale columns.forEach(function (nodes) { var nodesLength = nodes.length nodes.forEach(function (node, i) { if (node.partOfCycle) { if (node.circularLinkType == 'top') { node.y0 = y0 + i node.y1 = node.y0 + node.value * ky } else { node.y0 = y1 - (node.value * ky) - i node.y1 = node.y0 + node.value * ky } } else { // node.y1 = (node.y0 = i) + node.value * ky node.y0 = (y1 - y0) / 2 - nodesLength / 2 + i node.y1 = node.y0 + node.value * ky } }) }) graph.links.forEach(function (link) { link.width = link.value * ky }) } function relaxLeftAndRight (alpha) { let columnsLength = columns.length // console.log("cols: " + columnsLength); columns.forEach(function (nodes, i) { let n = nodes.length let depth = nodes[0].depth // console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') // console.log(depth + ': ' + n) nodes.forEach(function (node) { // check the node is not an orphan if (node.sourceLinks.length || node.targetLinks.length) { if (node.partOfCycle /*&& n > 1 /* && depth != 0 */) { // console.log('do nothing') } else if (depth == 0 && n == 1) { let nodeHeight = node.y1 - node.y0 node.y0 = y1 / 2 - nodeHeight / 2 node.y1 = y1 / 2 + nodeHeight / 2 } else if (depth == columnsLength - 1 && n == 1) { let nodeHeight = node.y1 - node.y0 node.y0 = y1 / 2 - nodeHeight / 2 node.y1 = y1 / 2 + nodeHeight / 2 } else { let avg = 0 let avgTargetY = d3Array.mean( node.sourceLinks, linkTargetCenter ) let avgSourceY = d3Array.mean( node.targetLinks, linkSourceCenter ) if (avgTargetY && avgSourceY) { avg = (avgTargetY + avgSourceY) / 2 } else { avg = avgTargetY || avgSourceY } let dy = (avg - nodeCenter(node)) * alpha // positive if it node needs to move down // let dy = (nodeCenter(node.sourceLinks[0].target) - nodeCenter(node.sourceLinks[0].source) / 2) * alpha; node.y0 += dy node.y1 += dy // console.log('after: ' + node.y0 + ' ' + node.y1) } } }) }) } function resolveCollisions () { columns.forEach(function (nodes) { var node, dy, y = y0, n = nodes.length, i // Push any overlapping nodes down. nodes.sort(ascendingBreadth) for (i = 0; i < n; ++i) { node = nodes[i] dy = y - node.y0 if (dy > 0) { node.y0 += dy node.y1 += dy } y = node.y1 + py } // If the bottommost node goes outside the bounds, push it back up. dy = y - py - y1 if (dy > 0) { ;(y = node.y0 -= dy), (node.y1 -= dy) // Push any overlapping nodes back up. for (i = n - 2; i >= 0; --i) { node = nodes[i] dy = node.y1 + py - y if (dy > 0) (node.y0 -= dy), (node.y1 -= dy) y = node.y0 } } }) } } function computeLinkBreadths (graph) { graph.nodes.forEach(function (node) { node.sourceLinks.sort(ascendingTargetBreadth) node.targetLinks.sort(ascendingSourceBreadth) }) graph.nodes.forEach(function (node) { var y0 = node.y0 var y1 = y0 // start from the bottom of the node for cycle links var y0cycle = node.y1 var y1cycle = y0cycle node.sourceLinks.forEach(function (link) { if (link.circular) { link.y0 = y0cycle - link.width / 2 y0cycle = y0cycle - link.width } else { link.y0 = y0 + link.width / 2 y0 += link.width } }) node.targetLinks.forEach(function (link) { if (link.circular) { link.y1 = y1cycle - link.width / 2 y1cycle = y1cycle - link.width } else { link.y1 = y1 + link.width / 2 y1 += link.width } }) }) } return sankey } /// ///////////////////////////////////////////////////////////////////////////////// // Cycle functions //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) } }) } function selectCircularLinkTypes (graph) { let numberOfTops = 0 let numberOfBottoms = 0 graph.links.forEach(function (link) { if (link.circular) { // if either souce or target has type already use that if (link.source.circularLinkType || link.target.circularLinkType) { // default to source type if available link.circularLinkType = link.source.circularLinkType ? link.source.circularLinkType : link.target.circularLinkType } else { link.circularLinkType = numberOfTops < numberOfBottoms ? 'top' : 'bottom' } if (link.circularLinkType == 'top') { numberOfTops = numberOfTops + 1 } else { numberOfBottoms = numberOfBottoms + 1 } graph.nodes.forEach(function (node) { if (node.name == link.source.name || node.name == link.target.name) { node.circularLinkType = link.circularLinkType } }) } }) } // Checks if link creates a cycle function createsCycle (originalSource, nodeToCheck, graph) { if (graph.length == 0) { return false } var nextLinks = findLinksOutward(nodeToCheck, graph) // leaf node check if (nextLinks.length == 0) { return false } // cycle check for (var i = 0; i < nextLinks.length; i++) { var nextLink = nextLinks[i] if (nextLink.target === originalSource) { return true } // Recurse if (createsCycle(originalSource, nextLink.target, graph)) { return true } } // Exhausted all links return false } /* Given a node, find all links for which this is a source in the current 'known' graph */ function findLinksOutward (node, graph) { var children = [] for (var i = 0; i < graph.length; i++) { if (node == graph[i].source) { children.push(graph[i]) } } return children } // Create a normal curve or circular curve //var curveSankeyForceLink = // Return the angle between a straight line between the source and target of the link, and the vertical plane of the node function linkAngle (link) { let adjacent = Math.abs(link.y1 - link.y0) let opposite = Math.abs(link.target.x0 - link.source.x1) return Math.atan(opposite / adjacent) } function circularLinksCross (link1, link2) { if (link1.source.depth < link2.target.depth) { return false } else if (link1.target.depth > link2.source.depth) { return false } else { return true } } function calcVerticalBuffer (links) { links.sort(sortLinkDepthAscending) links.forEach(function (link, i) { let buffer = 0 let j = 0 for (j; j < i; j++) { if (circularLinksCross(links[i], links[j])) { let bufferOverThisLink = links[j].circularPathData.verticalBuffer + links[j].width / 2 + circularLinkGap buffer = bufferOverThisLink > buffer ? bufferOverThisLink : buffer } } link.circularPathData.verticalBuffer = buffer + link.width / 2 }) return links } // calculate the optimum path for a link to reduce overlaps function addCircularPathData (graph) { let baseRadius = 10; let buffer = 10; 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) let bottomLinks = graph.links.filter(function (l) { return l.circularLinkType == 'bottom' }) bottomLinks = calcVerticalBuffer(bottomLinks) // add the base data for each link graph.links.forEach(function (link) { if (link.circular) { link.circularPathData.arcRadius = link.width + baseRadius link.circularPathData.leftNodeBuffer = 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 // add left extent coordinates, based on links with same source depth and circularLink type let thisDepth = link.source.depth let thisCircularLinkType = link.circularLinkType let sameDepthLinks = graph.links.filter(function (l) { return ( l.source.depth == thisDepth && l.circularLinkType == thisCircularLinkType ) }) if (link.circularLinkType == 'bottom') { sameDepthLinks.sort(sortLinkSourceYDescending) } else { sameDepthLinks.sort(sortLinkSourceYAscending) } let radiusOffset = 0; sameDepthLinks.forEach(function (l, i) { if (l.circularLinkID == link.circularLinkID) { link.circularPathData.leftSmallArcRadius = baseRadius + link.width/2 + radiusOffset link.circularPathData.leftLargeArcRadius = baseRadius + link.width/2 + i * circularLinkGap + radiusOffset } radiusOffset = radiusOffset + l.width }) // add right extent coordinates, based on links with same target depth and circularLink type thisDepth = link.target.depth sameDepthLinks = graph.links.filter(function (l) { return ( l.target.depth == thisDepth && l.circularLinkType == thisCircularLinkType ) }) if (link.circularLinkType == 'bottom') { sameDepthLinks.sort(sortLinkTargetYDescending) } else { sameDepthLinks.sort(sortLinkTargetYAscending) } radiusOffset = 0 sameDepthLinks.forEach(function (l, i) { if (l.circularLinkID == link.circularLinkID) { link.circularPathData.rightSmallArcRadius = baseRadius + link.width/2 + radiusOffset link.circularPathData.rightLargeArcRadius = baseRadius + link.width/2 + i * circularLinkGap + radiusOffset } radiusOffset = radiusOffset + l.width }) // all links link.circularPathData.leftInnerExtent = link.circularPathData.sourceX + link.circularPathData.leftNodeBuffer link.circularPathData.rightInnerExtent = link.circularPathData.targetX - link.circularPathData.rightNodeBuffer link.circularPathData.leftFullExtent = link.circularPathData.sourceX + link.circularPathData.leftLargeArcRadius + link.circularPathData.leftNodeBuffer link.circularPathData.rightFullExtent = link.circularPathData.targetX - link.circularPathData.rightLargeArcRadius - link.circularPathData.rightNodeBuffer // bottom links if (link.circularLinkType == 'bottom') { link.circularPathData.verticalFullExtent = height + 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 } link.circularPathData.path = createCircularPathString(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 depths // if the same, then use Y position of the source node function sortLinkDepthAscending (link1, link2) { if (linkDepthDistance(link1) == linkDepthDistance(link2)) { return link1.circularLinkType == 'bottom' ? sortLinkSourceYDescending(link1, link2) : sortLinkSourceYAscending(link1, link2) } else { // return linkDepthDistance(link1) - linkDepthDistance(link2); return linkDepthDistance(link2) - linkDepthDistance(link1) } } function sortLinkSourceYAscending (link1, link2) { return link1.y0 - link2.y0 } function sortLinkSourceYDescending (link1, link2) { return link2.y0 - link1.y0 } function sortLinkTargetYAscending (link1, link2) { return link1.y1 - link2.y1 } function sortLinkTargetYDescending (link1, link2) { return link2.y1 - link1.y1 } // return the distance between the link's target and source node, in terms of the nodes' depth function linkDepthDistance (link) { return link.target.depth - link.source.depth } // return the distance between the link's target and source node, in terms of the nodes' X coordinate function linkXLength (link) { return link.target.x0 - link.source.x1 } function linkPerpendicularYToLinkSource (longerLink, shorterLink) { // Return the Y coordinate on the longerLink path * which is perpendicular shorterLink's source. // * approx, based on a straight line from target to source, when in fact the path is a bezier // get the angle for the longer link let angle = linkAngle(longerLink) // get the adjacent length to the other link's x position let heightFromY1ToPependicular = linkXLength(shorterLink) / Math.tan(angle) // add or subtract from longer link1's original y1, depending on the slope let yPerpendicular = incline(longerLink) == 'up' ? longerLink.y1 + heightFromY1ToPependicular : longerLink.y1 - heightFromY1ToPependicular return yPerpendicular } function linkPerpendicularYToLinkTarget (longerLink, shorterLink) { // Return the Y coordinate on the longerLink path * which is perpendicular shorterLink's source. // * approx, based on a straight line from target to source, when in fact the path is a bezier // get the angle for the longer link let angle = linkAngle(longerLink) // get the adjacent length to the other link's x position let heightFromY1ToPependicular = linkXLength(shorterLink) / Math.tan(angle) // add or subtract from longer link's original y1, depending on the slope let yPerpendicular = incline(longerLink) == 'up' ? longerLink.y1 - heightFromY1ToPependicular : longerLink.y1 + heightFromY1ToPependicular return yPerpendicular } function resolveNodeLinkOverlaps (graph) { graph.links.forEach(function (link) { if (link.circular) { return } if (link.target.depth - link.source.depth > 1) { let depthToTest = link.source.depth + 1 let maxDepthToTest = link.target.depth - 1 let i = 1 let numberOfDepthsToTest = maxDepthToTest - depthToTest + 1 for ( depthToTest, (i = 1); depthToTest <= maxDepthToTest; depthToTest++, i++ ) { graph.nodes.forEach(function (node) { if (node.depth == depthToTest) { let t = i / (numberOfDepthsToTest + 1) // Find all the points of a cubic bezier curve in javascript // https://stackoverflow.com/questions/15397596/find-all-the-points-of-a-cubic-bezier-curve-in-javascript let B0_t = Math.pow(1 - t, 3) let B1_t = 3 * t * Math.pow(1 - t, 2) let B2_t = 3 * Math.pow(t, 2) * (1 - t) let B3_t = Math.pow(t, 3) let py_t = B0_t * link.y0 + B1_t * link.y0 + B2_t * link.y1 + B3_t * link.y1 let linkY0AtDepth = py_t - link.width / 2 let linkY1AtDepth = py_t + link.width / 2 // If top of link overlaps node, push node up if (linkY0AtDepth > node.y0 && linkY0AtDepth < node.y1) { // console.log("OVERLAP!") let dy = -(node.y1 - linkY0AtDepth + 10) node = adjustNodeHeight(node, dy); //check if other nodes need to move up too graph.nodes.forEach(function(otherNode) { //don't need to check itself or nodes at different depths if ((otherNode.name == node.name) || (otherNode.depth != node.depth)) { return } if (nodesOverlap(node, otherNode)) { adjustNodeHeight(otherNode, dy) } }) } else if (linkY1AtDepth > node.y0 && linkY1AtDepth < node.y1) { // If bottom of link overlaps node, push node down let dy = linkY1AtDepth - node.y0 + 10 node = adjustNodeHeight(node, dy); //check if other nodes need to move down too graph.nodes.forEach(function(otherNode) { //don't need to check itself or nodes at different depths if ((otherNode.name == node.name) || (otherNode.depth != node.depth)) { return } if (otherNode.y0 < node.y1 && otherNode.y1 > node.y1) { adjustNodeHeight(otherNode, dy) } }) } else if (linkY0AtDepth < node.y0 && linkY1AtDepth > node.y1) { // if link completely overlaps node let dy = linkY1AtDepth - node.y0 + 10 node = adjustNodeHeight(node, dy); graph.nodes.forEach(function(otherNode) { //don't need to check itself or nodes at different depths if ((otherNode.name == node.name) || (otherNode.depth != node.depth)) { return } if (otherNode.y0 < node.y1 && otherNode.y1 > node.y1) { adjustNodeHeight(otherNode, dy) } }) } } }) } } }) } function nodesOverlap(nodeA, nodeB) { //test if nodeA top partially overlaps nodeB if (nodeA.y0 > nodeB.y0 && nodeA.y0 < nodeB.y1) { return true } //test if nodeA bottom partially overlaps nodeB else if (nodeA.y1 > nodeB.y0 && nodeA.y1 < nodeB.y1) { return true } //test if nodeA covers nodeB else if (nodeA.y0 < nodeB.y0 && nodeA.y1 > nodeB.y1) { return true } else { return false; } } function adjustNodeHeight (node, dy) { node.y0 = node.y0 + dy node.y1 = node.y1 + dy node.targetLinks.forEach(function (l) { l.y1 = l.y1 + dy }) node.sourceLinks.forEach(function (l) { l.y0 = l.y0 + dy }) return node; } function sortSourceLinks (graph) { graph.nodes.forEach(function (node) { // move any nodes up which are off the bottom if (node.y + (node.y1 - node.y0) > height) { node.y = node.y - (node.y + (node.y1 - node.y0) - height) } let nodesSourceLinks = graph.links.filter(function (l) { return l.source.name == node.name }) // if more than 1 link then sort if (nodesSourceLinks.length > 1) { nodesSourceLinks.sort(function (link1, link2) { // if both are not circular... if (!link1.circular && !link2.circular) { // if the target nodes are the same depth, then sort by the link's target y if (link1.target.depth == link2.target.depth) { return link1.y1 - link2.y1 } else if (!sameInclines(link1, link2)) { // if the links slope in different directions, then sort by the link's target y return link1.y1 - link2.y1 // if the links slope in same directions, then sort by any overlap } else { if (link1.target.depth > link2.target.depth) { // if (node.name == "process10") {console.log("here")} /* let link2Angle = linkAngleFromSource(link2); let link2AdjToLink1Y = linkXLength(link1) / Math.tan(link2Angle); let link2Adj = incline(link2) == "up" ? link2.y0 - link2AdjToLink1Y : link2.y0 + link2AdjToLink1Y; */ let link2Adj = linkPerpendicularYToLinkTarget(link2, link1) return link1.y1 - link2Adj } if (link2.target.depth > link1.target.depth) { /* let link1Angle = linkAngleFromSource(link1); let link1AdjToLink2Y = linkXLength(link2) / Math.tan(link1Angle); let link1Adj = incline(link1) == "up" ? link1.y0 - link1AdjToLink2Y : link1.y0 + link1AdjToLink2Y; */ let link1Adj = linkPerpendicularYToLinkTarget(link1, link2) return link1Adj - link2.y1 } } } // if only one is circular, the move top links up, or bottom links down if (link1.circular && !link2.circular) { return link1.circularLinkType == 'top' ? -1 : 1 } else if (link2.circular && !link1.circular) { return link2.circularLinkType == 'top' ? 1 : -1 } // if both links are circular... if (link1.circular && link2.circular) { // ...and they both loop the same way (both top) if ( link1.circularLinkType === link2.circularLinkType && link1.circularLinkType == 'top' ) { // ...and they both connect to a target with same depth, then sort by the target's y if (link1.target.depth === link2.target.depth) { return link1.target.y1 - link2.target.y1 } else { // ...and they connect to different depth targets, then sort by how far back they return link2.target.depth - link1.target.depth } } else if ( link1.circularLinkType === link2.circularLinkType && link1.circularLinkType == 'bottom' ) { // ...and they both loop the same way (both bottom) // ...and they both connect to a target with same depth, then sort by the target's y if (link1.target.depth === link2.target.depth) { return link2.target.y1 - link1.target.y1 } else { // ...and they connect to different depth targets, then sort by how far back they return link1.target.depth - link2.target.depth } } else { // ...and they loop around different ways, the move top up and bottom down return link1.circularLinkType == 'top' ? -1 : 1 } } }) } // update y0 for links let ySourceOffset = node.y0 nodesSourceLinks.forEach(function (link) { link.y0 = ySourceOffset + link.width / 2 ySourceOffset = ySourceOffset + link.width }) }) } function sortTargetLinks (graph) { graph.nodes.forEach(function (node) { let nodesTargetLinks = graph.links.filter(function (l) { return l.target.name == node.name }) if (nodesTargetLinks.length > 1) { nodesTargetLinks.sort(function (link1, link2) { // if both are not circular, the base on the source y position if (!link1.circular && !link2.circular) { if (link1.source.depth == link2.source.depth) { return link1.y0 - link2.y0 } else if (!sameInclines(link1, link2)) { return link1.y0 - link2.y0 } else { // get the angle of the link to the further source node (ie the smaller depth) if (link2.source.depth < link1.source.depth) { let link2Adj = linkPerpendicularYToLinkSource(link2, link1) return link1.y0 - link2Adj } if (link1.source.depth < link2.source.depth) { let link1Adj = linkPerpendicularYToLinkSource(link1, link2) return link1Adj - link2.y0 } } } // if only one is circular, the move top links up, or bottom links down if (link1.circular && !link2.circular) { return link1.circularLinkType == 'top' ? -1 : 1 } else if (link2.circular && !link1.circular) { return link2.circularLinkType == 'top' ? 1 : -1 } // if both links are circular... if (link1.circular && link2.circular) { // ...and they both loop the same way (both top) if ( link1.circularLinkType === link2.circularLinkType && link1.circularLinkType == 'top' ) { // ...and they both connect to a target with same depth, then sort by the target's y if (link1.source.depth === link2.source.depth) { return link1.source.y1 - link2.source.y1 } else { // ...and they connect to different depth targets, then sort by how far back they return link1.source.depth - link2.source.depth } } else if ( link1.circularLinkType === link2.circularLinkType && link1.circularLinkType == 'bottom' ) { // ...and they both loop the same way (both bottom) // ...and they both connect to a target with same depth, then sort by the target's y if (link1.source.depth === link2.source.depth) { return link1.source.y1 - link2.source.y1 } else { // ...and they connect to different depth targets, then sort by how far back they return link2.source.depth - link1.source.depth } } else { // ...and they loop around different ways, the move top up and bottom down return link1.circularLinkType == 'top' ? -1 : 1 } } }) } // update y1 for links let yTargetOffset = node.y0 nodesTargetLinks.forEach(function (link) { link.y1 = yTargetOffset + link.width / 2 yTargetOffset = yTargetOffset + link.width }) }) } function sameInclines (link1, link2) { return incline(link1) == incline(link2) } function incline (link) { // positive => slopes up from source to target // negative => slopes down from source to target return link.y0 - link.y1 > 0 ? 'up' : 'down' } /////////////////////////////////////////////////////////////////////////////// exports.sankey = sankey exports.sankeyCenter = center exports.sankeyLeft = left exports.sankeyRight = right exports.sankeyJustify = justify //exports.sankeyPath = sankeyPath // exports.sankeyLinkHorizontal = sankeyLinkHorizontal // exports.curveSankeyForceLink = curveSankeyForceLink Object.defineProperty(exports, '__esModule', { value: true }) }) var sankeyPath = function(link) { let path = '' if (link.circular) { path = link.circularPathData.path } else { var normalPath = d3.linkHorizontal() .source(function (d) { let x = d.source.x0 + (d.source.x1 - d.source.x0) let y = d.y0 return [x, y] }) .target(function (d) { let x = d.target.x0 let y = d.y1 return [x, y] }) path = normalPath(link) } return path } function appendArrows(selection, arrowLength, gapLength, arrowHeadSize) { //let arrowLength = 20; //let gapLength = 300; let totalDashArrayLength = arrowLength + gapLength; arrows = selection.append("path") .attr("d", sankeyPath) .style("stroke-width", 1) .style("stroke", "black") .style("stroke-dasharray", arrowLength + "," + gapLength) arrows.each(function (arrow) { let thisPath = d3.select(this).node(); let parentG = d3.select(this.parentNode) let pathLength = thisPath.getTotalLength(); let numberOfArrows = Math.ceil(pathLength / totalDashArrayLength); //remove the last arrow head if it will overlap the target node //+4 to take into account arrow head size if ((((numberOfArrows - 1) * totalDashArrayLength) + (arrowLength + 5)) > pathLength) { numberOfArrows = numberOfArrows - 1; } let arrowHeadData = d3.range(numberOfArrows).map(function (d, i) { let length = (i * totalDashArrayLength) + arrowLength; let point = thisPath.getPointAtLength(length); let previousPoint = thisPath.getPointAtLength(length - 2); let rotation = 0; if (point.y == previousPoint.y) { rotation = (point.x < previousPoint.x) ? 180 : 0; } else if (point.x == previousPoint.x) { rotation = (point.y < previousPoint.y) ? -90 : 90; } else { let adj = Math.abs(point.x - previousPoint.x); let opp = Math.abs(point.y - previousPoint.y); let angle = Math.atan(opp / adj) * (180 / Math.PI); if (point.x < previousPoint.x) { angle = angle + ((90 - angle) * 2) } if (point.y < previousPoint.y) { rotation = -angle; } else { rotation = angle; } }; return { x: point.x, y: point.y, rotation: rotation }; }); let arrowHeads = parentG.selectAll(".arrow-heads") .data(arrowHeadData) .enter() .append("path") .attr("d", function (d) { return "M" + (d.x) + "," + (d.y - (arrowHeadSize/2)) + " " + "L" + (d.x + arrowHeadSize) + "," + (d.y) + " " + "L" + d.x + "," + (d.y + (arrowHeadSize/2)); }) .attr("class", "arrow-head") .attr("transform", function (d) { return "rotate(" + d.rotation + "," + d.x + "," + d.y + ")"; }) .style("fill", "black") }); }