Skip to content

Commit 5baa139

Browse files
authored
Merge pull request #3730 from plotly/sankey-multiple-hover
sankey: compare links in a flow on hover
2 parents e5b8c71 + f86df3c commit 5baa139

File tree

7 files changed

+127
-46
lines changed

7 files changed

+127
-46
lines changed

src/components/fx/hover.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,10 @@ exports.multiHovers = function multiHovers(hoverItems, opts) {
213213
// Fix vertical overlap
214214
var tooltipSpacing = 5;
215215
var lastBottomY = 0;
216+
var anchor = 0;
216217
hoverLabel
217218
.sort(function(a, b) {return a.y0 - b.y0;})
218-
.each(function(d) {
219+
.each(function(d, i) {
219220
var topY = d.y0 - d.by / 2;
220221

221222
if((topY - tooltipSpacing) < lastBottomY) {
@@ -225,12 +226,16 @@ exports.multiHovers = function multiHovers(hoverItems, opts) {
225226
}
226227

227228
lastBottomY = topY + d.by + d.offset;
228-
});
229229

230+
if(i === opts.anchorIndex || 0) anchor = d.offset;
231+
})
232+
.each(function(d) {
233+
d.offset -= anchor;
234+
});
230235

231236
alignHoverText(hoverLabel, fullOpts.rotateLabels);
232237

233-
return hoverLabel.node();
238+
return hoverLabel;
234239
};
235240

236241
// The actual implementation is here:

src/components/modebar/manage.js

+4
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) {
8686
var hasTernary = fullLayout._has('ternary');
8787
var hasMapbox = fullLayout._has('mapbox');
8888
var hasPolar = fullLayout._has('polar');
89+
var hasSankey = fullLayout._has('sankey');
8990
var allAxesFixed = areAllAxesFixed(fullLayout);
9091

9192
var groups = [];
@@ -139,6 +140,9 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd, showSendToCloud) {
139140
else if(hasPie) {
140141
hoverGroup = ['hoverClosestPie'];
141142
}
143+
else if(hasSankey) {
144+
hoverGroup = ['hoverClosestCartesian', 'hoverCompareCartesian'];
145+
}
142146
else { // hasPolar, hasTernary
143147
// always show at least one hover icon.
144148
hoverGroup = ['toggleHover'];

src/traces/sankey/plot.js

+56-38
Original file line numberDiff line numberDiff line change
@@ -156,51 +156,69 @@ module.exports = function plot(gd, calcData) {
156156
if(gd._fullLayout.hovermode === false) return;
157157
var obj = d.link.trace.link;
158158
if(obj.hoverinfo === 'none' || obj.hoverinfo === 'skip') return;
159-
var rootBBox = gd._fullLayout._paperdiv.node().getBoundingClientRect();
160-
var hoverCenterX;
161-
var hoverCenterY;
162-
if(d.link.circular) {
163-
hoverCenterX = (d.link.circularPathData.leftInnerExtent + d.link.circularPathData.rightInnerExtent) / 2 + d.parent.translateX;
164-
hoverCenterY = d.link.circularPathData.verticalFullExtent + d.parent.translateY;
165-
} else {
166-
var boundingBox = element.getBoundingClientRect();
167-
hoverCenterX = boundingBox.left + boundingBox.width / 2 - rootBBox.left;
168-
hoverCenterY = boundingBox.top + boundingBox.height / 2 - rootBBox.top;
169-
}
170159

171-
var hovertemplateLabels = {valueLabel: d3.format(d.valueFormat)(d.link.value) + d.valueSuffix};
172-
d.link.fullData = d.link.trace;
160+
var hoverItems = [];
173161

174-
var tooltip = Fx.loneHover({
175-
x: hoverCenterX,
176-
y: hoverCenterY,
177-
name: hovertemplateLabels.valueLabel,
178-
text: [
179-
d.link.label || '',
180-
sourceLabel + d.link.source.label,
181-
targetLabel + d.link.target.label,
182-
d.link.concentrationscale ? concentrationLabel + d3.format('%0.2f')(d.link.flow.labelConcentration) : ''
183-
].filter(renderableValuePresent).join('<br>'),
184-
color: castHoverOption(obj, 'bgcolor') || Color.addOpacity(d.tinyColorHue, 1),
185-
borderColor: castHoverOption(obj, 'bordercolor'),
186-
fontFamily: castHoverOption(obj, 'font.family'),
187-
fontSize: castHoverOption(obj, 'font.size'),
188-
fontColor: castHoverOption(obj, 'font.color'),
189-
idealAlign: d3.event.x < hoverCenterX ? 'right' : 'left',
162+
function hoverCenterPosition(link) {
163+
var hoverCenterX, hoverCenterY;
164+
if(link.circular) {
165+
hoverCenterX = (link.circularPathData.leftInnerExtent + link.circularPathData.rightInnerExtent) / 2 + d.parent.translateX;
166+
hoverCenterY = link.circularPathData.verticalFullExtent + d.parent.translateY;
167+
} else {
168+
hoverCenterX = (link.source.x1 + link.target.x0) / 2 + d.parent.translateX;
169+
hoverCenterY = (link.y0 + link.y1) / 2 + d.parent.translateY;
170+
}
171+
return [hoverCenterX, hoverCenterY];
172+
}
190173

191-
hovertemplate: obj.hovertemplate,
192-
hovertemplateLabels: hovertemplateLabels,
193-
eventData: [d.link]
194-
}, {
174+
// For each related links, create a hoverItem
175+
var anchorIndex = 0;
176+
for(var i = 0; i < d.flow.links.length; i++) {
177+
var link = d.flow.links[i];
178+
if(gd._fullLayout.hovermode === 'closest' && d.link.pointNumber !== link.pointNumber) continue;
179+
if(d.link.pointNumber === link.pointNumber) anchorIndex = i;
180+
link.fullData = link.trace;
181+
obj = d.link.trace.link;
182+
var hoverCenter = hoverCenterPosition(link);
183+
var hovertemplateLabels = {valueLabel: d3.format(d.valueFormat)(link.value) + d.valueSuffix};
184+
185+
hoverItems.push({
186+
x: hoverCenter[0],
187+
y: hoverCenter[1],
188+
name: hovertemplateLabels.valueLabel,
189+
text: [
190+
link.label || '',
191+
sourceLabel + link.source.label,
192+
targetLabel + link.target.label,
193+
link.concentrationscale ? concentrationLabel + d3.format('%0.2f')(link.flow.labelConcentration) : ''
194+
].filter(renderableValuePresent).join('<br>'),
195+
color: castHoverOption(obj, 'bgcolor') || Color.addOpacity(link.color, 1),
196+
borderColor: castHoverOption(obj, 'bordercolor'),
197+
fontFamily: castHoverOption(obj, 'font.family'),
198+
fontSize: castHoverOption(obj, 'font.size'),
199+
fontColor: castHoverOption(obj, 'font.color'),
200+
idealAlign: d3.event.x < hoverCenter[0] ? 'right' : 'left',
201+
202+
hovertemplate: obj.hovertemplate,
203+
hovertemplateLabels: hovertemplateLabels,
204+
eventData: [link]
205+
});
206+
}
207+
208+
var tooltips = Fx.multiHovers(hoverItems, {
195209
container: fullLayout._hoverlayer.node(),
196210
outerContainer: fullLayout._paper.node(),
197-
gd: gd
211+
gd: gd,
212+
anchorIndex: anchorIndex
198213
});
199214

200-
if(!d.link.concentrationscale) {
201-
makeTranslucent(tooltip, 0.65);
202-
}
203-
makeTextContrasty(tooltip);
215+
tooltips.each(function() {
216+
var tooltip = this;
217+
if(!d.link.concentrationscale) {
218+
makeTranslucent(tooltip, 0.65);
219+
}
220+
makeTextContrasty(tooltip);
221+
});
204222
};
205223

206224
var linkUnhover = function(element, d, sankey) {

src/traces/sankey/render.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ function sankeyModel(layout, d, traceIndex) {
142142
concentration: link.value / total,
143143
links: flowLinks
144144
};
145+
if(link.concentrationscale) {
146+
link.color = tinycolor(link.concentrationscale(link.flow.labelConcentration));
147+
}
145148
}
146149
}
147150

@@ -287,9 +290,6 @@ function sankeyModel(layout, d, traceIndex) {
287290

288291
function linkModel(d, l, i) {
289292
var tc = tinycolor(l.color);
290-
if(l.concentrationscale) {
291-
tc = tinycolor(l.concentrationscale(l.flow.labelConcentration));
292-
}
293293
var basicKey = l.source.label + '|' + l.target.label;
294294
var key = basicKey + '__' + i;
295295

test/image/mocks/sankey_circular_large.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,11 @@
220220
[1, "#9467bd"]
221221
]
222222
}],
223-
"hovertemplate": "<b>%{label}</b><br>%{flow.labelConcentration:%0.2f}<br>%{flow.value}"
223+
"hovertemplate": "%{label}<br><b>flow.labelConcentration</b>: %{flow.labelConcentration:0.2%}<br><b>flow.concentration</b>: %{flow.concentration:0.2%}<br><b>flow.value</b>: %{flow.value}"
224224
}
225225
}],
226226
"layout": {
227+
"hovermode": "x",
227228
"width": 800,
228229
"height": 800
229230
}

