diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 6731db8e483..9a2efdc3b68 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -213,9 +213,10 @@ exports.multiHovers = function multiHovers(hoverItems, opts) { // Fix vertical overlap var tooltipSpacing = 5; var lastBottomY = 0; + var anchor = 0; hoverLabel .sort(function(a, b) {return a.y0 - b.y0;}) - .each(function(d) { + .each(function(d, i) { var topY = d.y0 - d.by / 2; if((topY - tooltipSpacing) < lastBottomY) { @@ -225,12 +226,16 @@ exports.multiHovers = function multiHovers(hoverItems, opts) { } lastBottomY = topY + d.by + d.offset; - }); + if(i === opts.anchorIndex || 0) anchor = d.offset; + }) + .each(function(d) { + d.offset -= anchor; + }); alignHoverText(hoverLabel, fullOpts.rotateLabels); - return hoverLabel.node(); + return hoverLabel; }; // The actual implementation is here: diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 37db6546ee8..ffa98cdd38a 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -86,6 +86,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) { var hasTernary = fullLayout._has('ternary'); var hasMapbox = fullLayout._has('mapbox'); var hasPolar = fullLayout._has('polar'); + var hasSankey = fullLayout._has('sankey'); var allAxesFixed = areAllAxesFixed(fullLayout); var groups = []; @@ -139,6 +140,9 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) { else if(hasPie) { hoverGroup = ['hoverClosestPie']; } + else if(hasSankey) { + hoverGroup = ['hoverClosestCartesian', 'hoverCompareCartesian']; + } else { // hasPolar, hasTernary // always show at least one hover icon. hoverGroup = ['toggleHover']; diff --git a/src/traces/sankey/plot.js b/src/traces/sankey/plot.js index c10e0866851..8f3815b79e9 100644 --- a/src/traces/sankey/plot.js +++ b/src/traces/sankey/plot.js @@ -156,51 +156,69 @@ module.exports = function plot(gd, calcData) { if(gd._fullLayout.hovermode === false) return; var obj = d.link.trace.link; if(obj.hoverinfo === 'none' || obj.hoverinfo === 'skip') return; - var rootBBox = gd._fullLayout._paperdiv.node().getBoundingClientRect(); - var hoverCenterX; - var hoverCenterY; - if(d.link.circular) { - hoverCenterX = (d.link.circularPathData.leftInnerExtent + d.link.circularPathData.rightInnerExtent) / 2 + d.parent.translateX; - hoverCenterY = d.link.circularPathData.verticalFullExtent + d.parent.translateY; - } else { - var boundingBox = element.getBoundingClientRect(); - hoverCenterX = boundingBox.left + boundingBox.width / 2 - rootBBox.left; - hoverCenterY = boundingBox.top + boundingBox.height / 2 - rootBBox.top; - } - var hovertemplateLabels = {valueLabel: d3.format(d.valueFormat)(d.link.value) + d.valueSuffix}; - d.link.fullData = d.link.trace; + var hoverItems = []; - var tooltip = Fx.loneHover({ - x: hoverCenterX, - y: hoverCenterY, - name: hovertemplateLabels.valueLabel, - text: [ - d.link.label || '', - sourceLabel + d.link.source.label, - targetLabel + d.link.target.label, - d.link.concentrationscale ? concentrationLabel + d3.format('%0.2f')(d.link.flow.labelConcentration) : '' - ].filter(renderableValuePresent).join('
'), - 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', + function hoverCenterPosition(link) { + var hoverCenterX, hoverCenterY; + if(link.circular) { + hoverCenterX = (link.circularPathData.leftInnerExtent + link.circularPathData.rightInnerExtent) / 2 + d.parent.translateX; + hoverCenterY = link.circularPathData.verticalFullExtent + d.parent.translateY; + } else { + hoverCenterX = (link.source.x1 + link.target.x0) / 2 + d.parent.translateX; + hoverCenterY = (link.y0 + link.y1) / 2 + d.parent.translateY; + } + return [hoverCenterX, hoverCenterY]; + } - hovertemplate: obj.hovertemplate, - hovertemplateLabels: hovertemplateLabels, - eventData: [d.link] - }, { + // For each related links, create a hoverItem + var anchorIndex = 0; + for(var i = 0; i < d.flow.links.length; i++) { + var link = d.flow.links[i]; + if(gd._fullLayout.hovermode === 'closest' && d.link.pointNumber !== link.pointNumber) continue; + if(d.link.pointNumber === link.pointNumber) anchorIndex = i; + link.fullData = link.trace; + obj = d.link.trace.link; + var hoverCenter = hoverCenterPosition(link); + var hovertemplateLabels = {valueLabel: d3.format(d.valueFormat)(link.value) + d.valueSuffix}; + + hoverItems.push({ + x: hoverCenter[0], + y: hoverCenter[1], + name: hovertemplateLabels.valueLabel, + text: [ + link.label || '', + sourceLabel + link.source.label, + targetLabel + link.target.label, + link.concentrationscale ? concentrationLabel + d3.format('%0.2f')(link.flow.labelConcentration) : '' + ].filter(renderableValuePresent).join('
'), + color: castHoverOption(obj, 'bgcolor') || Color.addOpacity(link.color, 1), + borderColor: castHoverOption(obj, 'bordercolor'), + fontFamily: castHoverOption(obj, 'font.family'), + fontSize: castHoverOption(obj, 'font.size'), + fontColor: castHoverOption(obj, 'font.color'), + idealAlign: d3.event.x < hoverCenter[0] ? 'right' : 'left', + + hovertemplate: obj.hovertemplate, + hovertemplateLabels: hovertemplateLabels, + eventData: [link] + }); + } + + var tooltips = Fx.multiHovers(hoverItems, { container: fullLayout._hoverlayer.node(), outerContainer: fullLayout._paper.node(), - gd: gd + gd: gd, + anchorIndex: anchorIndex }); - if(!d.link.concentrationscale) { - makeTranslucent(tooltip, 0.65); - } - makeTextContrasty(tooltip); + tooltips.each(function() { + var tooltip = this; + if(!d.link.concentrationscale) { + makeTranslucent(tooltip, 0.65); + } + makeTextContrasty(tooltip); + }); }; var linkUnhover = function(element, d, sankey) { diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index ab4a330c8f5..9e3be406b35 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -142,6 +142,9 @@ function sankeyModel(layout, d, traceIndex) { concentration: link.value / total, links: flowLinks }; + if(link.concentrationscale) { + link.color = tinycolor(link.concentrationscale(link.flow.labelConcentration)); + } } } @@ -287,9 +290,6 @@ function sankeyModel(layout, d, traceIndex) { function linkModel(d, l, i) { var tc = tinycolor(l.color); - if(l.concentrationscale) { - tc = tinycolor(l.concentrationscale(l.flow.labelConcentration)); - } var basicKey = l.source.label + '|' + l.target.label; var key = basicKey + '__' + i; diff --git a/test/image/mocks/sankey_circular_large.json b/test/image/mocks/sankey_circular_large.json index 6af23521291..99c8af5835f 100644 --- a/test/image/mocks/sankey_circular_large.json +++ b/test/image/mocks/sankey_circular_large.json @@ -220,10 +220,11 @@ [1, "#9467bd"] ] }], - "hovertemplate": "%{label}
%{flow.labelConcentration:%0.2f}
%{flow.value}" + "hovertemplate": "%{label}
flow.labelConcentration: %{flow.labelConcentration:0.2%}
flow.concentration: %{flow.concentration:0.2%}
flow.value: %{flow.value}" } }], "layout": { + "hovermode": "x", "width": 800, "height": 800 } diff --git a/test/image/mocks/sankey_link_concentration.json b/test/image/mocks/sankey_link_concentration.json index a4b4564fff0..a5f45300584 100644 --- a/test/image/mocks/sankey_link_concentration.json +++ b/test/image/mocks/sankey_link_concentration.json @@ -70,6 +70,7 @@ "layout": { "title": "Sankey diagram with links colored based on their concentration within a flow", "width": 800, - "height": 800 + "height": 800, + "hovermode": "x" } } diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js index 5780a72cdec..b9a84534a66 100644 --- a/test/jasmine/tests/sankey_test.js +++ b/test/jasmine/tests/sankey_test.js @@ -778,6 +778,47 @@ describe('sankey tests', function() { .then(done); }); + it('should show the multiple hover labels in a flow in hovermode `x`', function(done) { + var gd = createGraphDiv(); + var mockCopy = Lib.extendDeep({}, mock); + Plotly.plot(gd, mockCopy).then(function() { + _hover(351, 202); + + assertLabel( + ['source: Nuclear', 'target: Thermal generation', '100TWh'], + ['rgb(144, 238, 144)', 'rgb(68, 68, 68)', 13, 'Arial', 'rgb(68, 68, 68)'] + ); + + var g = d3.selectAll('.hovertext'); + expect(g.size()).toBe(1); + return Plotly.relayout(gd, 'hovermode', 'x'); + }) + .then(function() { + _hover(351, 202); + + assertMultipleLabels( + [ + ['Old generation plant (made-up)', 'source: Nuclear', 'target: Thermal generation', '500TWh'], + ['New generation plant (made-up)', 'source: Nuclear', 'target: Thermal generation', '140TWh'], + ['source: Nuclear', 'target: Thermal generation', '100TWh'], + ['source: Nuclear', 'target: Thermal generation', '100TWh'] + ], + [ + ['rgb(33, 102, 172)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'], + ['rgb(178, 24, 43)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'], + ['rgb(144, 238, 144)', 'rgb(68, 68, 68)', 13, 'Arial', 'rgb(68, 68, 68)'], + ['rgb(218, 165, 32)', 'rgb(68, 68, 68)', 13, 'Arial', 'rgb(68, 68, 68)'] + ] + ); + + var g = d3.select('.hovertext:nth-child(3)'); + var domRect = g.node().getBoundingClientRect(); + expect((domRect.bottom + domRect.top) / 2).toBeCloseTo(203, 0, 'it should center the hoverlabel associated with hovered link'); + }) + .catch(failTest) + .then(done); + }); + it('should not show any labels if hovermode is false', function(done) { var gd = createGraphDiv(); var mockCopy = Lib.extendDeep({}, mock); @@ -1265,7 +1306,18 @@ describe('sankey tests', function() { }); function assertLabel(content, style) { + assertMultipleLabels([content], [style]); +} + +function assertMultipleLabels(contentArray, styleArray) { var g = d3.selectAll('.hovertext'); + expect(g.size()).toEqual(contentArray.length, 'wrong number of hoverlabels, expected to find ' + contentArray.length); + g.each(function(el, i) { + _assertLabelGroup(d3.select(this), contentArray[i], styleArray[i]); + }); +} + +function _assertLabelGroup(g, content, style) { var lines = g.selectAll('.nums .line'); var name = g.selectAll('.name'); var tooltipBoundingBox = g.node().getBoundingClientRect();