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
Show file tree
Hide file tree
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
Expand Up @@ -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) {
Expand All @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions src/components/modebar/manage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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'];
Expand Down
94 changes: 56 additions & 38 deletions src/traces/sankey/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions src/traces/sankey/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}

Expand Down Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion test/image/mocks/sankey_circular_large.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion test/image/mocks/sankey_link_concentration.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down