test/image/mocks/sankey_link_concentration.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"layout": {
7171
"title": "Sankey diagram with links colored based on their concentration within a flow",
7272
"width": 800,
73-
"height": 800
73+
"height": 800,
74+
"hovermode": "x"
7475
}
7576
}

test/jasmine/tests/sankey_test.js

+52
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,47 @@ describe('sankey tests', function() {
778778
.then(done);
779779
});
780780

781+
it('should show the multiple hover labels in a flow in hovermode `x`', function(done) {
782+
var gd = createGraphDiv();
783+
var mockCopy = Lib.extendDeep({}, mock);
784+
Plotly.plot(gd, mockCopy).then(function() {
785+
_hover(351, 202);
786+
787+
assertLabel(
788+
['source: Nuclear', 'target: Thermal generation', '100TWh'],
789+
['rgb(144, 238, 144)', 'rgb(68, 68, 68)', 13, 'Arial', 'rgb(68, 68, 68)']
790+
);
791+
792+
var g = d3.selectAll('.hovertext');
793+
expect(g.size()).toBe(1);
794+
return Plotly.relayout(gd, 'hovermode', 'x');
795+
})
796+
.then(function() {
797+
_hover(351, 202);
798+
799+
assertMultipleLabels(
800+
[
801+
['Old generation plant (made-up)', 'source: Nuclear', 'target: Thermal generation', '500TWh'],
802+
['New generation plant (made-up)', 'source: Nuclear', 'target: Thermal generation', '140TWh'],
803+
['source: Nuclear', 'target: Thermal generation', '100TWh'],
804+
['source: Nuclear', 'target: Thermal generation', '100TWh']
805+
],
806+
[
807+
['rgb(33, 102, 172)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'],
808+
['rgb(178, 24, 43)', 'rgb(255, 255, 255)', 13, 'Arial', 'rgb(255, 255, 255)'],
809+
['rgb(144, 238, 144)', 'rgb(68, 68, 68)', 13, 'Arial', 'rgb(68, 68, 68)'],
810+
['rgb(218, 165, 32)', 'rgb(68, 68, 68)', 13, 'Arial', 'rgb(68, 68, 68)']
811+
]
812+
);
813+
814+
var g = d3.select('.hovertext:nth-child(3)');
815+
var domRect = g.node().getBoundingClientRect();
816+
expect((domRect.bottom + domRect.top) / 2).toBeCloseTo(203, 0, 'it should center the hoverlabel associated with hovered link');
817+
})
818+
.catch(failTest)
819+
.then(done);
820+
});
821+
781822
it('should not show any labels if hovermode is false', function(done) {
782823
var gd = createGraphDiv();
783824
var mockCopy = Lib.extendDeep({}, mock);
@@ -1265,7 +1306,18 @@ describe('sankey tests', function() {
12651306
});
12661307

12671308
function assertLabel(content, style) {
1309+
assertMultipleLabels([content], [style]);
1310+
}
1311+
1312+
function assertMultipleLabels(contentArray, styleArray) {
12681313
var g = d3.selectAll('.hovertext');
1314+
expect(g.size()).toEqual(contentArray.length, 'wrong number of hoverlabels, expected to find ' + contentArray.length);
1315+
g.each(function(el, i) {
1316+
_assertLabelGroup(d3.select(this), contentArray[i], styleArray[i]);
1317+
});
1318+
}
1319+
1320+
function _assertLabelGroup(g, content, style) {
12691321
var lines = g.selectAll('.nums .line');
12701322
var name = g.selectAll('.name');
12711323
var tooltipBoundingBox = g.node().getBoundingClientRect();

0 commit comments

Comments
 (0)