diff --git a/src/traces/sankey/attributes.js b/src/traces/sankey/attributes.js
index b9144ad0e4c..9e0929100c7 100644
--- a/src/traces/sankey/attributes.js
+++ b/src/traces/sankey/attributes.js
@@ -89,6 +89,19 @@ var attrs = module.exports = overrideAll({
role: 'info',
description: 'The shown name of the node.'
},
+ groups: {
+ valType: 'info_array',
+ dimensions: 2,
+ freeLength: true,
+ dflt: [],
+ items: {valType: 'number', editType: 'calc'},
+ role: 'info',
+ description: [
+ 'Groups of nodes.',
+ 'Each group is defined by an array with the indices of the nodes it contains.',
+ 'Multiple groups can be specified.'
+ ].join(' ')
+ },
color: {
valType: 'color',
role: 'style',
diff --git a/src/traces/sankey/calc.js b/src/traces/sankey/calc.js
index 861ce4f0397..b46715a155e 100644
--- a/src/traces/sankey/calc.js
+++ b/src/traces/sankey/calc.js
@@ -34,7 +34,36 @@ function convertToD3Sankey(trace) {
components[cscale.label] = scale;
}
- var nodeCount = nodeSpec.label.length;
+ var maxNodeId = 0;
+ for(i = 0; i < linkSpec.value.length; i++) {
+ if(linkSpec.source[i] > maxNodeId) maxNodeId = linkSpec.source[i];
+ if(linkSpec.target[i] > maxNodeId) maxNodeId = linkSpec.target[i];
+ }
+ var nodeCount = maxNodeId + 1;
+
+ // Group nodes
+ var j;
+ var groups = trace.node.groups;
+ var groupLookup = {};
+ for(i = 0; i < groups.length; i++) {
+ var group = groups[i];
+ // Build a lookup table to quickly find in which group a node is
+ for(j = 0; j < group.length; j++) {
+ var nodeIndex = group[j];
+ var groupIndex = nodeCount + i;
+ if(groupLookup.hasOwnProperty(nodeIndex)) {
+ Lib.warn('Node ' + nodeIndex + ' is already part of a group.');
+ } else {
+ groupLookup[nodeIndex] = groupIndex;
+ }
+ }
+ }
+
+ // Process links
+ var groupedLinks = {
+ source: [],
+ target: []
+ };
for(i = 0; i < linkSpec.value.length; i++) {
var val = linkSpec.value[i];
// remove negative values, but keep zeros with special treatment
@@ -44,6 +73,21 @@ function convertToD3Sankey(trace) {
continue;
}
+ // Remove links that are within the same group
+ if(groupLookup.hasOwnProperty(source) && groupLookup.hasOwnProperty(target) && groupLookup[source] === groupLookup[target]) {
+ continue;
+ }
+
+ // if link targets a node in the group, relink target to that group
+ if(groupLookup.hasOwnProperty(target)) {
+ target = groupLookup[target];
+ }
+
+ // if link originates from a node in a group, relink source to that group
+ if(groupLookup.hasOwnProperty(source)) {
+ source = groupLookup[source];
+ }
+
source = +source;
target = +target;
linkedNodes[source] = linkedNodes[target] = true;
@@ -63,42 +107,46 @@ function convertToD3Sankey(trace) {
target: target,
value: +val
});
+
+ groupedLinks.source.push(source);
+ groupedLinks.target.push(target);
}
+ // Process nodes
+ var totalCount = nodeCount + groups.length;
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;
+ for(i = 0; i < totalCount; i++) {
+ if(!linkedNodes[i]) continue;
+ var l = nodeSpec.label[i];
+
+ nodes.push({
+ group: (i > nodeCount - 1),
+ childrenNodes: [],
+ pointNumber: i,
+ label: l,
+ color: hasNodeColorArray ? nodeSpec.color[i] : nodeSpec.color
+ });
}
- // 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];
- }
+ // Check if we have circularity on the resulting graph
+ var circular = false;
+ if(circularityPresent(totalCount, groupedLinks.source, groupedLinks.target)) {
+ circular = true;
}
return {
+ circular: circular,
links: links,
- nodes: nodes
+ nodes: nodes,
+
+ // Data structure for groups
+ groups: groups,
+ groupLookup: groupLookup
};
}
-function circularityPresent(nodeList, sources, targets) {
-
- var nodeLen = nodeList.length;
+function circularityPresent(nodeLen, sources, targets) {
var nodes = Lib.init2dArray(nodeLen, 0);
for(var i = 0; i < Math.min(sources.length, targets.length); i++) {
@@ -120,16 +168,15 @@ function circularityPresent(nodeList, sources, targets) {
}
module.exports = function calc(gd, trace) {
- var circular = false;
- if(circularityPresent(trace.node.label, trace.link.source, trace.link.target)) {
- circular = true;
- }
-
var result = convertToD3Sankey(trace);
return wrap({
- circular: circular,
+ circular: result.circular,
_nodes: result.nodes,
- _links: result.links
+ _links: result.links,
+
+ // Data structure for grouping
+ _groups: result.groups,
+ _groupLookup: result.groupLookup,
});
};
diff --git a/src/traces/sankey/constants.js b/src/traces/sankey/constants.js
index 2815475a341..534b05ad4e7 100644
--- a/src/traces/sankey/constants.js
+++ b/src/traces/sankey/constants.js
@@ -16,7 +16,7 @@ module.exports = {
forceIterations: 5,
forceTicksPerFrame: 10,
duration: 500,
- ease: 'cubic-in-out',
+ ease: 'linear',
cn: {
sankey: 'sankey',
sankeyLinks: 'sankey-links',
diff --git a/src/traces/sankey/defaults.js b/src/traces/sankey/defaults.js
index c5283910ade..531dfffcf50 100644
--- a/src/traces/sankey/defaults.js
+++ b/src/traces/sankey/defaults.js
@@ -32,6 +32,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
return Lib.coerce(nodeIn, nodeOut, attributes.node, attr, dflt);
}
coerceNode('label');
+ coerceNode('groups');
coerceNode('pad');
coerceNode('thickness');
coerceNode('line.color');
diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js
index 88b27dbdafc..7c2ca3489ce 100644
--- a/src/traces/sankey/render.js
+++ b/src/traces/sankey/render.js
@@ -45,10 +45,7 @@ function sankeyModel(layout, d, traceIndex) {
if(circular) {
sankey = d3SankeyCircular
.sankeyCircular()
- .circularLinkGap(0)
- .nodeId(function(d) {
- return d.pointNumber;
- });
+ .circularLinkGap(0);
} else {
sankey = d3Sankey.sankey();
}
@@ -58,6 +55,9 @@ function sankeyModel(layout, d, traceIndex) {
.size(horizontal ? [width, height] : [height, width])
.nodeWidth(nodeThickness)
.nodePadding(nodePad)
+ .nodeId(function(d) {
+ return d.pointNumber;
+ })
.nodes(nodes)
.links(links);
@@ -67,6 +67,36 @@ function sankeyModel(layout, d, traceIndex) {
Lib.warn('node.pad was reduced to ', sankey.nodePadding(), ' to fit within the figure.');
}
+ // Create transient nodes for animations
+ for(var nodePointNumber in calcData._groupLookup) {
+ var groupIndex = parseInt(calcData._groupLookup[nodePointNumber]);
+
+ // Find node representing groupIndex
+ var groupingNode;
+ for(var i = 0; i < graph.nodes.length; i++) {
+ if(graph.nodes[i].pointNumber === groupIndex) {
+ groupingNode = graph.nodes[i];
+ break;
+ }
+ }
+ // If groupinNode is undefined, no links are targeting this group
+ if(!groupingNode) continue;
+
+ var child = {
+ pointNumber: parseInt(nodePointNumber),
+ x0: groupingNode.x0,
+ x1: groupingNode.x1,
+ y0: groupingNode.y0,
+ y1: groupingNode.y1,
+ partOfGroup: true,
+ sourceLinks: [],
+ targetLinks: []
+ };
+
+ graph.nodes.unshift(child);
+ groupingNode.childrenNodes.unshift(child);
+ }
+
function computeLinkConcentrations() {
var i, j, k;
for(i = 0; i < graph.nodes.length; i++) {
@@ -137,7 +167,7 @@ function sankeyModel(layout, d, traceIndex) {
circular: circular,
key: traceIndex,
trace: trace,
- guid: Math.floor(1e12 * (1 + Math.random())),
+ guid: Lib.randstr(),
horizontal: horizontal,
width: width,
height: height,
@@ -184,6 +214,7 @@ function linkModel(d, l, i) {
link: l,
tinyColorHue: Color.tinyRGB(tc),
tinyColorAlpha: tc.getAlpha(),
+ linkPath: linkPath,
linkLineColor: d.linkLineColor,
linkLineWidth: d.linkLineWidth,
valueFormat: d.valueFormat,
@@ -343,7 +374,7 @@ function linkPath() {
return path;
}
-function nodeModel(d, n, i) {
+function nodeModel(d, n) {
var tc = tinycolor(n.color);
var zoneThicknessPad = c.nodePadAcross;
var zoneLengthPad = d.nodePad / 2;
@@ -352,8 +383,11 @@ function nodeModel(d, n, i) {
var visibleThickness = n.dx;
var visibleLength = Math.max(0.5, n.dy);
- var basicKey = n.label;
- var key = basicKey + '__' + i;
+ var key = 'node_' + n.pointNumber;
+ // If it's a group, it's mutable and should be unique
+ if(n.group) {
+ key = Lib.randstr();
+ }
// for event data
n.trace = d.trace;
@@ -362,6 +396,8 @@ function nodeModel(d, n, i) {
return {
index: n.pointNumber,
key: key,
+ partOfGroup: n.partOfGroup || false,
+ group: n.group,
traceId: d.key,
node: n,
nodePad: d.nodePad,
@@ -445,19 +481,19 @@ function attachPointerEvents(selection, sankey, eventSet) {
selection
.on('.basic', null) // remove any preexisting handlers
.on('mouseover.basic', function(d) {
- if(!d.interactionState.dragInProgress) {
+ if(!d.interactionState.dragInProgress && !d.partOfGroup) {
eventSet.hover(this, d, sankey);
d.interactionState.hovered = [this, d];
}
})
.on('mousemove.basic', function(d) {
- if(!d.interactionState.dragInProgress) {
+ if(!d.interactionState.dragInProgress && !d.partOfGroup) {
eventSet.follow(this, d);
d.interactionState.hovered = [this, d];
}
})
.on('mouseout.basic', function(d) {
- if(!d.interactionState.dragInProgress) {
+ if(!d.interactionState.dragInProgress && !d.partOfGroup) {
eventSet.unhover(this, d, sankey);
d.interactionState.hovered = false;
}
@@ -467,7 +503,7 @@ function attachPointerEvents(selection, sankey, eventSet) {
eventSet.unhover(this, d, sankey);
d.interactionState.hovered = false;
}
- if(!d.interactionState.dragInProgress) {
+ if(!d.interactionState.dragInProgress && !d.partOfGroup) {
eventSet.select(this, d, sankey);
}
});
@@ -530,6 +566,10 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) {
.on('dragend', function(d) {
d.interactionState.dragInProgress = false;
+ for(var i = 0; i < d.node.childrenNodes.length; i++) {
+ d.node.childrenNodes[i].x = d.node.x;
+ d.node.childrenNodes[i].y = d.node.y;
+ }
});
sankeyNode
@@ -540,7 +580,10 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks) {
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;});
+ var nodes = d.graph.nodes
+ .filter(function(n) {return n.originalX === d.node.originalX;})
+ // Filter out children
+ .filter(function(n) {return !n.partOfGroup;});
d.forceLayouts[forceKey] = d3Force.forceSimulation(nodes)
.alphaDecay(0)
.force('collide', d3Force.forceCollide()
@@ -639,6 +682,11 @@ function switchToSankeyFormat(nodes) {
// scene graph
module.exports = function(gd, svg, calcData, layout, callbacks) {
+ // To prevent animation on first render
+ var firstRender = false;
+ Lib.ensureSingle(gd._fullLayout._infolayer, 'g', 'first-render', function() {
+ firstRender = true;
+ });
var styledData = calcData
.filter(function(d) {return unwrap(d).trace.visible;})
@@ -683,7 +731,6 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
sankeyLink
.enter().append('path')
.classed(c.cn.sankeyLink, true)
- .attr('d', linkPath())
.call(attachPointerEvents, sankey, callbacks.linkEvents);
sankeyLink
@@ -701,13 +748,17 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
})
.style('stroke-width', function(d) {
return salientEnough(d) ? d.linkLineWidth : 1;
- });
+ })
+ .attr('d', linkPath());
- sankeyLink.transition()
- .ease(c.ease).duration(c.duration)
- .attr('d', linkPath());
+ sankeyLink
+ .style('opacity', function() { return (gd._context.staticPlot || firstRender) ? 1 : 0;})
+ .transition()
+ .ease(c.ease).duration(c.duration)
+ .style('opacity', 1);
- sankeyLink.exit().transition()
+ sankeyLink.exit()
+ .transition()
.ease(c.ease).duration(c.duration)
.style('opacity', 0)
.remove();
@@ -733,24 +784,26 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
var nodes = d.graph.nodes;
persistOriginalPlace(nodes);
return nodes
- .filter(function(n) {return n.value;})
- .map(nodeModel.bind(null, d));
+ .map(nodeModel.bind(null, d));
}, keyFun);
sankeyNode.enter()
.append('g')
.classed(c.cn.sankeyNode, true)
.call(updateNodePositions)
- .call(attachPointerEvents, sankey, callbacks.nodeEvents);
+ .style('opacity', function(n) { return ((gd._context.staticPlot || firstRender) && !n.partOfGroup) ? 1 : 0;});
sankeyNode
+ .call(attachPointerEvents, sankey, callbacks.nodeEvents)
.call(attachDragHandler, sankeyLink, callbacks); // has to be here as it binds sankeyLink
sankeyNode.transition()
.ease(c.ease).duration(c.duration)
- .call(updateNodePositions);
+ .call(updateNodePositions)
+ .style('opacity', function(n) { return n.partOfGroup ? 0 : 1;});
- sankeyNode.exit().transition()
+ sankeyNode.exit()
+ .transition()
.ease(c.ease).duration(c.duration)
.style('opacity', 0)
.remove();
diff --git a/test/image/baselines/sankey_groups.png b/test/image/baselines/sankey_groups.png
new file mode 100644
index 00000000000..ee05b7d512e
Binary files /dev/null and b/test/image/baselines/sankey_groups.png differ
diff --git a/test/image/mocks/sankey_groups.json b/test/image/mocks/sankey_groups.json
new file mode 100644
index 00000000000..90afbde12bb
--- /dev/null
+++ b/test/image/mocks/sankey_groups.json
@@ -0,0 +1,144 @@
+{
+ "data": [
+ {
+ "type": "sankey",
+ "node": {
+ "pad": 25,
+ "line": {
+ "color": "white",
+ "width": 2
+ },
+ "color": ["black", "black", "black", "black", "black", "orange", "orange" ],
+ "label": ["process0", "process1", "process2", "process3", "process4", "Group A", "Group B"],
+ "groups": [[2, 3, 4]]
+ },
+ "link": {
+ "source": [
+ 0, 0, 0, 0,
+ 1, 1, 1, 1,
+ 1, 1, 1, 1,
+ 1, 1,
+ 2
+ ],
+ "target": [
+ 1, 1, 1, 1,
+ 2, 2, 2, 2,
+ 3, 3, 3, 3,
+ 4, 4,
+ 0
+ ],
+ "value": [
+ 10, 20, 40, 30,
+ 10, 5, 10, 20,
+ 0, 10, 10, 10,
+ 15, 5,
+ 20
+
+ ],
+ "label": [
+ "elementA", "elementB", "elementC", "elementD",
+ "elementA", "elementB", "elementC", "elementD",
+ "elementA", "elementB", "elementC", "elementD",
+ "elementC", "elementC",
+ "elementA"
+ ],
+ "line": {
+ "color": "white",
+ "width": 2
+ },
+ "colorscales": [
+ {
+ "label": "elementA",
+ "colorscale": [[0, "white"], [1, "blue"]]
+ },
+ {
+ "label": "elementB",
+ "colorscale": [[0, "white"], [1, "red"]]
+ },
+ {
+ "label": "elementC",
+ "colorscale": [[0, "white"], [1, "green"]]
+ },
+ {
+ "label": "elementD"
+ }
+ ],
+
+ "hovertemplate": "%{label}
flow.labelConcentration: %{flow.labelConcentration:%0.2f}
flow.concentration: %{flow.concentration:%0.2f}
flow.value: %{flow.value}"
+ }
+
+ }],
+ "layout": {
+ "title": "Sankey diagram with links colored based on their concentration within a flow",
+ "width": 800,
+ "height": 800,
+ "updatemenus": [{
+ "y": 1,
+ "x": 0,
+ "active": 1,
+ "buttons": [{
+ "label": "Ungroup [[]]",
+ "method": "restyle",
+ "args": ["node.groups", [
+ []
+ ]]
+ },
+ {
+ "label": "Group [[2, 3, 4]]",
+ "method": "restyle",
+ "args": ["node.groups", [
+ [
+ [2, 3, 4]
+ ]
+ ]]
+ },
+ {
+ "label": "Group [[3, 4]]",
+ "method": "restyle",
+ "args": ["node.groups", [
+ [
+ [3, 4]
+ ]
+ ]]
+ },
+ {
+ "label": "Group [[1, 2]]",
+ "method": "restyle",
+ "args": ["node.groups", [
+ [
+ [1, 2]
+ ]
+ ]]
+ },
+ {
+ "label": "Group [[2, 4]]]",
+ "method": "restyle",
+ "args": ["node.groups", [
+ [
+ [2, 4]
+ ]
+ ]]
+ },
+ {
+ "label": "Group [[0, 2]]]",
+ "method": "restyle",
+ "args": ["node.groups", [
+ [
+ [0, 2]
+ ]
+ ]]
+ },
+ {
+ "label": "Group [[1, 2], [3, 4]]",
+ "method": "restyle",
+ "args": ["node.groups", [
+ [
+ [1, 2],
+ [3, 4]
+ ]
+ ]]
+ }
+ ]
+ }]
+ }
+}
diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js
index 6471906d6e1..69d985a5041 100644
--- a/test/jasmine/tests/sankey_test.js
+++ b/test/jasmine/tests/sankey_test.js
@@ -263,7 +263,7 @@ describe('sankey tests', function() {
label: ['a', 'b', 'c', 'd', 'e']
},
link: {
- value: [1, 1, 1, 1, 1, 1, 1, 1],
+ value: [1, 1, 1, 1],
source: [0, 1, 2, 3],
target: [1, 2, 0, 4]
}
@@ -277,21 +277,67 @@ describe('sankey tests', function() {
label: ['a', 'b', 'c', 'd', 'e']
},
link: {
- value: [1, 1, 1, 1, 1, 1, 1, 1],
+ value: [1, 1, 1, 1],
source: [0, 1, 2, 3],
target: [1, 2, 4, 4]
}
}));
expect(calcData[0].circular).toBe(false);
});
+
+ it('keep an index of groups', function() {
+ var calcData = _calc(Lib.extendDeep({}, base, {
+ node: {
+ label: ['a', 'b', 'c', 'd', 'e'],
+ groups: [[0, 1], [2, 3]]
+ },
+ link: {
+ value: [1, 1, 1, 1],
+ source: [0, 1, 2, 3],
+ target: [1, 2, 4, 4]
+ }
+ }));
+ var groups = calcData[0]._nodes.filter(function(node) {
+ return node.group;
+ });
+ expect(groups.length).toBe(2);
+ expect(calcData[0].circular).toBe(false);
+ });
+
+ it('emits a warning if a node is part of more than one group', function() {
+ var warnings = [];
+ spyOn(Lib, 'warn').and.callFake(function(msg) {
+ warnings.push(msg);
+ });
+
+ var calcData = _calc(Lib.extendDeep({}, base, {
+ node: {
+ label: ['a', 'b', 'c', 'd', 'e'],
+ groups: [[0, 1], [1, 2, 3]]
+ },
+ link: {
+ value: [1, 1, 1, 1],
+ source: [0, 1, 2, 3],
+ target: [1, 2, 4, 4]
+ }
+ }));
+
+ expect(warnings.length).toBe(1);
+
+ // Expect node '1' to be in the first group
+ expect(calcData[0]._groupLookup[1]).toBe(5);
+ });
});
describe('lifecycle methods', function() {
+ var gd;
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
afterEach(destroyGraphDiv);
it('Plotly.deleteTraces with two traces removes the deleted plot', function(done) {
- var gd = createGraphDiv();
var mockCopy = Lib.extendDeep({}, mock);
var mockCopy2 = Lib.extendDeep({}, mockDark);
@@ -320,7 +366,6 @@ describe('sankey tests', function() {
it('Plotly.plot does not show Sankey if \'visible\' is false', function(done) {
- var gd = createGraphDiv();
var mockCopy = Lib.extendDeep({}, mock);
Plotly.plot(gd, mockCopy)
@@ -343,7 +388,6 @@ describe('sankey tests', function() {
it('\'node\' remains visible even if \'value\' is very low', function(done) {
- var gd = createGraphDiv();
var minimock = [{
type: 'sankey',
node: {
@@ -365,7 +409,6 @@ describe('sankey tests', function() {
});
it('switch from normal to circular Sankey on react', function(done) {
- var gd = createGraphDiv();
var mockCopy = Lib.extendDeep({}, mock);
var mockCircularCopy = Lib.extendDeep({}, mockCircular);
@@ -381,7 +424,6 @@ describe('sankey tests', function() {
});
it('switch from circular to normal Sankey on react', function(done) {
- var gd = createGraphDiv();
var mockCircularCopy = Lib.extendDeep({}, mockCircular);
Plotly.plot(gd, mockCircularCopy)
@@ -404,6 +446,67 @@ describe('sankey tests', function() {
done();
});
});
+
+ it('can create groups, restyle groups and properly update DOM', function(done) {
+ var mockCircularCopy = Lib.extendDeep({}, mockCircular);
+ var firstGroup = [[2, 3], [0, 1]];
+ var newGroup = [[2, 3]];
+ mockCircularCopy.data[0].node.groups = firstGroup;
+
+ Plotly.plot(gd, mockCircularCopy)
+ .then(function() {
+ expect(gd._fullData[0].node.groups).toEqual(firstGroup);
+ return Plotly.restyle(gd, {'node.groups': [newGroup]});
+ })
+ .then(function() {
+ expect(gd._fullData[0].node.groups).toEqual(newGroup);
+
+ // Check that all links have updated their links
+ d3.selectAll('.sankey .sankey-link').each(function(d, i) {
+ var path = this.getAttribute('d');
+ expect(path).toBe(d.linkPath()(d), 'link ' + i + ' has wrong `d` attribute');
+ });
+
+ // Check that ghost nodes used for animations:
+ // 1) are drawn first so they apear behind
+ var seeRealNode = false;
+ var sankeyNodes = d3.selectAll('.sankey .sankey-node');
+ sankeyNodes.each(function(d, i) {
+ if(d.partOfGroup) {
+ if(seeRealNode) fail('node ' + i + ' is a ghost node and should be behind');
+ } else {
+ seeRealNode = true;
+ }
+ });
+ // 2) have an element for each grouped node
+ var L = sankeyNodes.filter(function(d) { return d.partOfGroup;}).size();
+ expect(L).toBe(newGroup.flat().length, 'does not have the right number of ghost nodes');
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('switches from normal to circular Sankey on grouping', function(done) {
+ var mockCopy = Lib.extendDeep({}, mock);
+
+ Plotly.plot(gd, mockCopy)
+ .then(function() {
+ expect(gd.calcdata[0][0].circular).toBe(false);
+
+ // Group two nodes that creates a circularity
+ return Plotly.restyle(gd, 'node.groups', [[[1, 3]]]);
+ })
+ .then(function() {
+ expect(gd.calcdata[0][0].circular).toBe(true);
+ // Group two nodes that do not create a circularity
+ return Plotly.restyle(gd, 'node.groups', [[[1, 4]]]);
+ })
+ .then(function() {
+ expect(gd.calcdata[0][0].circular).toBe(false);
+ done();
+ });
+ });
+
});
describe('Test hover/click interactions:', function() {