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