diff --git a/package-lock.json b/package-lock.json index 4cc24650b6d..9cb0763e50f 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", @@ -2232,11 +2222,33 @@ "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": "git://github.com/antoinerg/d3-sankey.git#4f37ed8d3578b545a8569ecd74583f373768e900", + "from": "git://github.com/antoinerg/d3-sankey.git#4f37ed8d3578b545a8569ecd74583f373768e900", + "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 ba09779f6fc..6ff9698aae3 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ }, "dependencies": { "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", @@ -65,7 +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", "delaunay-triangulate": "^1.1.6", "es6-promise": "^3.0.2", "fast-isnumeric": "^1.1.2", 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..98ec340061d --- /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 2ec53241b9c..87a5b9f486c 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -13,154 +13,45 @@ 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'); 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; 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 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 nodes = calcData._nodes; + var links = calcData._links; - 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 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); + var graph = sankey(); if(sankey.nodePadding() < nodePad) { 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 { key: traceIndex, trace: trace, @@ -168,22 +59,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, + graph: graph, forceLayouts: {}, interactionState: { dragInProgress: false, @@ -192,12 +82,10 @@ 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 key = basicKey + '__' + i; // for event data l.trace = d.trace; @@ -206,6 +94,7 @@ function linkModel(uniqueKeys, d, l) { return { key: key, traceId: d.key, + pointNumber: l.pointNumber, link: l, tinyColorHue: Color.tinyRGB(tc), tinyColorAlpha: tc.getAlpha(), @@ -218,22 +107,50 @@ function linkModel(uniqueKeys, d, l) { }; } -function nodeModel(uniqueKeys, d, n) { +function linkPath() { + 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) { 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 key = basicKey + '__' + i; // 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, @@ -260,8 +177,9 @@ function nodeModel(uniqueKeys, d, n) { valueFormat: d.valueFormat, valueSuffix: d.valueSuffix, sankey: d.sankey, + graph: d.sankey(), arrangement: d.arrangement, - uniqueNodeLabelPathId: [d.guid, d.key, key].join(' '), + uniqueNodeLabelPathId: [d.guid, d.key, key].join('_'), interactionState: d.interactionState }; } @@ -271,33 +189,26 @@ 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.node.x0.toFixed(3) + ', ' + (d.node.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('width', function(d) {return d.node.x1 - d.node.x0;}) + .attr('height', function(d) {return d.visibleHeight;}); } -function salientEnough(d) {return d.link.dy > 1 || d.linkLineWidth > 0;} +function salientEnough(d) {return (d.link.width > 1 || d.linkLineWidth > 0);} function sankeyTransform(d) { var offset = 'translate(' + d.translateX + ',' + d.translateY + ')'; @@ -319,7 +230,7 @@ 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 +// // event handling function attachPointerEvents(selection, sankey, eventSet) { selection @@ -354,25 +265,29 @@ function attachPointerEvents(selection, sankey, eventSet) { } function attachDragHandler(sankeyNode, sankeyLink, callbacks) { - var dragBehavior = d3.behavior.drag() - - .origin(function(d) {return d.node;}) + .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 + '|' + Math.floor(d.node.originalX); + var forceKey = d.traceId + '|' + d.key; if(d.forceLayouts[forceKey]) { d.forceLayouts[forceKey].alpha(1); - } else { // make a forceLayout iff needed + } else { // make a forceLayout if needed attachForce(sankeyNode, forceKey, d); } startForce(sankeyNode, sankeyLink, d, forceKey); @@ -384,17 +299,24 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) { var x = d3.event.x; var y = d3.event.y; if(d.arrangement === 'snap') { - d.node.x = x; - d.node.y = y; + 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.x = x; + 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.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.relayout(); + d.sankey.update(d.graph); updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink); } }) @@ -409,7 +331,9 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) { } function attachForce(sankeyNode, forceKey, d) { - var nodes = d.sankey.nodes().filter(function(n) {return n.originalX === d.node.originalX;}); + // 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() @@ -422,11 +346,17 @@ function attachForce(sankeyNode, forceKey, d) { function startForce(sankeyNode, sankeyLink, d, forceKey) { window.requestAnimationFrame(function faster() { - for(var i = 0; i < c.forceTicksPerFrame; i++) { + var i; + for(i = 0; i < c.forceTicksPerFrame; i++) { d.forceLayouts[forceKey].tick(); } - d.sankey.relayout(); + + 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); } @@ -453,13 +383,61 @@ function snappingForce(sankeyNode, forceKey, nodes, d) { }; } +// 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, 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(); @@ -487,18 +465,17 @@ module.exports = function(svg, styledData, 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); + .map(linkModel.bind(null, d)); + }, keyFun); - sankeyLink.enter() - .append('path') - .classed(c.cn.sankeyLink, true) - .attr('d', linkPath) - .call(attachPointerEvents, sankey, callbacks.linkEvents); + sankeyLink + .enter().append('path') + .classed(c.cn.sankeyLink, true) + .attr('d', linkPath()) + .call(attachPointerEvents, sankey, callbacks.linkEvents); sankeyLink .style('stroke', function(d) { @@ -512,8 +489,8 @@ module.exports = function(svg, styledData, 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) @@ -538,12 +515,11 @@ module.exports = function(svg, styledData, layout, callbacks) { var sankeyNode = sankeyNodeSet.selectAll('.' + c.cn.sankeyNode) .data(function(d) { - var nodes = d.sankey.nodes(); - var uniqueKeys = {}; + var nodes = d.sankey().nodes; persistOriginalPlace(nodes); return nodes .filter(function(n) {return n.value;}) - .map(nodeModel.bind(null, uniqueKeys, d)); + .map(nodeModel.bind(null, d)); }, keyFun); sankeyNode.enter() diff --git a/test/image/baselines/sankey_large_padding.png b/test/image/baselines/sankey_large_padding.png index 38d263f799d..b27b24c12eb 100644 Binary files a/test/image/baselines/sankey_large_padding.png and b/test/image/baselines/sankey_large_padding.png differ 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..41ee87abf9f --- /dev/null +++ b/test/jasmine/tests/sankey_d3_sankey_test.js @@ -0,0 +1,107 @@ +// var Plotly = require('@lib/index'); +// var attributes = require('@src/traces/sankey/attributes'); + +var d3sankey = require('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 + .sankey() + .nodeWidth(36) + .nodePadding(10) + .nodes(graph.nodes) + .links(graph.links) + .size([width, height]) + .iterations(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.width); + }); + 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'); + }); +});