Skip to content

sankey: compare links in a flow on hover #3730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 8, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
@@ -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:
4 changes: 4 additions & 0 deletions src/components/modebar/manage.js
Original file line number Diff line number Diff line change
@@ -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'];
94 changes: 56 additions & 38 deletions src/traces/sankey/plot.js
Original file line number Diff line number Diff line change
@@ -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('<br>'),
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('<br>'),
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) {
6 changes: 3 additions & 3 deletions src/traces/sankey/render.js
Original file line number Diff line number Diff line change
@@ -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;

3 changes: 2 additions & 1 deletion test/image/mocks/sankey_circular_large.json
Original file line number Diff line number Diff line change
@@ -220,10 +220,11 @@
[1, "#9467bd"]
]
}],
"hovertemplate": "<b>%{label}</b><br>%{flow.labelConcentration:%0.2f}<br>%{flow.value}"
"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}"
}
}],
"layout": {
"hovermode": "x",
"width": 800,
"height": 800
}
3 changes: 2 additions & 1 deletion test/image/mocks/sankey_link_concentration.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
52 changes: 52 additions & 0 deletions test/jasmine/tests/sankey_test.js
Original file line number Diff line number Diff line change
@@ -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();