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); }); });