From 03a3cb578bc33a06e0c27ed1d91bb79c32e673f4 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 12 Dec 2018 17:49:40 -0500 Subject: [PATCH 1/8] merge @Plotly/d3-sankey into plotly.js (es6 -> es5, tests in jasmine) --- package-lock.json | 10 - package.json | 4 +- src/traces/sankey/d3-sankey.js | 356 ++++++++++++++++++++ src/traces/sankey/render.js | 2 +- test/jasmine/tests/sankey_d3_sankey_test.js | 106 ++++++ 5 files changed, 466 insertions(+), 12 deletions(-) create mode 100644 src/traces/sankey/d3-sankey.js create mode 100644 test/jasmine/tests/sankey_d3_sankey_test.js diff --git a/package-lock.json b/package-lock.json index 4cc24650b6d..494ba124b4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,16 +117,6 @@ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" }, - "@plotly/d3-sankey": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.5.1.tgz", - "integrity": "sha512-uMToNGexOSLG0hBm+uAzElfFW0Pt2utgJ//puL5nuerNnPnRTTe3Un7XFVcWqRhvXEViF00Xq/8wGoA8i8eZJA==", - "requires": { - "d3-array": "1", - "d3-collection": "1", - "d3-interpolate": "1" - } - }, "@types/bluebird": { "version": "3.5.24", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.24.tgz", diff --git a/package.json b/package.json index ba09779f6fc..4611dc2072d 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,10 @@ ] }, "dependencies": { + "d3-array": "1", + "d3-collection": "1", + "d3-interpolate": "1", "3d-view": "^2.0.0", - "@plotly/d3-sankey": "^0.5.1", "alpha-shape": "^1.0.0", "array-range": "^1.0.1", "canvas-fit": "^1.5.0", diff --git a/src/traces/sankey/d3-sankey.js b/src/traces/sankey/d3-sankey.js new file mode 100644 index 00000000000..c2dbbb89f23 --- /dev/null +++ b/src/traces/sankey/d3-sankey.js @@ -0,0 +1,356 @@ +/** +Copyright 2015, Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*/ + +/** +The following is a fork of https://github.com/plotly/d3-sankey which itself +was a fork of https://github.com/d3/d3-sankey from Mike Bostock. +*/ + +'use strict'; + +var d3array = require('d3-array'); +var ascending = d3array.ascending; +var min = d3array.min; +var sum = d3array.sum; +var max = d3array.max; +var nest = require('d3-collection').nest; +var interpolateNumber = require('d3-interpolate').interpolateNumber; + +module.exports = function() { + var sankey = {}, + nodeWidth = 24, + nodePadding = 8, + size = [1, 1], + nodes = [], + links = [], + maxPaddedSpace = 2 / 3; // Defined as a fraction of the total available space + + sankey.nodeWidth = function(_) { + if(!arguments.length) return nodeWidth; + nodeWidth = +_; + return sankey; + }; + + sankey.nodePadding = function(_) { + if(!arguments.length) return nodePadding; + nodePadding = +_; + return sankey; + }; + + sankey.nodes = function(_) { + if(!arguments.length) return nodes; + nodes = _; + return sankey; + }; + + sankey.links = function(_) { + if(!arguments.length) return links; + links = _; + return sankey; + }; + + sankey.size = function(_) { + if(!arguments.length) return size; + size = _; + return sankey; + }; + + sankey.layout = function(iterations) { + computeNodeLinks(); + computeNodeValues(); + computeNodeBreadths(); + computeNodeDepths(iterations); + computeLinkDepths(); + return sankey; + }; + + sankey.relayout = function() { + computeLinkDepths(); + return sankey; + }; + + sankey.link = function() { + var curvature = 0.5; + + function link(d) { + var x0 = d.source.x + d.source.dx, + x1 = d.target.x, + xi = interpolateNumber(x0, x1), + x2 = xi(curvature), + x3 = xi(1 - curvature), + y0a = d.source.y + d.sy, + y0b = y0a + d.dy, + y1a = d.target.y + d.ty, + y1b = y1a + d.dy; + return 'M' + x0 + ',' + y0a + + 'C' + x2 + ',' + y0a + + ' ' + x3 + ',' + y1a + + ' ' + x1 + ',' + y1a + + 'L' + x1 + ',' + y1b + + 'C' + x3 + ',' + y1b + + ' ' + x2 + ',' + y0b + + ' ' + x0 + ',' + y0b + + 'Z'; + } + + link.curvature = function(_) { + if(!arguments.length) return curvature; + curvature = +_; + return link; + }; + + return link; + }; + + // Populate the sourceLinks and targetLinks for each node. + // Also, if the source and target are not objects, assume they are indices. + function computeNodeLinks() { + nodes.forEach(function(node) { + node.sourceLinks = []; + node.targetLinks = []; + }); + links.forEach(function(link, i) { + var source = link.source, + target = link.target; + if(typeof source === 'number') source = link.source = nodes[link.source]; + if(typeof target === 'number') target = link.target = nodes[link.target]; + link.originalIndex = i; + source.sourceLinks.push(link); + target.targetLinks.push(link); + }); + } + + // Compute the value (size) of each node by summing the associated links. + function computeNodeValues() { + nodes.forEach(function(node) { + node.value = Math.max( + sum(node.sourceLinks, value), + sum(node.targetLinks, value) + ); + }); + } + + // Iteratively assign the breadth (x-position) for each node. + // Nodes are assigned the maximum breadth of incoming neighbors plus one; + // nodes with no incoming links are assigned breadth zero, while + // nodes with no outgoing links are assigned the maximum breadth. + function computeNodeBreadths() { + var remainingNodes = nodes, + nextNodes, + x = 0; + + function processNode(node) { + node.x = x; + node.dx = nodeWidth; + node.sourceLinks.forEach(function(link) { + if(nextNodes.indexOf(link.target) < 0) { + nextNodes.push(link.target); + } + }); + } + + while(remainingNodes.length) { + nextNodes = []; + remainingNodes.forEach(processNode); + remainingNodes = nextNodes; + ++x; + } + + // + moveSinksRight(x); + scaleNodeBreadths((size[0] - nodeWidth) / (x - 1)); + } + + // function moveSourcesRight() { + // nodes.forEach(function(node) { + // if (!node.targetLinks.length) { + // node.x = min(node.sourceLinks, function(d) { return d.target.x; }) - 1; + // } + // }); + // } + + function moveSinksRight(x) { + nodes.forEach(function(node) { + if(!node.sourceLinks.length) { + node.x = x - 1; + } + }); + } + + function scaleNodeBreadths(kx) { + nodes.forEach(function(node) { + node.x *= kx; + }); + } + + function computeNodeDepths(iterations) { + var nodesByBreadth = nest() + .key(function(d) { return d.x; }) + .sortKeys(ascending) + .entries(nodes) + .map(function(d) { return d.values; }); + + // + initializeNodeDepth(); + resolveCollisions(); + for(var alpha = 1; iterations > 0; --iterations) { + relaxRightToLeft(alpha *= 0.99); + resolveCollisions(); + relaxLeftToRight(alpha); + resolveCollisions(); + } + + function initializeNodeDepth() { + var L = max(nodesByBreadth, function(nodes) { + return nodes.length; + }); + var maxNodePadding = maxPaddedSpace * size[1] / (L - 1); + if(nodePadding > maxNodePadding) nodePadding = maxNodePadding; + var ky = min(nodesByBreadth, function(nodes) { + return (size[1] - (nodes.length - 1) * nodePadding) / sum(nodes, value); + }); + + nodesByBreadth.forEach(function(nodes) { + nodes.forEach(function(node, i) { + node.y = i; + node.dy = node.value * ky; + }); + }); + + links.forEach(function(link) { + link.dy = link.value * ky; + }); + } + + function relaxLeftToRight(alpha) { + nodesByBreadth.forEach(function(nodes) { + nodes.forEach(function(node) { + if(node.targetLinks.length) { + var y = sum(node.targetLinks, weightedSource) / sum(node.targetLinks, value); + node.y += (y - center(node)) * alpha; + } + }); + }); + + function weightedSource(link) { + return center(link.source) * link.value; + } + } + + function relaxRightToLeft(alpha) { + nodesByBreadth.slice().reverse().forEach(function(nodes) { + nodes.forEach(function(node) { + if(node.sourceLinks.length) { + var y = sum(node.sourceLinks, weightedTarget) / sum(node.sourceLinks, value); + node.y += (y - center(node)) * alpha; + } + }); + }); + + function weightedTarget(link) { + return center(link.target) * link.value; + } + } + + function resolveCollisions() { + nodesByBreadth.forEach(function(nodes) { + var node, + dy, + y0 = 0, + n = nodes.length, + i; + + // Push any overlapping nodes down. + nodes.sort(ascendingDepth); + for(i = 0; i < n; ++i) { + node = nodes[i]; + dy = y0 - node.y; + if(dy > 0) node.y += dy; + y0 = node.y + node.dy + nodePadding; + } + + // If the bottommost node goes outside the bounds, push it back up. + dy = y0 - nodePadding - size[1]; + if(dy > 0) { + y0 = node.y -= dy; + + // Push any overlapping nodes back up. + for(i = n - 2; i >= 0; --i) { + node = nodes[i]; + dy = node.y + node.dy + nodePadding - y0; + if(dy > 0) node.y -= dy; + y0 = node.y; + } + } + }); + } + + function ascendingDepth(a, b) { + return a.y - b.y; + } + } + + function computeLinkDepths() { + nodes.forEach(function(node) { + node.sourceLinks.sort(ascendingTargetDepth); + node.targetLinks.sort(ascendingSourceDepth); + }); + nodes.forEach(function(node) { + var sy = 0, ty = 0; + node.sourceLinks.forEach(function(link) { + link.sy = sy; + sy += link.dy; + }); + node.targetLinks.forEach(function(link) { + link.ty = ty; + ty += link.dy; + }); + }); + + function ascendingSourceDepth(a, b) { + return (a.source.y - b.source.y) || (a.originalIndex - b.originalIndex); + } + + function ascendingTargetDepth(a, b) { + return (a.target.y - b.target.y) || (a.originalIndex - b.originalIndex); + } + } + + function center(node) { + return node.y + node.dy / 2; + } + + function value(link) { + return link.value; + } + + return sankey; +}; diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 2ec53241b9c..0a02dd41d9f 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -13,7 +13,7 @@ var d3 = require('d3'); var tinycolor = require('tinycolor2'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); -var d3sankey = require('@plotly/d3-sankey').sankey; +var d3sankey = require('./d3-sankey.js'); var d3Force = require('d3-force'); var Lib = require('../../lib'); var isArrayOrTypedArray = Lib.isArrayOrTypedArray; diff --git a/test/jasmine/tests/sankey_d3_sankey_test.js b/test/jasmine/tests/sankey_d3_sankey_test.js new file mode 100644 index 00000000000..874d4175591 --- /dev/null +++ b/test/jasmine/tests/sankey_d3_sankey_test.js @@ -0,0 +1,106 @@ +// var Plotly = require('@lib/index'); +// var attributes = require('@src/traces/sankey/attributes'); + +var d3sankey = require('@src/traces/sankey/d3-sankey'); + +var graph = { + 'nodes': [{ + 'node': 0, + 'name': 'node0' + }, { + 'node': 1, + 'name': 'node1' + }, { + 'node': 2, + 'name': 'node2' + }, { + 'node': 3, + 'name': 'node3' + }, { + 'node': 4, + 'name': 'node4' + }], + 'links': [{ + 'source': 0, + 'target': 2, + 'value': 2 + }, { + 'source': 1, + 'target': 2, + 'value': 2 + }, { + 'source': 1, + 'target': 3, + 'value': 2 + }, { + 'source': 0, + 'target': 4, + 'value': 2 + }, { + 'source': 2, + 'target': 3, + 'value': 2 + }, { + 'source': 2, + 'target': 4, + 'value': 2 + }, { + 'source': 3, + 'target': 4, + 'value': 4 + }] +}; + + +describe('d3-sankey', function() { + var margin = { + top: 10, + right: 10, + bottom: 10, + left: 10 + }, + width = 1200 - margin.left - margin.right, + height = 740 - margin.top - margin.bottom; + + var s; + + beforeEach(function() { + s = d3sankey() + .nodeWidth(36) + .nodePadding(10) + .nodes(graph.nodes) + .links(graph.links) + .size([width, height]) + .layout(32); + }); + + it('controls the width of nodes', function() { + expect(s.nodeWidth()).toEqual(36, 'incorrect nodeWidth'); + }); + + it('controls the padding between nodes', function() { + expect(s.nodePadding()).toEqual(10, 'incorrect nodePadding'); + }); + + it('controls the padding between nodes', function() { + expect(s.nodePadding()).toEqual(10, 'incorrect nodePadding'); + }); + + it('keep a list of nodes', function() { + var node_names = s.nodes().map(function(obj) { + return obj.name; + }); + expect(node_names).toEqual(['node0', 'node1', 'node2', 'node3', 'node4']); + }); + + it('keep a list of links', function() { + var link_widths = s.links().map(function(obj) { + return obj.dy; + }); + expect(link_widths).toEqual([177.5, 177.5, 177.5, 177.5, 177.5, 177.5, 355]); + }); + + it('controls the size of the figure', function() { + expect(s.size()).toEqual([1180, 720], 'incorrect size'); + }); +}); From 2504dc3b6910fcc0ad335794c675a9a23b5923d6 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 12 Dec 2018 17:59:57 -0500 Subject: [PATCH 2/8] fix header for d3-sankey.js --- src/traces/sankey/d3-sankey.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/traces/sankey/d3-sankey.js b/src/traces/sankey/d3-sankey.js index c2dbbb89f23..87a5587a1ed 100644 --- a/src/traces/sankey/d3-sankey.js +++ b/src/traces/sankey/d3-sankey.js @@ -1,3 +1,11 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + /** Copyright 2015, Mike Bostock All rights reserved. From fd19907c44a6486aea3fb4b38293a2520a38df35 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Thu, 13 Dec 2018 17:05:32 -0500 Subject: [PATCH 3/8] move data conversion into its own function and run in calc() phase --- src/traces/sankey/calc.js | 8 +- src/traces/sankey/convert-to-d3-sankey.js | 77 ++++++++++++++++ src/traces/sankey/render.js | 107 ++++------------------ 3 files changed, 103 insertions(+), 89 deletions(-) create mode 100644 src/traces/sankey/convert-to-d3-sankey.js diff --git a/src/traces/sankey/calc.js b/src/traces/sankey/calc.js index e9dc3bc0270..8aaee62f8c3 100644 --- a/src/traces/sankey/calc.js +++ b/src/traces/sankey/calc.js @@ -12,6 +12,8 @@ var tarjan = require('strongly-connected-components'); var Lib = require('../../lib'); var wrap = require('../../lib/gup').wrap; +var convertToD3Sankey = require('./convert-to-d3-sankey'); + function circularityPresent(nodeList, sources, targets) { var nodeLen = nodeList.length; @@ -48,8 +50,10 @@ module.exports = function calc(gd, trace) { trace.node.color = []; } + var result = convertToD3Sankey(trace); + return wrap({ - link: trace.link, - node: trace.node + _nodes: result.nodes, + _links: result.links }); }; diff --git a/src/traces/sankey/convert-to-d3-sankey.js b/src/traces/sankey/convert-to-d3-sankey.js new file mode 100644 index 00000000000..f5ba897106c --- /dev/null +++ b/src/traces/sankey/convert-to-d3-sankey.js @@ -0,0 +1,77 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Lib = require('../../lib'); +var isArrayOrTypedArray = Lib.isArrayOrTypedArray; +var isIndex = Lib.isIndex; + +module.exports = function(trace) { + var nodeSpec = trace.node; + var linkSpec = trace.link; + + var links = []; + var hasLinkColorArray = isArrayOrTypedArray(linkSpec.color); + var linkedNodes = {}; + + var nodeCount = nodeSpec.label.length; + var i; + for(i = 0; i < linkSpec.value.length; i++) { + var val = linkSpec.value[i]; + // remove negative values, but keep zeros with special treatment + var source = linkSpec.source[i]; + var target = linkSpec.target[i]; + if(!(val > 0 && isIndex(source, nodeCount) && isIndex(target, nodeCount))) { + continue; + } + + source = +source; + target = +target; + linkedNodes[source] = linkedNodes[target] = true; + + links.push({ + pointNumber: i, + label: linkSpec.label[i], + color: hasLinkColorArray ? linkSpec.color[i] : linkSpec.color, + source: source, + target: target, + value: +val + }); + } + + var hasNodeColorArray = isArrayOrTypedArray(nodeSpec.color); + var nodes = []; + var removedNodes = false; + var nodeIndices = {}; + + for(i = 0; i < nodeCount; i++) { + if(linkedNodes[i]) { + var l = nodeSpec.label[i]; + nodeIndices[i] = nodes.length; + nodes.push({ + pointNumber: i, + label: l, + color: hasNodeColorArray ? nodeSpec.color[i] : nodeSpec.color + }); + } else removedNodes = true; + } + + // need to re-index links now, since we didn't put all the nodes in + if(removedNodes) { + for(i = 0; i < links.length; i++) { + links[i].source = nodeIndices[links[i].source]; + links[i].target = nodeIndices[links[i].target]; + } + } + + return { + links: links, + nodes: nodes + }; +}; diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 0a02dd41d9f..42520ddd4fd 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -16,8 +16,6 @@ var Drawing = require('../../components/drawing'); var d3sankey = require('./d3-sankey.js'); var d3Force = require('d3-force'); var Lib = require('../../lib'); -var isArrayOrTypedArray = Lib.isArrayOrTypedArray; -var isIndex = Lib.isIndex; var gup = require('../../lib/gup'); var keyFun = gup.keyFun; var repeat = gup.repeat; @@ -67,78 +65,18 @@ function switchToSankeyFormat(nodes) { // view models function sankeyModel(layout, d, traceIndex) { - var trace = unwrap(d).trace; + var calcData = unwrap(d); + var trace = calcData.trace; var domain = trace.domain; - var nodeSpec = trace.node; - var linkSpec = trace.link; - var arrangement = trace.arrangement; var horizontal = trace.orientation === 'h'; var nodePad = trace.node.pad; var nodeThickness = trace.node.thickness; - var nodeLineColor = trace.node.line.color; - var nodeLineWidth = trace.node.line.width; - var linkLineColor = trace.link.line.color; - var linkLineWidth = trace.link.line.width; - var valueFormat = trace.valueformat; - var valueSuffix = trace.valuesuffix; - var textFont = trace.textfont; var width = layout.width * (domain.x[1] - domain.x[0]); var height = layout.height * (domain.y[1] - domain.y[0]); - var links = []; - var hasLinkColorArray = isArrayOrTypedArray(linkSpec.color); - var linkedNodes = {}; - - var nodeCount = nodeSpec.label.length; - var i; - for(i = 0; i < linkSpec.value.length; i++) { - var val = linkSpec.value[i]; - // remove negative values, but keep zeros with special treatment - var source = linkSpec.source[i]; - var target = linkSpec.target[i]; - if(!(val > 0 && isIndex(source, nodeCount) && isIndex(target, nodeCount))) { - continue; - } - - source = +source; - target = +target; - linkedNodes[source] = linkedNodes[target] = true; - - links.push({ - pointNumber: i, - label: linkSpec.label[i], - color: hasLinkColorArray ? linkSpec.color[i] : linkSpec.color, - source: source, - target: target, - value: +val - }); - } - - var hasNodeColorArray = isArrayOrTypedArray(nodeSpec.color); - var nodes = []; - var removedNodes = false; - var nodeIndices = {}; - for(i = 0; i < nodeCount; i++) { - if(linkedNodes[i]) { - var l = nodeSpec.label[i]; - nodeIndices[i] = nodes.length; - nodes.push({ - pointNumber: i, - label: l, - color: hasNodeColorArray ? nodeSpec.color[i] : nodeSpec.color - }); - } - else removedNodes = true; - } - - // need to re-index links now, since we didn't put all the nodes in - if(removedNodes) { - for(i = 0; i < links.length; i++) { - links[i].source = nodeIndices[links[i].source]; - links[i].target = nodeIndices[links[i].target]; - } - } + var nodes = calcData._nodes; + var links = calcData._links; var sankey = d3sankey() .size(horizontal ? [width, height] : [height, width]) @@ -152,13 +90,6 @@ function sankeyModel(layout, d, traceIndex) { Lib.warn('node.pad was reduced to ', sankey.nodePadding(), ' to fit within the figure.'); } - var node, sankeyNodes = sankey.nodes(); - for(var n = 0; n < sankeyNodes.length; n++) { - node = sankeyNodes[n]; - node.width = width; - node.height = height; - } - switchToForceFormat(nodes); return { @@ -168,21 +99,21 @@ function sankeyModel(layout, d, traceIndex) { horizontal: horizontal, width: width, height: height, - nodePad: nodePad, - nodeLineColor: nodeLineColor, - nodeLineWidth: nodeLineWidth, - linkLineColor: linkLineColor, - linkLineWidth: linkLineWidth, - valueFormat: valueFormat, - valueSuffix: valueSuffix, - textFont: textFont, + nodePad: trace.node.pad, + nodeLineColor: trace.node.line.color, + nodeLineWidth: trace.node.line.width, + linkLineColor: trace.link.line.color, + linkLineWidth: trace.link.line.width, + valueFormat: trace.valueformat, + valueSuffix: trace.valuesuffix, + textFont: trace.textfont, translateX: domain.x[0] * layout.width + layout.margin.l, translateY: layout.height - domain.y[1] * layout.height + layout.margin.t, dragParallel: horizontal ? height : width, dragPerpendicular: horizontal ? width : height, nodes: nodes, links: links, - arrangement: arrangement, + arrangement: trace.arrangement, sankey: sankey, forceLayouts: {}, interactionState: { @@ -454,12 +385,14 @@ function snappingForce(sankeyNode, forceKey, nodes, d) { } // scene graph -module.exports = function(svg, styledData, layout, callbacks) { +module.exports = function(svg, calcData, layout, callbacks) { + + var styledData = calcData + .filter(function(d) {return unwrap(d).trace.visible;}) + .map(sankeyModel.bind(null, layout)); + var sankey = svg.selectAll('.' + c.cn.sankey) - .data(styledData - .filter(function(d) {return unwrap(d).trace.visible;}) - .map(sankeyModel.bind(null, layout)), - keyFun); + .data(styledData, keyFun); sankey.exit() .remove(); From bde2a79fecac0a68119cd3aaae807bb55cd6dcdf Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Thu, 13 Dec 2018 17:07:38 -0500 Subject: [PATCH 4/8] refactor d3-sankey to look more similar to upstream --- src/traces/sankey/d3-sankey.js | 100 ++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 46 deletions(-) diff --git a/src/traces/sankey/d3-sankey.js b/src/traces/sankey/d3-sankey.js index 87a5587a1ed..1d606da4e50 100644 --- a/src/traces/sankey/d3-sankey.js +++ b/src/traces/sankey/d3-sankey.js @@ -52,24 +52,54 @@ var max = d3array.max; var nest = require('d3-collection').nest; var interpolateNumber = require('d3-interpolate').interpolateNumber; +// sort links' breadth (ie top to bottom in a column), based on their source nodes' breadths +function ascendingSourceDepth(a, b) { + return ascendingBreadth(a.source, b.source) || (a.originalIndex - b.originalIndex); +} + +// sort links' breadth (ie top to bottom in a column), based on their target nodes' breadths +function ascendingTargetDepth(a, b) { + return ascendingBreadth(a.target, b.target) || (a.originalIndex - b.originalIndex); +} + +function ascendingBreadth(a, b) { + return a.y - b.y; +} + +function value(d) { + return d.value; +} + +function nodeCenter(node) { + return node.y + node.dy / 2; +} + +function weightedSource(link) { + return nodeCenter(link.source) * link.value; +} + +function weightedTarget(link) { + return nodeCenter(link.target) * link.value; +} + module.exports = function() { var sankey = {}, - nodeWidth = 24, - nodePadding = 8, + dx = 24, // nodeWidth + py = 8, // nodePadding size = [1, 1], nodes = [], links = [], maxPaddedSpace = 2 / 3; // Defined as a fraction of the total available space sankey.nodeWidth = function(_) { - if(!arguments.length) return nodeWidth; - nodeWidth = +_; + if(!arguments.length) return dx; + dx = +_; return sankey; }; sankey.nodePadding = function(_) { - if(!arguments.length) return nodePadding; - nodePadding = +_; + if(!arguments.length) return py; + py = +_; return sankey; }; @@ -94,14 +124,14 @@ module.exports = function() { sankey.layout = function(iterations) { computeNodeLinks(); computeNodeValues(); - computeNodeBreadths(); - computeNodeDepths(iterations); - computeLinkDepths(); + computeNodeDepths(); + computeNodeBreadths(iterations); + computeLinkBreadths(); return sankey; }; sankey.relayout = function() { - computeLinkDepths(); + computeLinkBreadths(); return sankey; }; @@ -170,14 +200,15 @@ module.exports = function() { // Nodes are assigned the maximum breadth of incoming neighbors plus one; // nodes with no incoming links are assigned breadth zero, while // nodes with no outgoing links are assigned the maximum breadth. - function computeNodeBreadths() { + function computeNodeDepths() { var remainingNodes = nodes, nextNodes, x = 0; function processNode(node) { + node.depth = x; node.x = x; - node.dx = nodeWidth; + node.dx = dx; node.sourceLinks.forEach(function(link) { if(nextNodes.indexOf(link.target) < 0) { nextNodes.push(link.target); @@ -194,7 +225,7 @@ module.exports = function() { // moveSinksRight(x); - scaleNodeBreadths((size[0] - nodeWidth) / (x - 1)); + scaleNodeBreadths((size[0] - dx) / (x - 1)); } // function moveSourcesRight() { @@ -208,6 +239,7 @@ module.exports = function() { function moveSinksRight(x) { nodes.forEach(function(node) { if(!node.sourceLinks.length) { + node.depth = x - 1; node.x = x - 1; } }); @@ -219,7 +251,7 @@ module.exports = function() { }); } - function computeNodeDepths(iterations) { + function computeNodeBreadths(iterations) { var nodesByBreadth = nest() .key(function(d) { return d.x; }) .sortKeys(ascending) @@ -241,9 +273,9 @@ module.exports = function() { return nodes.length; }); var maxNodePadding = maxPaddedSpace * size[1] / (L - 1); - if(nodePadding > maxNodePadding) nodePadding = maxNodePadding; + if(py > maxNodePadding) py = maxNodePadding; var ky = min(nodesByBreadth, function(nodes) { - return (size[1] - (nodes.length - 1) * nodePadding) / sum(nodes, value); + return (size[1] - (nodes.length - 1) * py) / sum(nodes, value); }); nodesByBreadth.forEach(function(nodes) { @@ -263,14 +295,10 @@ module.exports = function() { nodes.forEach(function(node) { if(node.targetLinks.length) { var y = sum(node.targetLinks, weightedSource) / sum(node.targetLinks, value); - node.y += (y - center(node)) * alpha; + node.y += (y - nodeCenter(node)) * alpha; } }); }); - - function weightedSource(link) { - return center(link.source) * link.value; - } } function relaxRightToLeft(alpha) { @@ -278,14 +306,10 @@ module.exports = function() { nodes.forEach(function(node) { if(node.sourceLinks.length) { var y = sum(node.sourceLinks, weightedTarget) / sum(node.sourceLinks, value); - node.y += (y - center(node)) * alpha; + node.y += (y - nodeCenter(node)) * alpha; } }); }); - - function weightedTarget(link) { - return center(link.target) * link.value; - } } function resolveCollisions() { @@ -302,18 +326,18 @@ module.exports = function() { node = nodes[i]; dy = y0 - node.y; if(dy > 0) node.y += dy; - y0 = node.y + node.dy + nodePadding; + y0 = node.y + node.dy + py; } // If the bottommost node goes outside the bounds, push it back up. - dy = y0 - nodePadding - size[1]; + dy = y0 - py - size[1]; if(dy > 0) { y0 = node.y -= dy; // Push any overlapping nodes back up. for(i = n - 2; i >= 0; --i) { node = nodes[i]; - dy = node.y + node.dy + nodePadding - y0; + dy = node.y + node.dy + py - y0; if(dy > 0) node.y -= dy; y0 = node.y; } @@ -326,7 +350,7 @@ module.exports = function() { } } - function computeLinkDepths() { + function computeLinkBreadths() { nodes.forEach(function(node) { node.sourceLinks.sort(ascendingTargetDepth); node.targetLinks.sort(ascendingSourceDepth); @@ -342,22 +366,6 @@ module.exports = function() { ty += link.dy; }); }); - - function ascendingSourceDepth(a, b) { - return (a.source.y - b.source.y) || (a.originalIndex - b.originalIndex); - } - - function ascendingTargetDepth(a, b) { - return (a.target.y - b.target.y) || (a.originalIndex - b.originalIndex); - } - } - - function center(node) { - return node.y + node.dy / 2; - } - - function value(link) { - return link.value; } return sankey; From 5f89d78ced2a73dbe31952bc2e6fdb8c54259032 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Thu, 13 Dec 2018 17:49:41 -0500 Subject: [PATCH 5/8] fix indentation --- src/traces/sankey/convert-to-d3-sankey.js | 4 +- src/traces/sankey/d3-sankey.js | 46 ++++++++++------------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/traces/sankey/convert-to-d3-sankey.js b/src/traces/sankey/convert-to-d3-sankey.js index f5ba897106c..98ec340061d 100644 --- a/src/traces/sankey/convert-to-d3-sankey.js +++ b/src/traces/sankey/convert-to-d3-sankey.js @@ -24,7 +24,7 @@ module.exports = function(trace) { var i; for(i = 0; i < linkSpec.value.length; i++) { var val = linkSpec.value[i]; - // remove negative values, but keep zeros with special treatment + // remove negative values, but keep zeros with special treatment var source = linkSpec.source[i]; var target = linkSpec.target[i]; if(!(val > 0 && isIndex(source, nodeCount) && isIndex(target, nodeCount))) { @@ -62,7 +62,7 @@ module.exports = function(trace) { } else removedNodes = true; } - // need to re-index links now, since we didn't put all the nodes in + // need to re-index links now, since we didn't put all the nodes in if(removedNodes) { for(i = 0; i < links.length; i++) { links[i].source = nodeIndices[links[i].source]; diff --git a/src/traces/sankey/d3-sankey.js b/src/traces/sankey/d3-sankey.js index 1d606da4e50..c1ee9bc2f2a 100644 --- a/src/traces/sankey/d3-sankey.js +++ b/src/traces/sankey/d3-sankey.js @@ -149,14 +149,14 @@ module.exports = function() { y1a = d.target.y + d.ty, y1b = y1a + d.dy; return 'M' + x0 + ',' + y0a + - 'C' + x2 + ',' + y0a + - ' ' + x3 + ',' + y1a + - ' ' + x1 + ',' + y1a + - 'L' + x1 + ',' + y1b + - 'C' + x3 + ',' + y1b + - ' ' + x2 + ',' + y0b + - ' ' + x0 + ',' + y0b + - 'Z'; + 'C' + x2 + ',' + y0a + + ' ' + x3 + ',' + y1a + + ' ' + x1 + ',' + y1a + + 'L' + x1 + ',' + y1b + + 'C' + x3 + ',' + y1b + + ' ' + x2 + ',' + y0b + + ' ' + x0 + ',' + y0b + + 'Z'; } link.curvature = function(_) { @@ -168,8 +168,8 @@ module.exports = function() { return link; }; - // Populate the sourceLinks and targetLinks for each node. - // Also, if the source and target are not objects, assume they are indices. + // Populate the sourceLinks and targetLinks for each node. + // Also, if the source and target are not objects, assume they are indices. function computeNodeLinks() { nodes.forEach(function(node) { node.sourceLinks = []; @@ -186,7 +186,7 @@ module.exports = function() { }); } - // Compute the value (size) of each node by summing the associated links. + // Compute the value (size) of each node by summing the associated links. function computeNodeValues() { nodes.forEach(function(node) { node.value = Math.max( @@ -196,10 +196,10 @@ module.exports = function() { }); } - // Iteratively assign the breadth (x-position) for each node. - // Nodes are assigned the maximum breadth of incoming neighbors plus one; - // nodes with no incoming links are assigned breadth zero, while - // nodes with no outgoing links are assigned the maximum breadth. + // Iteratively assign the breadth (x-position) for each node. + // Nodes are assigned the maximum breadth of incoming neighbors plus one; + // nodes with no incoming links are assigned breadth zero, while + // nodes with no outgoing links are assigned the maximum breadth. function computeNodeDepths() { var remainingNodes = nodes, nextNodes, @@ -228,14 +228,6 @@ module.exports = function() { scaleNodeBreadths((size[0] - dx) / (x - 1)); } - // function moveSourcesRight() { - // nodes.forEach(function(node) { - // if (!node.targetLinks.length) { - // node.x = min(node.sourceLinks, function(d) { return d.target.x; }) - 1; - // } - // }); - // } - function moveSinksRight(x) { nodes.forEach(function(node) { if(!node.sourceLinks.length) { @@ -258,7 +250,7 @@ module.exports = function() { .entries(nodes) .map(function(d) { return d.values; }); - // + // initializeNodeDepth(); resolveCollisions(); for(var alpha = 1; iterations > 0; --iterations) { @@ -320,7 +312,7 @@ module.exports = function() { n = nodes.length, i; - // Push any overlapping nodes down. + // Push any overlapping nodes down. nodes.sort(ascendingDepth); for(i = 0; i < n; ++i) { node = nodes[i]; @@ -329,12 +321,12 @@ module.exports = function() { y0 = node.y + node.dy + py; } - // If the bottommost node goes outside the bounds, push it back up. + // If the bottommost node goes outside the bounds, push it back up. dy = y0 - py - size[1]; if(dy > 0) { y0 = node.y -= dy; - // Push any overlapping nodes back up. + // Push any overlapping nodes back up. for(i = n - 2; i >= 0; --i) { node = nodes[i]; dy = node.y + node.dy + py - y0; From 1ad958662804895ffbc76cbe3d6d00e927576153 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Fri, 14 Dec 2018 22:36:00 -0500 Subject: [PATCH 6/8] updating to d3-sankey 0.7.1 --- package-lock.json | 23 +++ package.json | 7 +- src/traces/sankey/render.js | 372 ++++++++++++++++++------------------ 3 files changed, 216 insertions(+), 186 deletions(-) diff --git a/package-lock.json b/package-lock.json index 494ba124b4d..07b41d78297 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2222,11 +2222,34 @@ "d3-color": "1" } }, + "d3-path": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz", + "integrity": "sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA==" + }, "d3-quadtree": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.3.tgz", "integrity": "sha1-rHmH4+I/6AWpkPKOG1DTj8uCJDg=" }, + "d3-sankey": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.7.1.tgz", + "integrity": "sha1-0imDImj8aaf+yEgD6WwiVqYUxSE=", + "requires": { + "d3-array": "1", + "d3-collection": "1", + "d3-shape": "^1.2.0" + } + }, + "d3-shape": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.2.tgz", + "integrity": "sha512-hUGEozlKecFZ2bOSNt7ENex+4Tk9uc/m0TtTEHBvitCBxUNjhzm5hS2GrrVRD/ae4IylSmxGeqX5tWC2rASMlQ==", + "requires": { + "d3-path": "1" + } + }, "d3-timer": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.7.tgz", diff --git a/package.json b/package.json index 4611dc2072d..6d4bf466eed 100644 --- a/package.json +++ b/package.json @@ -56,9 +56,6 @@ ] }, "dependencies": { - "d3-array": "1", - "d3-collection": "1", - "d3-interpolate": "1", "3d-view": "^2.0.0", "alpha-shape": "^1.0.0", "array-range": "^1.0.1", @@ -67,7 +64,11 @@ "convex-hull": "^1.0.3", "country-regex": "^1.1.0", "d3": "^3.5.12", + "d3-array": "1", + "d3-collection": "1", "d3-force": "^1.0.6", + "d3-interpolate": "1", + "d3-sankey": "^0.7.1", "delaunay-triangulate": "^1.1.6", "es6-promise": "^3.0.2", "fast-isnumeric": "^1.1.2", diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 42520ddd4fd..7f21862a011 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -13,7 +13,8 @@ var d3 = require('d3'); var tinycolor = require('tinycolor2'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); -var d3sankey = require('./d3-sankey.js'); +var d3sankey = require('d3-sankey'); +var d3sankeyLinkHorizontal = require('d3').sankeyLinkHorizontal; var d3Force = require('d3-force'); var Lib = require('../../lib'); var gup = require('../../lib/gup'); @@ -78,13 +79,14 @@ function sankeyModel(layout, d, traceIndex) { var nodes = calcData._nodes; var links = calcData._links; - var sankey = d3sankey() + var sankey = d3sankey + .sankey() + .iterations(c.sankeyIterations) .size(horizontal ? [width, height] : [height, width]) .nodeWidth(nodeThickness) .nodePadding(nodePad) .nodes(nodes) - .links(links) - .layout(c.sankeyIterations); + .links(links); if(sankey.nodePadding() < nodePad) { Lib.warn('node.pad was reduced to ', sankey.nodePadding(), ' to fit within the figure.'); @@ -111,8 +113,6 @@ function sankeyModel(layout, d, traceIndex) { translateY: layout.height - domain.y[1] * layout.height + layout.margin.t, dragParallel: horizontal ? height : width, dragPerpendicular: horizontal ? width : height, - nodes: nodes, - links: links, arrangement: trace.arrangement, sankey: sankey, forceLayouts: {}, @@ -123,20 +123,21 @@ function sankeyModel(layout, d, traceIndex) { }; } -function linkModel(uniqueKeys, d, l) { +function linkModel(d, l, i) { var tc = tinycolor(l.color); var basicKey = l.source.label + '|' + l.target.label; - var foundKey = uniqueKeys[basicKey]; - uniqueKeys[basicKey] = (foundKey || 0) + 1; - var key = basicKey + '__' + uniqueKeys[basicKey]; + // var foundKey = uniqueKeys[basicKey]; + // uniqueKeys[basicKey] = (foundKey || 0) + 1; + // var key = basicKey + '__' + uniqueKeys[basicKey]; // for event data l.trace = d.trace; l.curveNumber = d.trace.index; return { - key: key, + key: basicKey, traceId: d.key, + pointNumber: l.pointNumber, link: l, tinyColorHue: Color.tinyRGB(tc), tinyColorAlpha: tc.getAlpha(), @@ -149,25 +150,38 @@ function linkModel(uniqueKeys, d, l) { }; } -function nodeModel(uniqueKeys, d, n) { +function linkPath() { + return d3sankey.sankeyLinkHorizontal() + .source(function(d) { return [d.link.source.x1, d.link.y0];}) + .target(function(d) { return [d.link.target.x0, d.link.y1];}); +} + +function nodeModel(d, n, i) { var tc = tinycolor(n.color), zoneThicknessPad = c.nodePadAcross, zoneLengthPad = d.nodePad / 2, - visibleThickness = n.dx, - visibleLength = Math.max(0.5, n.dy); + visibleThickness = n.x1 - n.x0, + visibleLength = Math.max(0.5, (n.y1 - n.y0)); var basicKey = n.label; - var foundKey = uniqueKeys[basicKey]; - uniqueKeys[basicKey] = (foundKey || 0) + 1; - var key = basicKey + '__' + uniqueKeys[basicKey]; + // var foundKey = uniqueKeys[basicKey]; + // uniqueKeys[basicKey] = (foundKey || 0) + 1; + // var key = basicKey + '__' + uniqueKeys[basicKey]; // for event data n.trace = d.trace; n.curveNumber = d.trace.index; + // additionnal coordinates + n.dx = n.x1 - n.x0; + n.dy = n.y1 - n.y0; return { - key: key, - traceId: d.key, + x0: n.x0, + x1: n.x1, + y0: n.y0, + y1: n.y1, + key: basicKey, + traceId: basicKey, node: n, nodePad: d.nodePad, nodeLineColor: d.nodeLineColor, @@ -192,7 +206,7 @@ function nodeModel(uniqueKeys, d, n) { valueSuffix: d.valueSuffix, sankey: d.sankey, arrangement: d.arrangement, - uniqueNodeLabelPathId: [d.guid, d.key, key].join(' '), + uniqueNodeLabelPathId: [d.guid, d.key].join(' '), interactionState: d.interactionState }; } @@ -202,33 +216,28 @@ function nodeModel(uniqueKeys, d, n) { function updateNodePositions(sankeyNode) { sankeyNode .attr('transform', function(d) { - return 'translate(' + d.node.x.toFixed(3) + ', ' + (d.node.y - d.node.dy / 2).toFixed(3) + ')'; + return 'translate(' + d.x0.toFixed(3) + ', ' + (d.y0).toFixed(3) + ')'; }); } -function linkPath(d) { - var nodes = d.sankey.nodes(); - switchToSankeyFormat(nodes); - var result = d.sankey.link()(d.link); - switchToForceFormat(nodes); - return result; -} - function updateNodeShapes(sankeyNode) { sankeyNode.call(updateNodePositions); } function updateShapes(sankeyNode, sankeyLink) { sankeyNode.call(updateNodeShapes); - sankeyLink.attr('d', linkPath); + sankeyLink.attr('d', linkPath()); } function sizeNode(rect) { - rect.attr('width', function(d) {return d.visibleWidth;}) - .attr('height', function(d) {return d.visibleHeight;}); + rect + // .attr('x', function(d) {return d.x0;}) + // .attr('y', function(d) {return d.y0;}) + .attr('width', function(d) {return d.x1 - d.x0;}) + .attr('height', function(d) {return d.y1 - d.y0;}); } -function salientEnough(d) {return d.link.dy > 1 || d.linkLineWidth > 0;} +function salientEnough(d) {return (d.y0 - d.y1) > 1 || d.linkLineWidth > 0;} function sankeyTransform(d) { var offset = 'translate(' + d.translateX + ',' + d.translateY + ')'; @@ -250,139 +259,139 @@ function textFlip(d) {return d.horizontal ? 'scale(1 1)' : 'scale(-1 1)';} function nodeTextColor(d) {return d.darkBackground && !d.horizontal ? 'rgb(255,255,255)' : 'rgb(0,0,0)';} function nodeTextOffset(d) {return d.horizontal && d.left ? '100%' : '0%';} -// event handling - -function attachPointerEvents(selection, sankey, eventSet) { - selection - .on('.basic', null) // remove any preexisting handlers - .on('mouseover.basic', function(d) { - if(!d.interactionState.dragInProgress) { - eventSet.hover(this, d, sankey); - d.interactionState.hovered = [this, d]; - } - }) - .on('mousemove.basic', function(d) { - if(!d.interactionState.dragInProgress) { - eventSet.follow(this, d); - d.interactionState.hovered = [this, d]; - } - }) - .on('mouseout.basic', function(d) { - if(!d.interactionState.dragInProgress) { - eventSet.unhover(this, d, sankey); - d.interactionState.hovered = false; - } - }) - .on('click.basic', function(d) { - if(d.interactionState.hovered) { - eventSet.unhover(this, d, sankey); - d.interactionState.hovered = false; - } - if(!d.interactionState.dragInProgress) { - eventSet.select(this, d, sankey); - } - }); -} - -function attachDragHandler(sankeyNode, sankeyLink, callbacks) { - - var dragBehavior = d3.behavior.drag() - - .origin(function(d) {return d.node;}) - - .on('dragstart', function(d) { - if(d.arrangement === 'fixed') return; - Lib.raiseToTop(this); - d.interactionState.dragInProgress = d.node; - saveCurrentDragPosition(d.node); - if(d.interactionState.hovered) { - callbacks.nodeEvents.unhover.apply(0, d.interactionState.hovered); - d.interactionState.hovered = false; - } - if(d.arrangement === 'snap') { - var forceKey = d.traceId + '|' + Math.floor(d.node.originalX); - if(d.forceLayouts[forceKey]) { - d.forceLayouts[forceKey].alpha(1); - } else { // make a forceLayout iff needed - attachForce(sankeyNode, forceKey, d); - } - startForce(sankeyNode, sankeyLink, d, forceKey); - } - }) - - .on('drag', function(d) { - if(d.arrangement === 'fixed') return; - var x = d3.event.x; - var y = d3.event.y; - if(d.arrangement === 'snap') { - d.node.x = x; - d.node.y = y; - } else { - if(d.arrangement === 'freeform') { - d.node.x = x; - } - d.node.y = Math.max(d.node.dy / 2, Math.min(d.size - d.node.dy / 2, y)); - } - saveCurrentDragPosition(d.node); - if(d.arrangement !== 'snap') { - d.sankey.relayout(); - updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); - } - }) - - .on('dragend', function(d) { - d.interactionState.dragInProgress = false; - }); - - sankeyNode - .on('.drag', null) // remove possible previous handlers - .call(dragBehavior); -} - -function attachForce(sankeyNode, forceKey, d) { - var nodes = d.sankey.nodes().filter(function(n) {return n.originalX === d.node.originalX;}); - d.forceLayouts[forceKey] = d3Force.forceSimulation(nodes) - .alphaDecay(0) - .force('collide', d3Force.forceCollide() - .radius(function(n) {return n.dy / 2 + d.nodePad / 2;}) - .strength(1) - .iterations(c.forceIterations)) - .force('constrain', snappingForce(sankeyNode, forceKey, nodes, d)) - .stop(); -} - -function startForce(sankeyNode, sankeyLink, d, forceKey) { - window.requestAnimationFrame(function faster() { - for(var i = 0; i < c.forceTicksPerFrame; i++) { - d.forceLayouts[forceKey].tick(); - } - d.sankey.relayout(); - updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); - if(d.forceLayouts[forceKey].alpha() > 0) { - window.requestAnimationFrame(faster); - } - }); -} - -function snappingForce(sankeyNode, forceKey, nodes, d) { - return function _snappingForce() { - var maxVelocity = 0; - for(var i = 0; i < nodes.length; i++) { - var n = nodes[i]; - if(n === d.interactionState.dragInProgress) { // constrain node position to the dragging pointer - n.x = n.lastDraggedX; - n.y = n.lastDraggedY; - } else { - n.vx = (n.originalX - n.x) / c.forceTicksPerFrame; // snap to layer - n.y = Math.min(d.size - n.dy / 2, Math.max(n.dy / 2, n.y)); // constrain to extent - } - maxVelocity = Math.max(maxVelocity, Math.abs(n.vx), Math.abs(n.vy)); - } - if(!d.interactionState.dragInProgress && maxVelocity < 0.1 && d.forceLayouts[forceKey].alpha() > 0) { - d.forceLayouts[forceKey].alpha(0); - } - }; -} +// // event handling +// +// function attachPointerEvents(selection, sankey, eventSet) { +// selection +// .on('.basic', null) // remove any preexisting handlers +// .on('mouseover.basic', function(d) { +// if(!d.interactionState.dragInProgress) { +// eventSet.hover(this, d, sankey); +// d.interactionState.hovered = [this, d]; +// } +// }) +// .on('mousemove.basic', function(d) { +// if(!d.interactionState.dragInProgress) { +// eventSet.follow(this, d); +// d.interactionState.hovered = [this, d]; +// } +// }) +// .on('mouseout.basic', function(d) { +// if(!d.interactionState.dragInProgress) { +// eventSet.unhover(this, d, sankey); +// d.interactionState.hovered = false; +// } +// }) +// .on('click.basic', function(d) { +// if(d.interactionState.hovered) { +// eventSet.unhover(this, d, sankey); +// d.interactionState.hovered = false; +// } +// if(!d.interactionState.dragInProgress) { +// eventSet.select(this, d, sankey); +// } +// }); +// } +// +// function attachDragHandler(sankeyNode, sankeyLink, callbacks) { +// +// var dragBehavior = d3.behavior.drag() +// +// .origin(function(d) {return d.node;}) +// +// .on('dragstart', function(d) { +// if(d.arrangement === 'fixed') return; +// Lib.raiseToTop(this); +// d.interactionState.dragInProgress = d.node; +// saveCurrentDragPosition(d.node); +// if(d.interactionState.hovered) { +// callbacks.nodeEvents.unhover.apply(0, d.interactionState.hovered); +// d.interactionState.hovered = false; +// } +// if(d.arrangement === 'snap') { +// var forceKey = d.traceId + '|' + Math.floor(d.node.originalX); +// if(d.forceLayouts[forceKey]) { +// d.forceLayouts[forceKey].alpha(1); +// } else { // make a forceLayout iff needed +// attachForce(sankeyNode, forceKey, d); +// } +// startForce(sankeyNode, sankeyLink, d, forceKey); +// } +// }) +// +// .on('drag', function(d) { +// if(d.arrangement === 'fixed') return; +// var x = d3.event.x; +// var y = d3.event.y; +// if(d.arrangement === 'snap') { +// d.node.x = x; +// d.node.y = y; +// } else { +// if(d.arrangement === 'freeform') { +// d.node.x = x; +// } +// d.node.y = Math.max(d.node.dy / 2, Math.min(d.size - d.node.dy / 2, y)); +// } +// saveCurrentDragPosition(d.node); +// if(d.arrangement !== 'snap') { +// d.sankey.relayout(); +// updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); +// } +// }) +// +// .on('dragend', function(d) { +// d.interactionState.dragInProgress = false; +// }); +// +// sankeyNode +// .on('.drag', null) // remove possible previous handlers +// .call(dragBehavior); +// } +// +// function attachForce(sankeyNode, forceKey, d) { +// var nodes = d.sankey.nodes().filter(function(n) {return n.originalX === d.node.originalX;}); +// d.forceLayouts[forceKey] = d3Force.forceSimulation(nodes) +// .alphaDecay(0) +// .force('collide', d3Force.forceCollide() +// .radius(function(n) {return n.dy / 2 + d.nodePad / 2;}) +// .strength(1) +// .iterations(c.forceIterations)) +// .force('constrain', snappingForce(sankeyNode, forceKey, nodes, d)) +// .stop(); +// } +// +// function startForce(sankeyNode, sankeyLink, d, forceKey) { +// window.requestAnimationFrame(function faster() { +// for(var i = 0; i < c.forceTicksPerFrame; i++) { +// d.forceLayouts[forceKey].tick(); +// } +// d.sankey.relayout(); +// updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); +// if(d.forceLayouts[forceKey].alpha() > 0) { +// window.requestAnimationFrame(faster); +// } +// }); +// } +// +// function snappingForce(sankeyNode, forceKey, nodes, d) { +// return function _snappingForce() { +// var maxVelocity = 0; +// for(var i = 0; i < nodes.length; i++) { +// var n = nodes[i]; +// if(n === d.interactionState.dragInProgress) { // constrain node position to the dragging pointer +// n.x = n.lastDraggedX; +// n.y = n.lastDraggedY; +// } else { +// n.vx = (n.originalX - n.x) / c.forceTicksPerFrame; // snap to layer +// n.y = Math.min(d.size - n.dy / 2, Math.max(n.dy / 2, n.y)); // constrain to extent +// } +// maxVelocity = Math.max(maxVelocity, Math.abs(n.vx), Math.abs(n.vy)); +// } +// if(!d.interactionState.dragInProgress && maxVelocity < 0.1 && d.forceLayouts[forceKey].alpha() > 0) { +// d.forceLayouts[forceKey].alpha(0); +// } +// }; +// } // scene graph module.exports = function(svg, calcData, layout, callbacks) { @@ -420,18 +429,15 @@ module.exports = function(svg, calcData, layout, callbacks) { .style('fill', 'none'); var sankeyLink = sankeyLinks.selectAll('.' + c.cn.sankeyLink) - .data(function(d) { - var uniqueKeys = {}; - return d.sankey.links() + .data(function(d) { + return d.sankey().links .filter(function(l) {return l.value;}) - .map(linkModel.bind(null, uniqueKeys, d)); - }, keyFun); - - sankeyLink.enter() - .append('path') - .classed(c.cn.sankeyLink, true) - .attr('d', linkPath) - .call(attachPointerEvents, sankey, callbacks.linkEvents); + .map(linkModel.bind(null, d)); + }, keyFun) + sankeyLink + .enter().append('path') + .attr('d', linkPath()); + // .call(attachPointerEvents, sankey, callbacks.linkEvents); sankeyLink .style('stroke', function(d) { @@ -445,8 +451,8 @@ module.exports = function(svg, calcData, layout, callbacks) { .style('fill-opacity', function(d) {return d.tinyColorAlpha;}); sankeyLink.transition() - .ease(c.ease).duration(c.duration) - .attr('d', linkPath); + .ease(c.ease).duration(c.duration) + .attr('d', linkPath()); sankeyLink.exit().transition() .ease(c.ease).duration(c.duration) @@ -471,22 +477,22 @@ module.exports = function(svg, calcData, layout, callbacks) { var sankeyNode = sankeyNodeSet.selectAll('.' + c.cn.sankeyNode) .data(function(d) { - var nodes = d.sankey.nodes(); + var nodes = d.sankey().nodes; var uniqueKeys = {}; persistOriginalPlace(nodes); return nodes .filter(function(n) {return n.value;}) - .map(nodeModel.bind(null, uniqueKeys, d)); + .map(nodeModel.bind(null, d)); }, keyFun); sankeyNode.enter() .append('g') .classed(c.cn.sankeyNode, true) .call(updateNodePositions) - .call(attachPointerEvents, sankey, callbacks.nodeEvents); + //.call(attachPointerEvents, sankey, callbacks.nodeEvents); sankeyNode - .call(attachDragHandler, sankeyLink, callbacks); // has to be here as it binds sankeyLink + //.call(attachDragHandler, sankeyLink, callbacks); // has to be here as it binds sankeyLink sankeyNode.transition() .ease(c.ease).duration(c.duration) From ee7088adec59aeab81ea582e570bc967ed8c023d Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Mon, 17 Dec 2018 15:52:43 -0500 Subject: [PATCH 7/8] generate link path appropriately, reattach mouse events --- src/traces/sankey/render.js | 98 ++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 7f21862a011..43be8bd01f2 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -126,6 +126,7 @@ function sankeyModel(layout, d, traceIndex) { function linkModel(d, l, i) { var tc = tinycolor(l.color); var basicKey = l.source.label + '|' + l.target.label; + var key = basicKey + '__' + i; // var foundKey = uniqueKeys[basicKey]; // uniqueKeys[basicKey] = (foundKey || 0) + 1; // var key = basicKey + '__' + uniqueKeys[basicKey]; @@ -135,7 +136,7 @@ function linkModel(d, l, i) { l.curveNumber = d.trace.index; return { - key: basicKey, + key: key, traceId: d.key, pointNumber: l.pointNumber, link: l, @@ -166,7 +167,7 @@ function nodeModel(d, n, i) { var basicKey = n.label; // var foundKey = uniqueKeys[basicKey]; // uniqueKeys[basicKey] = (foundKey || 0) + 1; - // var key = basicKey + '__' + uniqueKeys[basicKey]; + var key = basicKey + '__' + i; // for event data n.trace = d.trace; @@ -180,8 +181,8 @@ function nodeModel(d, n, i) { x1: n.x1, y0: n.y0, y1: n.y1, - key: basicKey, - traceId: basicKey, + key: key, + traceId: d.key, node: n, nodePad: d.nodePad, nodeLineColor: d.nodeLineColor, @@ -237,7 +238,7 @@ function sizeNode(rect) { .attr('height', function(d) {return d.y1 - d.y0;}); } -function salientEnough(d) {return (d.y0 - d.y1) > 1 || d.linkLineWidth > 0;} +function salientEnough(d) {return (d.y0 - d.y1) > 1 || d.link.width > 0 || d.linkLineWidth > 0;} function sankeyTransform(d) { var offset = 'translate(' + d.translateX + ',' + d.translateY + ')'; @@ -260,38 +261,38 @@ function nodeTextColor(d) {return d.darkBackground && !d.horizontal ? 'rgb(255,2 function nodeTextOffset(d) {return d.horizontal && d.left ? '100%' : '0%';} // // event handling -// -// function attachPointerEvents(selection, sankey, eventSet) { -// selection -// .on('.basic', null) // remove any preexisting handlers -// .on('mouseover.basic', function(d) { -// if(!d.interactionState.dragInProgress) { -// eventSet.hover(this, d, sankey); -// d.interactionState.hovered = [this, d]; -// } -// }) -// .on('mousemove.basic', function(d) { -// if(!d.interactionState.dragInProgress) { -// eventSet.follow(this, d); -// d.interactionState.hovered = [this, d]; -// } -// }) -// .on('mouseout.basic', function(d) { -// if(!d.interactionState.dragInProgress) { -// eventSet.unhover(this, d, sankey); -// d.interactionState.hovered = false; -// } -// }) -// .on('click.basic', function(d) { -// if(d.interactionState.hovered) { -// eventSet.unhover(this, d, sankey); -// d.interactionState.hovered = false; -// } -// if(!d.interactionState.dragInProgress) { -// eventSet.select(this, d, sankey); -// } -// }); -// } + +function attachPointerEvents(selection, sankey, eventSet) { + selection + .on('.basic', null) // remove any preexisting handlers + .on('mouseover.basic', function(d) { + if(!d.interactionState.dragInProgress) { + eventSet.hover(this, d, sankey); + d.interactionState.hovered = [this, d]; + } + }) + .on('mousemove.basic', function(d) { + if(!d.interactionState.dragInProgress) { + eventSet.follow(this, d); + d.interactionState.hovered = [this, d]; + } + }) + .on('mouseout.basic', function(d) { + if(!d.interactionState.dragInProgress) { + eventSet.unhover(this, d, sankey); + d.interactionState.hovered = false; + } + }) + .on('click.basic', function(d) { + if(d.interactionState.hovered) { + eventSet.unhover(this, d, sankey); + d.interactionState.hovered = false; + } + if(!d.interactionState.dragInProgress) { + eventSet.select(this, d, sankey); + } + }); +} // // function attachDragHandler(sankeyNode, sankeyLink, callbacks) { // @@ -433,22 +434,27 @@ module.exports = function(svg, calcData, layout, callbacks) { return d.sankey().links .filter(function(l) {return l.value;}) .map(linkModel.bind(null, d)); - }, keyFun) + }, keyFun); + sankeyLink .enter().append('path') - .attr('d', linkPath()); - // .call(attachPointerEvents, sankey, callbacks.linkEvents); + .classed(c.cn.sankeyLink, true) + .attr('d', linkPath()) + .call(attachPointerEvents, sankey, callbacks.linkEvents); sankeyLink .style('stroke', function(d) { - return salientEnough(d) ? Color.tinyRGB(tinycolor(d.linkLineColor)) : d.tinyColorHue; + // return salientEnough(d) ? Color.tinyRGB(tinycolor(d.linkLineColor)) : d.tinyColorHue; + return d.tinyColorHue; }) .style('stroke-opacity', function(d) { - return salientEnough(d) ? Color.opacity(d.linkLineColor) : d.tinyColorAlpha; + // return salientEnough(d) ? Color.opacity(d.linkLineColor) : d.tinyColorAlpha; + return d.tinyColorAlpha; }) - .style('stroke-width', function(d) {return salientEnough(d) ? d.linkLineWidth : 1;}) - .style('fill', function(d) {return d.tinyColorHue;}) - .style('fill-opacity', function(d) {return d.tinyColorAlpha;}); + .style('stroke-width', function(d) {return salientEnough(d) ? d.link.width : 1;}); + // Uncomment the following if the link isn't a simple SVG path element + // .style('fill', function(d) {return d.tinyColorHue;}) + // .style('fill-opacity', function(d) {return d.tinyColorAlpha;}); sankeyLink.transition() .ease(c.ease).duration(c.duration) @@ -489,7 +495,7 @@ module.exports = function(svg, calcData, layout, callbacks) { .append('g') .classed(c.cn.sankeyNode, true) .call(updateNodePositions) - //.call(attachPointerEvents, sankey, callbacks.nodeEvents); + .call(attachPointerEvents, sankey, callbacks.nodeEvents); sankeyNode //.call(attachDragHandler, sankeyLink, callbacks); // has to be here as it binds sankeyLink From 5418d25c757e77963d0aa7e1eedc39ff64cbab09 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 19 Dec 2018 17:27:09 -0500 Subject: [PATCH 8/8] fix dragging, snapping, pushing and pass all tests --- package-lock.json | 5 +- package.json | 2 +- src/traces/sankey/d3-sankey.js | 364 ----------------- src/traces/sankey/render.js | 377 ++++++++++-------- test/image/baselines/sankey_large_padding.png | Bin 52187 -> 52384 bytes test/jasmine/tests/sankey_d3_sankey_test.js | 13 +- 6 files changed, 214 insertions(+), 547 deletions(-) delete mode 100644 src/traces/sankey/d3-sankey.js diff --git a/package-lock.json b/package-lock.json index 07b41d78297..9cb0763e50f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2233,9 +2233,8 @@ "integrity": "sha1-rHmH4+I/6AWpkPKOG1DTj8uCJDg=" }, "d3-sankey": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.7.1.tgz", - "integrity": "sha1-0imDImj8aaf+yEgD6WwiVqYUxSE=", + "version": "git://github.com/antoinerg/d3-sankey.git#4f37ed8d3578b545a8569ecd74583f373768e900", + "from": "git://github.com/antoinerg/d3-sankey.git#4f37ed8d3578b545a8569ecd74583f373768e900", "requires": { "d3-array": "1", "d3-collection": "1", diff --git a/package.json b/package.json index 6d4bf466eed..6ff9698aae3 100644 --- a/package.json +++ b/package.json @@ -64,11 +64,11 @@ "convex-hull": "^1.0.3", "country-regex": "^1.1.0", "d3": "^3.5.12", + "d3-sankey": "git://github.com/antoinerg/d3-sankey.git#4f37ed8d3578b545a8569ecd74583f373768e900", "d3-array": "1", "d3-collection": "1", "d3-force": "^1.0.6", "d3-interpolate": "1", - "d3-sankey": "^0.7.1", "delaunay-triangulate": "^1.1.6", "es6-promise": "^3.0.2", "fast-isnumeric": "^1.1.2", diff --git a/src/traces/sankey/d3-sankey.js b/src/traces/sankey/d3-sankey.js deleted file mode 100644 index c1ee9bc2f2a..00000000000 --- a/src/traces/sankey/d3-sankey.js +++ /dev/null @@ -1,364 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -/** -Copyright 2015, Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -*/ - -/** -The following is a fork of https://github.com/plotly/d3-sankey which itself -was a fork of https://github.com/d3/d3-sankey from Mike Bostock. -*/ - -'use strict'; - -var d3array = require('d3-array'); -var ascending = d3array.ascending; -var min = d3array.min; -var sum = d3array.sum; -var max = d3array.max; -var nest = require('d3-collection').nest; -var interpolateNumber = require('d3-interpolate').interpolateNumber; - -// sort links' breadth (ie top to bottom in a column), based on their source nodes' breadths -function ascendingSourceDepth(a, b) { - return ascendingBreadth(a.source, b.source) || (a.originalIndex - b.originalIndex); -} - -// sort links' breadth (ie top to bottom in a column), based on their target nodes' breadths -function ascendingTargetDepth(a, b) { - return ascendingBreadth(a.target, b.target) || (a.originalIndex - b.originalIndex); -} - -function ascendingBreadth(a, b) { - return a.y - b.y; -} - -function value(d) { - return d.value; -} - -function nodeCenter(node) { - return node.y + node.dy / 2; -} - -function weightedSource(link) { - return nodeCenter(link.source) * link.value; -} - -function weightedTarget(link) { - return nodeCenter(link.target) * link.value; -} - -module.exports = function() { - var sankey = {}, - dx = 24, // nodeWidth - py = 8, // nodePadding - size = [1, 1], - nodes = [], - links = [], - maxPaddedSpace = 2 / 3; // Defined as a fraction of the total available space - - sankey.nodeWidth = function(_) { - if(!arguments.length) return dx; - dx = +_; - return sankey; - }; - - sankey.nodePadding = function(_) { - if(!arguments.length) return py; - py = +_; - return sankey; - }; - - sankey.nodes = function(_) { - if(!arguments.length) return nodes; - nodes = _; - return sankey; - }; - - sankey.links = function(_) { - if(!arguments.length) return links; - links = _; - return sankey; - }; - - sankey.size = function(_) { - if(!arguments.length) return size; - size = _; - return sankey; - }; - - sankey.layout = function(iterations) { - computeNodeLinks(); - computeNodeValues(); - computeNodeDepths(); - computeNodeBreadths(iterations); - computeLinkBreadths(); - return sankey; - }; - - sankey.relayout = function() { - computeLinkBreadths(); - return sankey; - }; - - sankey.link = function() { - var curvature = 0.5; - - function link(d) { - var x0 = d.source.x + d.source.dx, - x1 = d.target.x, - xi = interpolateNumber(x0, x1), - x2 = xi(curvature), - x3 = xi(1 - curvature), - y0a = d.source.y + d.sy, - y0b = y0a + d.dy, - y1a = d.target.y + d.ty, - y1b = y1a + d.dy; - return 'M' + x0 + ',' + y0a + - 'C' + x2 + ',' + y0a + - ' ' + x3 + ',' + y1a + - ' ' + x1 + ',' + y1a + - 'L' + x1 + ',' + y1b + - 'C' + x3 + ',' + y1b + - ' ' + x2 + ',' + y0b + - ' ' + x0 + ',' + y0b + - 'Z'; - } - - link.curvature = function(_) { - if(!arguments.length) return curvature; - curvature = +_; - return link; - }; - - return link; - }; - - // Populate the sourceLinks and targetLinks for each node. - // Also, if the source and target are not objects, assume they are indices. - function computeNodeLinks() { - nodes.forEach(function(node) { - node.sourceLinks = []; - node.targetLinks = []; - }); - links.forEach(function(link, i) { - var source = link.source, - target = link.target; - if(typeof source === 'number') source = link.source = nodes[link.source]; - if(typeof target === 'number') target = link.target = nodes[link.target]; - link.originalIndex = i; - source.sourceLinks.push(link); - target.targetLinks.push(link); - }); - } - - // Compute the value (size) of each node by summing the associated links. - function computeNodeValues() { - nodes.forEach(function(node) { - node.value = Math.max( - sum(node.sourceLinks, value), - sum(node.targetLinks, value) - ); - }); - } - - // Iteratively assign the breadth (x-position) for each node. - // Nodes are assigned the maximum breadth of incoming neighbors plus one; - // nodes with no incoming links are assigned breadth zero, while - // nodes with no outgoing links are assigned the maximum breadth. - function computeNodeDepths() { - var remainingNodes = nodes, - nextNodes, - x = 0; - - function processNode(node) { - node.depth = x; - node.x = x; - node.dx = dx; - node.sourceLinks.forEach(function(link) { - if(nextNodes.indexOf(link.target) < 0) { - nextNodes.push(link.target); - } - }); - } - - while(remainingNodes.length) { - nextNodes = []; - remainingNodes.forEach(processNode); - remainingNodes = nextNodes; - ++x; - } - - // - moveSinksRight(x); - scaleNodeBreadths((size[0] - dx) / (x - 1)); - } - - function moveSinksRight(x) { - nodes.forEach(function(node) { - if(!node.sourceLinks.length) { - node.depth = x - 1; - node.x = x - 1; - } - }); - } - - function scaleNodeBreadths(kx) { - nodes.forEach(function(node) { - node.x *= kx; - }); - } - - function computeNodeBreadths(iterations) { - var nodesByBreadth = nest() - .key(function(d) { return d.x; }) - .sortKeys(ascending) - .entries(nodes) - .map(function(d) { return d.values; }); - - // - initializeNodeDepth(); - resolveCollisions(); - for(var alpha = 1; iterations > 0; --iterations) { - relaxRightToLeft(alpha *= 0.99); - resolveCollisions(); - relaxLeftToRight(alpha); - resolveCollisions(); - } - - function initializeNodeDepth() { - var L = max(nodesByBreadth, function(nodes) { - return nodes.length; - }); - var maxNodePadding = maxPaddedSpace * size[1] / (L - 1); - if(py > maxNodePadding) py = maxNodePadding; - var ky = min(nodesByBreadth, function(nodes) { - return (size[1] - (nodes.length - 1) * py) / sum(nodes, value); - }); - - nodesByBreadth.forEach(function(nodes) { - nodes.forEach(function(node, i) { - node.y = i; - node.dy = node.value * ky; - }); - }); - - links.forEach(function(link) { - link.dy = link.value * ky; - }); - } - - function relaxLeftToRight(alpha) { - nodesByBreadth.forEach(function(nodes) { - nodes.forEach(function(node) { - if(node.targetLinks.length) { - var y = sum(node.targetLinks, weightedSource) / sum(node.targetLinks, value); - node.y += (y - nodeCenter(node)) * alpha; - } - }); - }); - } - - function relaxRightToLeft(alpha) { - nodesByBreadth.slice().reverse().forEach(function(nodes) { - nodes.forEach(function(node) { - if(node.sourceLinks.length) { - var y = sum(node.sourceLinks, weightedTarget) / sum(node.sourceLinks, value); - node.y += (y - nodeCenter(node)) * alpha; - } - }); - }); - } - - function resolveCollisions() { - nodesByBreadth.forEach(function(nodes) { - var node, - dy, - y0 = 0, - n = nodes.length, - i; - - // Push any overlapping nodes down. - nodes.sort(ascendingDepth); - for(i = 0; i < n; ++i) { - node = nodes[i]; - dy = y0 - node.y; - if(dy > 0) node.y += dy; - y0 = node.y + node.dy + py; - } - - // If the bottommost node goes outside the bounds, push it back up. - dy = y0 - py - size[1]; - if(dy > 0) { - y0 = node.y -= dy; - - // Push any overlapping nodes back up. - for(i = n - 2; i >= 0; --i) { - node = nodes[i]; - dy = node.y + node.dy + py - y0; - if(dy > 0) node.y -= dy; - y0 = node.y; - } - } - }); - } - - function ascendingDepth(a, b) { - return a.y - b.y; - } - } - - function computeLinkBreadths() { - nodes.forEach(function(node) { - node.sourceLinks.sort(ascendingTargetDepth); - node.targetLinks.sort(ascendingSourceDepth); - }); - nodes.forEach(function(node) { - var sy = 0, ty = 0; - node.sourceLinks.forEach(function(link) { - link.sy = sy; - sy += link.dy; - }); - node.targetLinks.forEach(function(link) { - link.ty = ty; - ty += link.dy; - }); - }); - } - - return sankey; -}; diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index 43be8bd01f2..87a5b9f486c 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -14,54 +14,13 @@ var tinycolor = require('tinycolor2'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var d3sankey = require('d3-sankey'); -var d3sankeyLinkHorizontal = require('d3').sankeyLinkHorizontal; var d3Force = require('d3-force'); var Lib = require('../../lib'); var gup = require('../../lib/gup'); var keyFun = gup.keyFun; var repeat = gup.repeat; var unwrap = gup.unwrap; - -// basic data utilities - -function persistOriginalPlace(nodes) { - var i, distinctLayerPositions = []; - for(i = 0; i < nodes.length; i++) { - nodes[i].originalX = nodes[i].x; - nodes[i].originalY = nodes[i].y; - if(distinctLayerPositions.indexOf(nodes[i].x) === -1) { - distinctLayerPositions.push(nodes[i].x); - } - } - distinctLayerPositions.sort(function(a, b) {return a - b;}); - for(i = 0; i < nodes.length; i++) { - nodes[i].originalLayerIndex = distinctLayerPositions.indexOf(nodes[i].originalX); - nodes[i].originalLayer = nodes[i].originalLayerIndex / (distinctLayerPositions.length - 1); - } -} - -function saveCurrentDragPosition(d) { - d.lastDraggedX = d.x; - d.lastDraggedY = d.y; -} - -function sameLayer(d) { - return function(n) {return n.node.originalX === d.node.originalX;}; -} - -function switchToForceFormat(nodes) { - // force uses x, y as centers - for(var i = 0; i < nodes.length; i++) { - nodes[i].y = nodes[i].y + nodes[i].dy / 2; - } -} - -function switchToSankeyFormat(nodes) { - // sankey uses x, y as top left - for(var i = 0; i < nodes.length; i++) { - nodes[i].y = nodes[i].y - nodes[i].dy / 2; - } -} +var interpolateNumber = require('d3-interpolate').interpolateNumber; // view models @@ -88,12 +47,11 @@ function sankeyModel(layout, d, traceIndex) { .nodes(nodes) .links(links); + var graph = sankey(); if(sankey.nodePadding() < nodePad) { Lib.warn('node.pad was reduced to ', sankey.nodePadding(), ' to fit within the figure.'); } - switchToForceFormat(nodes); - return { key: traceIndex, trace: trace, @@ -115,6 +73,7 @@ function sankeyModel(layout, d, traceIndex) { dragPerpendicular: horizontal ? width : height, arrangement: trace.arrangement, sankey: sankey, + graph: graph, forceLayouts: {}, interactionState: { dragInProgress: false, @@ -127,9 +86,6 @@ function linkModel(d, l, i) { var tc = tinycolor(l.color); var basicKey = l.source.label + '|' + l.target.label; var key = basicKey + '__' + i; - // var foundKey = uniqueKeys[basicKey]; - // uniqueKeys[basicKey] = (foundKey || 0) + 1; - // var key = basicKey + '__' + uniqueKeys[basicKey]; // for event data l.trace = d.trace; @@ -152,9 +108,29 @@ function linkModel(d, l, i) { } function linkPath() { - return d3sankey.sankeyLinkHorizontal() - .source(function(d) { return [d.link.source.x1, d.link.y0];}) - .target(function(d) { return [d.link.target.x0, d.link.y1];}); + var curvature = 0.5; + + function shape(d) { + var x0 = d.link.source.x1, + x1 = d.link.target.x0, + xi = interpolateNumber(x0, x1), + x2 = xi(curvature), + x3 = xi(1 - curvature), + y0a = d.link.y0 - d.link.width / 2, + y0b = d.link.y0 + d.link.width / 2, + y1a = d.link.y1 - d.link.width / 2, + y1b = d.link.y1 + d.link.width / 2; + return 'M' + x0 + ',' + y0a + + 'C' + x2 + ',' + y0a + + ' ' + x3 + ',' + y1a + + ' ' + x1 + ',' + y1a + + 'L' + x1 + ',' + y1b + + 'C' + x3 + ',' + y1b + + ' ' + x2 + ',' + y0b + + ' ' + x0 + ',' + y0b + + 'Z'; + } + return shape; } function nodeModel(d, n, i) { @@ -165,8 +141,6 @@ function nodeModel(d, n, i) { visibleLength = Math.max(0.5, (n.y1 - n.y0)); var basicKey = n.label; - // var foundKey = uniqueKeys[basicKey]; - // uniqueKeys[basicKey] = (foundKey || 0) + 1; var key = basicKey + '__' + i; // for event data @@ -176,11 +150,8 @@ function nodeModel(d, n, i) { // additionnal coordinates n.dx = n.x1 - n.x0; n.dy = n.y1 - n.y0; + return { - x0: n.x0, - x1: n.x1, - y0: n.y0, - y1: n.y1, key: key, traceId: d.key, node: n, @@ -206,8 +177,9 @@ function nodeModel(d, n, i) { valueFormat: d.valueFormat, valueSuffix: d.valueSuffix, sankey: d.sankey, + graph: d.sankey(), arrangement: d.arrangement, - uniqueNodeLabelPathId: [d.guid, d.key].join(' '), + uniqueNodeLabelPathId: [d.guid, d.key, key].join('_'), interactionState: d.interactionState }; } @@ -217,7 +189,7 @@ function nodeModel(d, n, i) { function updateNodePositions(sankeyNode) { sankeyNode .attr('transform', function(d) { - return 'translate(' + d.x0.toFixed(3) + ', ' + (d.y0).toFixed(3) + ')'; + return 'translate(' + d.node.x0.toFixed(3) + ', ' + (d.node.y0).toFixed(3) + ')'; }); } @@ -232,13 +204,11 @@ function updateShapes(sankeyNode, sankeyLink) { function sizeNode(rect) { rect - // .attr('x', function(d) {return d.x0;}) - // .attr('y', function(d) {return d.y0;}) - .attr('width', function(d) {return d.x1 - d.x0;}) - .attr('height', function(d) {return d.y1 - d.y0;}); + .attr('width', function(d) {return d.node.x1 - d.node.x0;}) + .attr('height', function(d) {return d.visibleHeight;}); } -function salientEnough(d) {return (d.y0 - d.y1) > 1 || d.link.width > 0 || d.linkLineWidth > 0;} +function salientEnough(d) {return (d.link.width > 1 || d.linkLineWidth > 0);} function sankeyTransform(d) { var offset = 'translate(' + d.translateX + ',' + d.translateY + ')'; @@ -293,106 +263,171 @@ function attachPointerEvents(selection, sankey, eventSet) { } }); } -// -// function attachDragHandler(sankeyNode, sankeyLink, callbacks) { -// -// var dragBehavior = d3.behavior.drag() -// -// .origin(function(d) {return d.node;}) -// -// .on('dragstart', function(d) { -// if(d.arrangement === 'fixed') return; -// Lib.raiseToTop(this); -// d.interactionState.dragInProgress = d.node; -// saveCurrentDragPosition(d.node); -// if(d.interactionState.hovered) { -// callbacks.nodeEvents.unhover.apply(0, d.interactionState.hovered); -// d.interactionState.hovered = false; -// } -// if(d.arrangement === 'snap') { -// var forceKey = d.traceId + '|' + Math.floor(d.node.originalX); -// if(d.forceLayouts[forceKey]) { -// d.forceLayouts[forceKey].alpha(1); -// } else { // make a forceLayout iff needed -// attachForce(sankeyNode, forceKey, d); -// } -// startForce(sankeyNode, sankeyLink, d, forceKey); -// } -// }) -// -// .on('drag', function(d) { -// if(d.arrangement === 'fixed') return; -// var x = d3.event.x; -// var y = d3.event.y; -// if(d.arrangement === 'snap') { -// d.node.x = x; -// d.node.y = y; -// } else { -// if(d.arrangement === 'freeform') { -// d.node.x = x; -// } -// d.node.y = Math.max(d.node.dy / 2, Math.min(d.size - d.node.dy / 2, y)); -// } -// saveCurrentDragPosition(d.node); -// if(d.arrangement !== 'snap') { -// d.sankey.relayout(); -// updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); -// } -// }) -// -// .on('dragend', function(d) { -// d.interactionState.dragInProgress = false; -// }); -// -// sankeyNode -// .on('.drag', null) // remove possible previous handlers -// .call(dragBehavior); -// } -// -// function attachForce(sankeyNode, forceKey, d) { -// var nodes = d.sankey.nodes().filter(function(n) {return n.originalX === d.node.originalX;}); -// d.forceLayouts[forceKey] = d3Force.forceSimulation(nodes) -// .alphaDecay(0) -// .force('collide', d3Force.forceCollide() -// .radius(function(n) {return n.dy / 2 + d.nodePad / 2;}) -// .strength(1) -// .iterations(c.forceIterations)) -// .force('constrain', snappingForce(sankeyNode, forceKey, nodes, d)) -// .stop(); -// } -// -// function startForce(sankeyNode, sankeyLink, d, forceKey) { -// window.requestAnimationFrame(function faster() { -// for(var i = 0; i < c.forceTicksPerFrame; i++) { -// d.forceLayouts[forceKey].tick(); -// } -// d.sankey.relayout(); -// updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); -// if(d.forceLayouts[forceKey].alpha() > 0) { -// window.requestAnimationFrame(faster); -// } -// }); -// } -// -// function snappingForce(sankeyNode, forceKey, nodes, d) { -// return function _snappingForce() { -// var maxVelocity = 0; -// for(var i = 0; i < nodes.length; i++) { -// var n = nodes[i]; -// if(n === d.interactionState.dragInProgress) { // constrain node position to the dragging pointer -// n.x = n.lastDraggedX; -// n.y = n.lastDraggedY; -// } else { -// n.vx = (n.originalX - n.x) / c.forceTicksPerFrame; // snap to layer -// n.y = Math.min(d.size - n.dy / 2, Math.max(n.dy / 2, n.y)); // constrain to extent -// } -// maxVelocity = Math.max(maxVelocity, Math.abs(n.vx), Math.abs(n.vy)); -// } -// if(!d.interactionState.dragInProgress && maxVelocity < 0.1 && d.forceLayouts[forceKey].alpha() > 0) { -// d.forceLayouts[forceKey].alpha(0); -// } -// }; -// } + +function attachDragHandler(sankeyNode, sankeyLink, callbacks) { + var dragBehavior = d3.behavior.drag() + .origin(function(d) { + return { + x: d.node.x0, + y: d.node.y0 + }; + }) + + .on('dragstart', function(d) { + if(d.arrangement === 'fixed') return; + Lib.raiseToTop(this); + d.interactionState.dragInProgress = d.node; + + saveCurrentDragPosition(d.node); + if(d.interactionState.hovered) { + callbacks.nodeEvents.unhover.apply(0, d.interactionState.hovered); + d.interactionState.hovered = false; + } + if(d.arrangement === 'snap') { + var forceKey = d.traceId + '|' + d.key; + if(d.forceLayouts[forceKey]) { + d.forceLayouts[forceKey].alpha(1); + } else { // make a forceLayout if needed + attachForce(sankeyNode, forceKey, d); + } + startForce(sankeyNode, sankeyLink, d, forceKey); + } + }) + + .on('drag', function(d) { + if(d.arrangement === 'fixed') return; + var x = d3.event.x; + var y = d3.event.y; + if(d.arrangement === 'snap') { + d.node.x0 = x - d.visibleWidth / 2; + d.node.x1 = x + d.visibleWidth / 2; + d.node.y0 = y - d.visibleHeight / 2; + d.node.y1 = y + d.visibleHeight / 2; + } else { + if(d.arrangement === 'freeform') { + d.node.x0 = x - d.visibleWidth / 2; + d.node.x1 = x + d.visibleWidth / 2; + // d.x0 = x; + } + // d.node.y = Math.max(d.node.dy / 2, Math.min(d.size - d.node.dy / 2, y)); + d.node.y0 = Math.max(0, Math.min(d.size - d.visibleHeight, y)); + d.node.y1 = d.node.y0 + d.visibleHeight; + } + + saveCurrentDragPosition(d.node); + if(d.arrangement !== 'snap') { + d.sankey.update(d.graph); + updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); + } + }) + + .on('dragend', function(d) { + d.interactionState.dragInProgress = false; + }); + + sankeyNode + .on('.drag', null) // remove possible previous handlers + .call(dragBehavior); +} + +function attachForce(sankeyNode, forceKey, d) { + // Attach force to nodes in the same column (same x coordinate) + switchToForceFormat(d.graph.nodes); + var nodes = d.graph.nodes.filter(function(n) {return n.originalX === d.node.originalX;}); + d.forceLayouts[forceKey] = d3Force.forceSimulation(nodes) + .alphaDecay(0) + .force('collide', d3Force.forceCollide() + .radius(function(n) {return n.dy / 2 + d.nodePad / 2;}) + .strength(1) + .iterations(c.forceIterations)) + .force('constrain', snappingForce(sankeyNode, forceKey, nodes, d)) + .stop(); +} + +function startForce(sankeyNode, sankeyLink, d, forceKey) { + window.requestAnimationFrame(function faster() { + var i; + for(i = 0; i < c.forceTicksPerFrame; i++) { + d.forceLayouts[forceKey].tick(); + } + + var nodes = d.graph.nodes; + switchToSankeyFormat(nodes); + + d.sankey.update(d.graph); + updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); + + if(d.forceLayouts[forceKey].alpha() > 0) { + window.requestAnimationFrame(faster); + } + }); +} + +function snappingForce(sankeyNode, forceKey, nodes, d) { + return function _snappingForce() { + var maxVelocity = 0; + for(var i = 0; i < nodes.length; i++) { + var n = nodes[i]; + if(n === d.interactionState.dragInProgress) { // constrain node position to the dragging pointer + n.x = n.lastDraggedX; + n.y = n.lastDraggedY; + } else { + n.vx = (n.originalX - n.x) / c.forceTicksPerFrame; // snap to layer + n.y = Math.min(d.size - n.dy / 2, Math.max(n.dy / 2, n.y)); // constrain to extent + } + maxVelocity = Math.max(maxVelocity, Math.abs(n.vx), Math.abs(n.vy)); + } + if(!d.interactionState.dragInProgress && maxVelocity < 0.1 && d.forceLayouts[forceKey].alpha() > 0) { + d.forceLayouts[forceKey].alpha(0); + } + }; +} + +// basic data utilities + +function persistOriginalPlace(nodes) { + var i, distinctLayerPositions = []; + for(i = 0; i < nodes.length; i++) { + nodes[i].originalX = (nodes[i].x0 + nodes[i].x1) / 2; + nodes[i].originalY = (nodes[i].y0 + nodes[i].y1) / 2; + if(distinctLayerPositions.indexOf(nodes[i].originalX) === -1) { + distinctLayerPositions.push(nodes[i].originalX); + } + } + distinctLayerPositions.sort(function(a, b) {return a - b;}); + for(i = 0; i < nodes.length; i++) { + nodes[i].originalLayerIndex = distinctLayerPositions.indexOf(nodes[i].originalX); + nodes[i].originalLayer = nodes[i].originalLayerIndex / (distinctLayerPositions.length - 1); + } +} + +function saveCurrentDragPosition(d) { + d.lastDraggedX = d.x0 + d.dx / 2; + d.lastDraggedY = d.y0 + d.dy / 2; +} + +function sameLayer(d) { + return function(n) {return n.node.originalX === d.node.originalX;}; +} + +function switchToForceFormat(nodes) { + // force uses x, y as centers + for(var i = 0; i < nodes.length; i++) { + nodes[i].y = nodes[i].y0 + nodes[i].dy / 2; + nodes[i].x = nodes[i].x0 + nodes[i].dx / 2; + } +} + +function switchToSankeyFormat(nodes) { + // sankey uses x0, x1, y0, y1 + for(var i = 0; i < nodes.length; i++) { + nodes[i].y0 = nodes[i].y - nodes[i].dy / 2; + nodes[i].y1 = nodes[i].y0 + nodes[i].dy; + + nodes[i].x0 = nodes[i].x - nodes[i].dx / 2; + nodes[i].x1 = nodes[i].x0 + nodes[i].dx; + } +} // scene graph module.exports = function(svg, calcData, layout, callbacks) { @@ -444,17 +479,14 @@ module.exports = function(svg, calcData, layout, callbacks) { sankeyLink .style('stroke', function(d) { - // return salientEnough(d) ? Color.tinyRGB(tinycolor(d.linkLineColor)) : d.tinyColorHue; - return d.tinyColorHue; + return salientEnough(d) ? Color.tinyRGB(tinycolor(d.linkLineColor)) : d.tinyColorHue; }) .style('stroke-opacity', function(d) { - // return salientEnough(d) ? Color.opacity(d.linkLineColor) : d.tinyColorAlpha; - return d.tinyColorAlpha; + return salientEnough(d) ? Color.opacity(d.linkLineColor) : d.tinyColorAlpha; }) - .style('stroke-width', function(d) {return salientEnough(d) ? d.link.width : 1;}); - // Uncomment the following if the link isn't a simple SVG path element - // .style('fill', function(d) {return d.tinyColorHue;}) - // .style('fill-opacity', function(d) {return d.tinyColorAlpha;}); + .style('stroke-width', function(d) {return salientEnough(d) ? d.linkLineWidth : 1;}) + .style('fill', function(d) {return d.tinyColorHue;}) + .style('fill-opacity', function(d) {return d.tinyColorAlpha;}); sankeyLink.transition() .ease(c.ease).duration(c.duration) @@ -484,7 +516,6 @@ module.exports = function(svg, calcData, layout, callbacks) { var sankeyNode = sankeyNodeSet.selectAll('.' + c.cn.sankeyNode) .data(function(d) { var nodes = d.sankey().nodes; - var uniqueKeys = {}; persistOriginalPlace(nodes); return nodes .filter(function(n) {return n.value;}) @@ -498,7 +529,7 @@ module.exports = function(svg, calcData, layout, callbacks) { .call(attachPointerEvents, sankey, callbacks.nodeEvents); sankeyNode - //.call(attachDragHandler, sankeyLink, callbacks); // has to be here as it binds sankeyLink + .call(attachDragHandler, sankeyLink, callbacks); // has to be here as it binds sankeyLink sankeyNode.transition() .ease(c.ease).duration(c.duration) diff --git a/test/image/baselines/sankey_large_padding.png b/test/image/baselines/sankey_large_padding.png index 38d263f799df4f8cec4bec6b2230daaed14601b9..b27b24c12eb8b973a2d0a24fbbee0ccd269d5a9d 100644 GIT binary patch delta 24190 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 P23{tHrwz*W9HRaU?>FuU delta 24043 zcmZ5|WmHsO*e@}_&_g#2-QC>{(%mgcS%e6OZUt$i5tNb?5E*Ka5JZ%aQecn}si7P0 z;s4%ut-J0Q7K<>_%BXptPcX!0&3mln zLJ7%AjV8zW0%ueGqASX#4ik;pd`wG5sp+9%@)?4wAu1wXQgXra2e>AAJm0+Ywsy;6 z(xLs-q1H0ks^_$3G2--h`(F2DA-1OhIlL1mRNrK}iasYvjhP*FTq+6OGtp-97~O>c zx@xXlGYu>`0yeIhxX_(xD&wB#a!0kbI@ z8iq+=u0x6#q3k?pcJg^Gie)JL=`UM>fr<}n%jztXlyBagcJKN7_QmFC$`|yKVmRTw zk(ld6gM`~7B6AEmLJmGTc-%nOXpSIZgAp?WPTe~PW&6rF3VU6$MaZ(Z6)PN~$AbB2 z*VQZVDNTl5r5=@?lwrFn#=cWG8cB%S*KL2>X_vpa`?YpHpIrBlu`xZe+|*6frWEmj zOCL?5lNviir5>Rcq1caA3xzp2`3O%Il@o<`m?KAL*#44Q1aHqal>xx*O`F`|4{esX zA8)i7%HozY)?b{;2W`{hk}&4wlICAG$v8{)>A$c@P#FFoz4Tn%yhMtZvE9z!%I5jQ z{vP!>hM^QR*WZ3N1b**i{oR)q!V<_PA1kWaRa0sdugFX$%1BAbE`fQDWd97=o(r^J zkIoIA;|tqZzLxUy$!CBg%-_8+W!fCdt1!ububsx6z!OC zqjLnh8_6!4|4rfH zo%kNxBT$JAK^OVfAvO}m6Asa_(rC2h^B!o|FTXHs9ap|Zd^fjm;zO*8#!0|%ZqNRt z*%eO)?ab$w6Wa7*seLok36Qc4Wk*!!sZQ|JN7BeK_=HN;YuhMj@u9AlV@L(=`^|sw zUY8F>>Yotm2w0nT?S28cOFkqAx%W9UVEv+(^1Yua<+!b%{yOYHHlSHbL45sfGgP&y z$LviR`zlIM%BN=+jx4}X*x`f3s%*l0$rZeZLmET%-|1lW*+GyM^R%Ypa7$#$Kj>!H z(S-i{qaE>a1&ZR3l_Zgfs}Q@_+??h`a!&=;%)oVWAzre$(*P^_gM?&DOQ(EL;ZtEt zTrT|ZizECO4AO1HX86MSbZGrIf7L>Ag`NE8=uPn(Sc=qG&d}4bDym?~s&Rs1vZk9s zOIMeBu8$^-fWt1N-o~hsjsm%={?L^LPmv6sMO!Z1As-4?Q23^38hWI(SB@ z&E021^pyYo4A5kgyT0T4Z#!_Lo!rMQv$#muFf0nV-g1iKG(Ii9aIr3a_sQ0UIKx0I z-modyS)%ix!rotADxO%IA$Um*ROl_L;fOQV zjq#F_cj%Mtxk)tpZ}5a8jujb(1YWqQ_=0sdPrBn#Lsf2nrE)WelR0bJ)ZVTQGrWrj z=yt4QE0TCt8KtTl{??9OMrreq%c$7b66b;^pANAvFB`nj9#C?+`042u@_5E6z>-mt zHC+gc4YA2qxpQ|wDtT(^3C@)BZcijyY@Dz9-_k@%|6C1uH{n zMM@+z;FFHG{9RSX!E6>WCZR$8o1QZ5fkIHFF(iCbEMJuNx=xF;fBrU)2?Sth)9@SQ zUjn0$@3xs-hNpei4bT3cTP2m1cJcEBD=Sg??WzLA?%Qbb@9+;S~v^&|C≪* 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 Ul9HNaD