diff --git a/src/traces/sankey/attributes.js b/src/traces/sankey/attributes.js
index 4ca7d632c25..dfbd8845822 100644
--- a/src/traces/sankey/attributes.js
+++ b/src/traces/sankey/attributes.js
@@ -9,20 +9,13 @@
'use strict';
var fontAttrs = require('../../plots/font_attributes');
-var plotAttrs = require('../../plots/attributes');
var colorAttrs = require('../../components/color/attributes');
var fxAttrs = require('../../components/fx/attributes');
var domainAttrs = require('../../plots/domain').attributes;
-var extendFlat = require('../../lib/extend').extendFlat;
var overrideAll = require('../../plot_api/edit_types').overrideAll;
-module.exports = overrideAll({
- hoverinfo: extendFlat({}, plotAttrs.hoverinfo, {
- flags: ['label', 'text', 'value', 'percent', 'name'],
- }),
- hoverlabel: fxAttrs.hoverlabel, // needs editType override
-
+var attrs = module.exports = overrideAll({
domain: domainAttrs({name: 'sankey', trace: true}),
orientation: {
@@ -127,6 +120,18 @@ module.exports = overrideAll({
role: 'style',
description: 'Sets the thickness (in px) of the `nodes`.'
},
+ hoverinfo: {
+ valType: 'enumerated',
+ values: ['all', 'none', 'skip'],
+ dflt: 'all',
+ role: 'info',
+ description: [
+ 'Determines which trace information appear when hovering nodes.',
+ 'If `none` or `skip` are set, no information is displayed upon hovering.',
+ 'But, if `none` is set, click and hover events are still fired.'
+ ].join(' ')
+ },
+ hoverlabel: fxAttrs.hoverlabel, // needs editType override,
description: 'The nodes of the Sankey plot.'
},
@@ -185,6 +190,21 @@ module.exports = overrideAll({
role: 'info',
description: 'A numeric value representing the flow volume value.'
},
+ hoverinfo: {
+ valType: 'enumerated',
+ values: ['all', 'none', 'skip'],
+ dflt: 'all',
+ role: 'info',
+ description: [
+ 'Determines which trace information appear when hovering links.',
+ 'If `none` or `skip` are set, no information is displayed upon hovering.',
+ 'But, if `none` is set, click and hover events are still fired.'
+ ].join(' ')
+ },
+ hoverlabel: fxAttrs.hoverlabel, // needs editType override,
description: 'The links of the Sankey plot.'
}
}, 'calc', 'nested');
+// hide unsupported top-level properties from plot-schema
+attrs.hoverinfo = undefined;
+attrs.hoverlabel = undefined;
diff --git a/src/traces/sankey/defaults.js b/src/traces/sankey/defaults.js
index 8fb3c785a51..a755fc34e75 100644
--- a/src/traces/sankey/defaults.js
+++ b/src/traces/sankey/defaults.js
@@ -13,37 +13,55 @@ var attributes = require('./attributes');
var Color = require('../../components/color');
var tinycolor = require('tinycolor2');
var handleDomainDefaults = require('../../plots/domain').defaults;
+var handleHoverLabelDefaults = require('../../components/fx/hoverlabel_defaults');
+var Template = require('../../plot_api/plot_template');
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}
- coerce('node.label');
- coerce('node.pad');
- coerce('node.thickness');
- coerce('node.line.color');
- coerce('node.line.width');
+ // node attributes
+ var nodeIn = traceIn.node, nodeOut = Template.newContainer(traceOut, 'node');
+ function coerceNode(attr, dflt) {
+ return Lib.coerce(nodeIn, nodeOut, attributes.node, attr, dflt);
+ }
+ coerceNode('label');
+ coerceNode('pad');
+ coerceNode('thickness');
+ coerceNode('line.color');
+ coerceNode('line.width');
+ coerceNode('hoverinfo');
+ handleHoverLabelDefaults(nodeIn, nodeOut, coerceNode, layout.hoverlabel);
var colors = layout.colorway;
var defaultNodePalette = function(i) {return colors[i % colors.length];};
- coerce('node.color', traceOut.node.label.map(function(d, i) {
+ coerceNode('color', nodeOut.label.map(function(d, i) {
return Color.addOpacity(defaultNodePalette(i), 0.8);
}));
- coerce('link.label');
- coerce('link.source');
- coerce('link.target');
- coerce('link.value');
- coerce('link.line.color');
- coerce('link.line.width');
-
- coerce('link.color', traceOut.link.value.map(function() {
- return tinycolor(layout.paper_bgcolor).getLuminance() < 0.333 ?
- 'rgba(255, 255, 255, 0.6)' :
- 'rgba(0, 0, 0, 0.2)';
+ // link attributes
+ var linkIn = traceIn.link, linkOut = Template.newContainer(traceOut, 'link');
+ function coerceLink(attr, dflt) {
+ return Lib.coerce(linkIn, linkOut, attributes.link, attr, dflt);
+ }
+ coerceLink('label');
+ coerceLink('source');
+ coerceLink('target');
+ coerceLink('value');
+ coerceLink('line.color');
+ coerceLink('line.width');
+ coerceLink('hoverinfo');
+ handleHoverLabelDefaults(linkIn, linkOut, coerceLink, layout.hoverlabel);
+
+ var defaultLinkColor = tinycolor(layout.paper_bgcolor).getLuminance() < 0.333 ?
+ 'rgba(255, 255, 255, 0.6)' :
+ 'rgba(0, 0, 0, 0.2)';
+
+ coerceLink('color', linkOut.value.map(function() {
+ return defaultLinkColor;
}));
handleDomainDefaults(traceOut, layout, coerce);
diff --git a/src/traces/sankey/plot.js b/src/traces/sankey/plot.js
index 150cff65b9f..cb6c7300c74 100644
--- a/src/traces/sankey/plot.js
+++ b/src/traces/sankey/plot.js
@@ -132,10 +132,13 @@ module.exports = function plot(gd, calcData) {
var linkHover = function(element, d, sankey) {
if(gd._fullLayout.hovermode === false) return;
d3.select(element).call(linkHoveredStyle.bind(0, d, sankey, true));
- gd.emit('plotly_hover', {
- event: d3.event,
- points: [d.link]
- });
+ if(d.link.trace.link.hoverinfo !== 'skip') {
+ gd.emit('plotly_hover', {
+ event: d3.event,
+ points: [d.link]
+ });
+ }
+
};
var sourceLabel = _(gd, 'source:') + ' ';
@@ -145,7 +148,8 @@ module.exports = function plot(gd, calcData) {
var linkHoverFollow = function(element, d) {
if(gd._fullLayout.hovermode === false) return;
- var trace = d.link.trace;
+ var obj = d.link.trace.link;
+ if(obj.hoverinfo === 'none' || obj.hoverinfo === 'skip') return;
var rootBBox = gd._fullLayout._paperdiv.node().getBoundingClientRect();
var boundingBox = element.getBoundingClientRect();
var hoverCenterX = boundingBox.left + boundingBox.width / 2;
@@ -160,11 +164,11 @@ module.exports = function plot(gd, calcData) {
sourceLabel + d.link.source.label,
targetLabel + d.link.target.label
].filter(renderableValuePresent).join('
'),
- color: castHoverOption(trace, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1),
- borderColor: castHoverOption(trace, 'bordercolor'),
- fontFamily: castHoverOption(trace, 'font.family'),
- fontSize: castHoverOption(trace, 'font.size'),
- fontColor: castHoverOption(trace, 'font.color'),
+ color: castHoverOption(obj, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1),
+ borderColor: castHoverOption(obj, 'bordercolor'),
+ fontFamily: castHoverOption(obj, 'font.family'),
+ fontSize: castHoverOption(obj, 'font.size'),
+ fontColor: castHoverOption(obj, 'font.color'),
idealAlign: d3.event.x < hoverCenterX ? 'right' : 'left'
}, {
container: fullLayout._hoverlayer.node(),
@@ -179,10 +183,12 @@ module.exports = function plot(gd, calcData) {
var linkUnhover = function(element, d, sankey) {
if(gd._fullLayout.hovermode === false) return;
d3.select(element).call(linkNonHoveredStyle.bind(0, d, sankey, true));
- gd.emit('plotly_unhover', {
- event: d3.event,
- points: [d.link]
- });
+ if(d.link.trace.link.hoverinfo !== 'skip') {
+ gd.emit('plotly_unhover', {
+ event: d3.event,
+ points: [d.link]
+ });
+ }
Fx.loneUnhover(fullLayout._hoverlayer.node());
};
@@ -198,15 +204,19 @@ module.exports = function plot(gd, calcData) {
var nodeHover = function(element, d, sankey) {
if(gd._fullLayout.hovermode === false) return;
d3.select(element).call(nodeHoveredStyle, d, sankey);
- gd.emit('plotly_hover', {
- event: d3.event,
- points: [d.node]
- });
+ if(d.node.trace.node.hoverinfo !== 'skip') {
+ gd.emit('plotly_hover', {
+ event: d3.event,
+ points: [d.node]
+ });
+ }
};
var nodeHoverFollow = function(element, d) {
if(gd._fullLayout.hovermode === false) return;
- var trace = d.node.trace;
+
+ var obj = d.node.trace.node;
+ if(obj.hoverinfo === 'none' || obj.hoverinfo === 'skip') return;
var nodeRect = d3.select(element).select('.' + cn.nodeRect);
var rootBBox = gd._fullLayout._paperdiv.node().getBoundingClientRect();
var boundingBox = nodeRect.node().getBoundingClientRect();
@@ -224,11 +234,11 @@ module.exports = function plot(gd, calcData) {
incomingLabel + d.node.targetLinks.length,
outgoingLabel + d.node.sourceLinks.length
].filter(renderableValuePresent).join('
'),
- color: castHoverOption(trace, 'bgcolor') || d.tinyColorHue,
- borderColor: castHoverOption(trace, 'bordercolor'),
- fontFamily: castHoverOption(trace, 'font.family'),
- fontSize: castHoverOption(trace, 'font.size'),
- fontColor: castHoverOption(trace, 'font.color'),
+ color: castHoverOption(obj, 'bgcolor') || d.tinyColorHue,
+ borderColor: castHoverOption(obj, 'bordercolor'),
+ fontFamily: castHoverOption(obj, 'font.family'),
+ fontSize: castHoverOption(obj, 'font.size'),
+ fontColor: castHoverOption(obj, 'font.color'),
idealAlign: 'left'
}, {
container: fullLayout._hoverlayer.node(),
@@ -243,10 +253,12 @@ module.exports = function plot(gd, calcData) {
var nodeUnhover = function(element, d, sankey) {
if(gd._fullLayout.hovermode === false) return;
d3.select(element).call(nodeNonHoveredStyle, d, sankey);
- gd.emit('plotly_unhover', {
- event: d3.event,
- points: [d.node]
- });
+ if(d.node.trace.node.hoverinfo !== 'skip') {
+ gd.emit('plotly_unhover', {
+ event: d3.event,
+ points: [d.node]
+ });
+ }
Fx.loneUnhover(fullLayout._hoverlayer.node());
};
diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js
index f2f26d3a2c1..eb25d602b27 100644
--- a/test/jasmine/tests/sankey_test.js
+++ b/test/jasmine/tests/sankey_test.js
@@ -394,6 +394,9 @@ describe('sankey tests', function() {
Lib.clearThrottle();
}
+ var node = [404, 302],
+ link = [450, 300];
+
it('should show the correct hover labels', function(done) {
var gd = createGraphDiv();
var mockCopy = Lib.extendDeep({}, mock);
@@ -433,10 +436,14 @@ describe('sankey tests', function() {
);
return Plotly.restyle(gd, {
- 'hoverlabel.bgcolor': 'red',
- 'hoverlabel.bordercolor': 'blue',
- 'hoverlabel.font.size': 20,
- 'hoverlabel.font.color': 'black'
+ 'node.hoverlabel.bgcolor': 'red',
+ 'node.hoverlabel.bordercolor': 'blue',
+ 'node.hoverlabel.font.size': 20,
+ 'node.hoverlabel.font.color': 'black',
+ 'link.hoverlabel.bgcolor': 'yellow',
+ 'link.hoverlabel.bordercolor': 'magenta',
+ 'link.hoverlabel.font.size': 18,
+ 'link.hoverlabel.font.color': 'green'
});
})
.then(function() {
@@ -452,14 +459,67 @@ describe('sankey tests', function() {
assertLabel(
['source: Solid', 'target: Industry', '46TWh'],
+ ['rgb(255, 255, 0)', 'rgb(255, 0, 255)', 18, 'Roboto', 'rgb(0, 128, 0)']
+ );
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should show the correct hover labels with the style provided in template', function(done) {
+ var gd = createGraphDiv();
+ var mockCopy = Lib.extendDeep({}, mock);
+ mockCopy.layout.template = {
+ data: {
+ sankey: [{
+ node: {
+ hoverlabel: {
+ bgcolor: 'red',
+ bordercolor: 'blue',
+ font: {
+ size: 20,
+ color: 'black',
+ family: 'Roboto'
+ }
+ }
+ },
+ link: {
+ hoverlabel: {
+ bgcolor: 'yellow',
+ bordercolor: 'magenta',
+ font: {
+ size: 18,
+ color: 'green',
+ family: 'Roboto'
+ }
+ }
+ }
+ }]
+ }
+ };
+
+ Plotly.plot(gd, mockCopy)
+ .then(function() {
+ _hover(404, 302);
+
+ assertLabel(
+ ['Solid', 'incoming flow count: 4', 'outgoing flow count: 3', '447TWh'],
['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 20, 'Roboto', 'rgb(0, 0, 0)']
);
})
+ .then(function() {
+ _hover(450, 300);
+
+ assertLabel(
+ ['source: Solid', 'target: Industry', '46TWh'],
+ ['rgb(255, 255, 0)', 'rgb(255, 0, 255)', 18, 'Roboto', 'rgb(0, 128, 0)']
+ );
+ })
.catch(failTest)
.then(done);
});
- it('should show correct hover labels even if there is no link.label supplied', function(done) {
+ it('should show the correct hover labels even if there is no link.label supplied', function(done) {
var gd = createGraphDiv();
var mockCopy = Lib.extendDeep({}, mock);
delete mockCopy.data[0].link.label;
@@ -477,7 +537,7 @@ describe('sankey tests', function() {
.then(done);
});
- it('should not show labels if hovermode is false', function(done) {
+ it('should not show any labels if hovermode is false', function(done) {
var gd = createGraphDiv();
var mockCopy = Lib.extendDeep({}, mock);
@@ -485,8 +545,71 @@ describe('sankey tests', function() {
return Plotly.relayout(gd, 'hovermode', false);
})
.then(function() {
- _hover(404, 302);
+ _hover(node[0], node[1]);
+ assertNoLabel();
+ })
+ .then(function() {
+ _hover(link[0], link[1]);
+ assertNoLabel();
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should not show node labels if node.hoverinfo is none', function(done) {
+ var gd = createGraphDiv();
+ var mockCopy = Lib.extendDeep({}, mock);
+ Plotly.plot(gd, mockCopy).then(function() {
+ return Plotly.restyle(gd, 'node.hoverinfo', 'none');
+ })
+ .then(function() {
+ _hover(node[0], node[1]);
+ assertNoLabel();
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should not show link labels if link.hoverinfo is none', function(done) {
+ var gd = createGraphDiv();
+ var mockCopy = Lib.extendDeep({}, mock);
+
+ Plotly.plot(gd, mockCopy).then(function() {
+ return Plotly.restyle(gd, 'link.hoverinfo', 'none');
+ })
+ .then(function() {
+ _hover(link[0], link[1]);
+ assertNoLabel();
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should not show node labels if node.hoverinfo is skip', function(done) {
+ var gd = createGraphDiv();
+ var mockCopy = Lib.extendDeep({}, mock);
+
+ Plotly.plot(gd, mockCopy).then(function() {
+ return Plotly.restyle(gd, 'node.hoverinfo', 'skip');
+ })
+ .then(function() {
+ _hover(node[0], node[1]);
+ assertNoLabel();
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should not show link labels if link.hoverinfo is skip', function(done) {
+ var gd = createGraphDiv();
+ var mockCopy = Lib.extendDeep({}, mock);
+
+ Plotly.plot(gd, mockCopy).then(function() {
+ return Plotly.restyle(gd, 'link.hoverinfo', 'skip');
+ })
+ .then(function() {
+ _hover(link[0], link[1]);
assertNoLabel();
})
.catch(failTest)
@@ -574,6 +697,7 @@ describe('sankey tests', function() {
var fig = Lib.extendDeep({}, mock);
Plotly.plot(gd, fig)
+ .then(function() { return Plotly.restyle(gd, 'hoverinfo', 'none'); })
.then(function() { return _hover('node'); })
.then(function(d) {
_assert(d, {
@@ -610,27 +734,40 @@ describe('sankey tests', function() {
.then(done);
});
+ function assertNoHoverEvents(type) {
+ return function() {
+ return Promise.resolve()
+ .then(function() { return _hover(type); })
+ .then(failTest).catch(function(err) {
+ expect(err).toBe('plotly_hover did not get called!');
+ })
+ .then(function() { return _unhover(type); })
+ .then(failTest).catch(function(err) {
+ expect(err).toBe('plotly_unhover did not get called!');
+ });
+ };
+ }
+
it('should not output hover/unhover event data when hovermoder is false', function(done) {
var fig = Lib.extendDeep({}, mock);
Plotly.plot(gd, fig)
.then(function() { return Plotly.relayout(gd, 'hovermode', false); })
- .then(function() { return _hover('node'); })
- .then(failTest).catch(function(err) {
- expect(err).toBe('plotly_hover did not get called!');
- })
- .then(function() { return _unhover('node'); })
- .then(failTest).catch(function(err) {
- expect(err).toBe('plotly_unhover did not get called!');
- })
- .then(function() { return _hover('link'); })
- .then(failTest).catch(function(err) {
- expect(err).toBe('plotly_hover did not get called!');
- })
- .then(function() { return _unhover('link'); })
- .then(failTest).catch(function(err) {
- expect(err).toBe('plotly_unhover did not get called!');
- })
+ .then(assertNoHoverEvents('node'))
+ .then(assertNoHoverEvents('link'))
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('should not output hover/unhover event data when hoverinfo is skip', function(done) {
+ var fig = Lib.extendDeep({}, mock);
+
+ Plotly.plot(gd, fig)
+ .then(function() { return Plotly.restyle(gd, 'link.hoverinfo', 'skip'); })
+ .then(assertNoHoverEvents('link'))
+ .then(function() { return Plotly.restyle(gd, 'node.hoverinfo', 'skip'); })
+ .then(assertNoHoverEvents('node'))
+ .catch(failTest)
.then(done);
});
});