Skip to content

Set unified hovermode via template #4669

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 7 commits into from
Mar 24, 2020
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
38 changes: 29 additions & 9 deletions src/components/fx/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ var Lib = require('../../lib');

// look for either subplot or xaxis and yaxis attributes
// does not handle splom case
exports.getSubplot = function getSubplot(trace) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nonblocking, but at one point these names were useful in stack traces. That may or may not not be the case anymore, based on changes to our build and sourcemap processes... but historically that was the reason for names like this that are otherwise unused.

exports.getSubplot = function(trace) {
return trace.subplot || (trace.xaxis + trace.yaxis) || trace.geo;
};

// is trace in given list of subplots?
// does handle splom case
exports.isTraceInSubplots = function isTraceInSubplots(trace, subplots) {
exports.isTraceInSubplots = function(trace, subplots) {
if(trace.type === 'splom') {
var xaxes = trace.xaxes || [];
var yaxes = trace.yaxes || [];
Expand All @@ -36,28 +36,28 @@ exports.isTraceInSubplots = function isTraceInSubplots(trace, subplots) {
};

// convenience functions for mapping all relevant axes
exports.flat = function flat(subplots, v) {
exports.flat = function(subplots, v) {
var out = new Array(subplots.length);
for(var i = 0; i < subplots.length; i++) {
out[i] = v;
}
return out;
};

exports.p2c = function p2c(axArray, v) {
exports.p2c = function(axArray, v) {
var out = new Array(axArray.length);
for(var i = 0; i < axArray.length; i++) {
out[i] = axArray[i].p2c(v);
}
return out;
};

exports.getDistanceFunction = function getDistanceFunction(mode, dx, dy, dxy) {
exports.getDistanceFunction = function(mode, dx, dy, dxy) {
if(mode === 'closest') return dxy || exports.quadrature(dx, dy);
return mode.charAt(0) === 'x' ? dx : dy;
};

exports.getClosest = function getClosest(cd, distfn, pointData) {
exports.getClosest = function(cd, distfn, pointData) {
// do we already have a point number? (array mode only)
if(pointData.index !== false) {
if(pointData.index >= 0 && pointData.index < cd.length) {
Expand Down Expand Up @@ -87,11 +87,11 @@ exports.getClosest = function getClosest(cd, distfn, pointData) {
* @param {number} v1: signed difference between the current position and the right edge
* @param {number} passVal: the value to return on success
*/
exports.inbox = function inbox(v0, v1, passVal) {
exports.inbox = function(v0, v1, passVal) {
return (v0 * v1 < 0 || v0 === 0) ? passVal : Infinity;
};

exports.quadrature = function quadrature(dx, dy) {
exports.quadrature = function(dx, dy) {
return function(di) {
var x = dx(di);
var y = dy(di);
Expand All @@ -114,7 +114,7 @@ exports.quadrature = function quadrature(dx, dy) {
* @param {object} cd
* @return {object}
*/
exports.makeEventData = function makeEventData(pt, trace, cd) {
exports.makeEventData = function(pt, trace, cd) {
// hover uses 'index', select uses 'pointNumber'
var pointNumber = 'index' in pt ? pt.index : pt.pointNumber;

Expand Down Expand Up @@ -238,3 +238,23 @@ function getPointData(val, pointNumber) {
return val[pointNumber];
}
}

var xyHoverMode = {
x: true,
y: true
};

var unifiedHoverMode = {
'x unified': true,
'y unified': true
};

exports.isUnifiedHover = function(hovermode) {
if(typeof hovermode !== 'string') return false;
return !!unifiedHoverMode[hovermode];
};

exports.isXYhover = function(hovermode) {
if(typeof hovermode !== 'string') return false;
return !!xyHoverMode[hovermode];
};
15 changes: 7 additions & 8 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ var YSHIFTY = Math.sin(YA_RADIANS);
var HOVERARROWSIZE = constants.HOVERARROWSIZE;
var HOVERTEXTPAD = constants.HOVERTEXTPAD;

var XY = {x: 1, y: 1};

// fx.hover: highlight data on hover
// evt can be a mousemove event, or an object with data about what points
// to hover on
Expand Down Expand Up @@ -394,7 +392,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {

// within one trace mode can sometimes be overridden
mode = hovermode;
if(['x unified', 'y unified'].indexOf(mode) !== -1) {
if(helpers.isUnifiedHover(mode)) {
mode = mode.charAt(0);
}

Expand Down Expand Up @@ -631,9 +629,10 @@ function _hover(gd, evt, subplot, noHoverEvent) {
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });

// If in compare mode, select every point at position
if(hoverData[0].length !== 0 &&
XY[mode] &&
hoverData[0].trace.type !== 'splom' // TODO: add support for splom
if(
helpers.isXYhover(mode) &&
hoverData[0].length !== 0 &&
hoverData[0].trace.type !== 'splom' // TODO: add support for splom
) {
var hd = hoverData[0];
var cd0 = hd.cd[hd.index];
Expand Down Expand Up @@ -715,7 +714,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {

var hoverLabels = createHoverText(hoverData, labelOpts, gd);

if(['x unified', 'y unified'].indexOf(hovermode) === -1) {
if(!helpers.isUnifiedHover(hovermode)) {
hoverAvoidOverlaps(hoverLabels, rotateLabels ? 'xa' : 'ya', fullLayout);
alignHoverText(hoverLabels, rotateLabels);
}
Expand Down Expand Up @@ -976,7 +975,7 @@ function createHoverText(hoverData, opts, gd) {
}

// Show a single hover label
if(['x unified', 'y unified'].indexOf(hovermode) !== -1) {
if(helpers.isUnifiedHover(hovermode)) {
// Delete leftover hover labels from other hovermodes
container.selectAll('g.hovertext').remove();

Expand Down
54 changes: 54 additions & 0 deletions src/components/fx/hovermode_defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright 2012-2020, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Lib = require('../../lib');
var layoutAttributes = require('./layout_attributes');

module.exports = function handleHoverModeDefaults(layoutIn, layoutOut, fullData) {
function coerce(attr, dflt) {
// don't coerce if it is already coerced in other place e.g. in cartesian defaults
if(layoutOut[attr] !== undefined) return layoutOut[attr];

return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
}

var clickmode = coerce('clickmode');

var hovermodeDflt;
if(layoutOut._has('cartesian')) {
if(clickmode.indexOf('select') > -1) {
hovermodeDflt = 'closest';
} else {
// flag for 'horizontal' plots:
// determines the state of the mode bar 'compare' hovermode button
layoutOut._isHoriz = isHoriz(fullData, layoutOut);
hovermodeDflt = layoutOut._isHoriz ? 'y' : 'x';
}
} else hovermodeDflt = 'closest';

return coerce('hovermode', hovermodeDflt);
};

function isHoriz(fullData, fullLayout) {
var stackOpts = fullLayout._scatterStackOpts || {};

for(var i = 0; i < fullData.length; i++) {
var trace = fullData[i];
var subplot = trace.xaxis + trace.yaxis;
var subplotStackOpts = stackOpts[subplot] || {};
var groupOpts = subplotStackOpts[trace.stackgroup] || {};

if(trace.orientation !== 'h' && groupOpts.orientation !== 'h') {
return false;
}
}

return true;
}
47 changes: 7 additions & 40 deletions src/components/fx/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,24 @@
'use strict';

var Lib = require('../../lib');
var isUnifiedHover = require('./helpers').isUnifiedHover;
var layoutAttributes = require('./layout_attributes');
var handleHoverModeDefaults = require('./hovermode_defaults');

module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
function coerce(attr, dflt) {
return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
}

var clickmode = coerce('clickmode');

var dragMode = coerce('dragmode');
if(dragMode === 'select') coerce('selectdirection');

var hovermodeDflt;
if(layoutOut._has('cartesian')) {
if(clickmode.indexOf('select') > -1) {
hovermodeDflt = 'closest';
} else {
// flag for 'horizontal' plots:
// determines the state of the mode bar 'compare' hovermode button
layoutOut._isHoriz = isHoriz(fullData, layoutOut);
hovermodeDflt = layoutOut._isHoriz ? 'y' : 'x';
}
} else hovermodeDflt = 'closest';

var hoverMode = coerce('hovermode', hovermodeDflt);
var hoverMode = handleHoverModeDefaults(layoutIn, layoutOut, fullData);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about short-circuiting this one, right? something like:

var hoverMode = layoutOut.hovermode;
if (hoverMode === undefined) {
    hoverMode = handleHoverModeDefaults(layoutIn, layoutOut, fullData);
}

Is that a possibility? Or would that not work for some reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hovermode and clickmode use special coerce function which returns previously coerced value.

module.exports = function handleHoverModeDefaults(layoutIn, layoutOut, fullData) {
function coerce(attr, dflt) {
// don't coerce if it is already coerced in other place e.g. in cartesian defaults
if(layoutOut[attr] !== undefined) return layoutOut[attr];
return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
}

if(hoverMode) {
var dflt;
if(['x unified', 'y unified'].indexOf(hoverMode) !== -1) {
dflt = -1;
}
coerce('hoverdistance');
coerce('spikedistance', dflt);
coerce('spikedistance', isUnifiedHover(hoverMode) ? -1 : undefined);
}

var dragMode = coerce('dragmode');
if(dragMode === 'select') coerce('selectdirection');

// if only mapbox or geo subplots is present on graph,
// reset 'zoom' dragmode to 'pan' until 'zoom' is implemented,
// so that the correct modebar button is active
Expand All @@ -57,20 +41,3 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
layoutOut.dragmode = 'pan';
}
};

function isHoriz(fullData, fullLayout) {
var stackOpts = fullLayout._scatterStackOpts || {};

for(var i = 0; i < fullData.length; i++) {
var trace = fullData[i];
var subplot = trace.xaxis + trace.yaxis;
var subplotStackOpts = stackOpts[subplot] || {};
var groupOpts = subplotStackOpts[trace.stackgroup] || {};

if(trace.orientation !== 'h' && groupOpts.orientation !== 'h') {
return false;
}
}

return true;
}
3 changes: 2 additions & 1 deletion src/components/modebar/manage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
var axisIds = require('../../plots/cartesian/axis_ids');
var scatterSubTypes = require('../../traces/scatter/subtypes');
var Registry = require('../../registry');
var isUnifiedHover = require('../fx/helpers').isUnifiedHover;

var createModeBar = require('./modebar');
var modeBarButtons = require('./buttons');
Expand Down Expand Up @@ -85,7 +86,7 @@ function getButtonGroups(gd) {
var hasPolar = fullLayout._has('polar');
var hasSankey = fullLayout._has('sankey');
var allAxesFixed = areAllAxesFixed(fullLayout);
var hasUnifiedHoverLabel = ['x unified', 'y unified'].indexOf(fullLayout.hovermode) !== -1;
var hasUnifiedHoverLabel = isUnifiedHover(fullLayout.hovermode);

var groups = [];

Expand Down
8 changes: 6 additions & 2 deletions src/plots/cartesian/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

var Lib = require('../../lib');
var Color = require('../../components/color');
var isUnifiedHover = require('../../components/fx/helpers').isUnifiedHover;
var handleHoverModeDefaults = require('../../components/fx/hovermode_defaults');
var Template = require('../../plot_api/plot_template');
var basePlotLayoutAttributes = require('../layout_attributes');

Expand Down Expand Up @@ -205,6 +207,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
}
}

var hovermode = handleHoverModeDefaults(layoutIn, layoutOut, fullData);
var unifiedHover = isUnifiedHover(hovermode);

// first pass creates the containers, determines types, and handles most of the settings
for(i = 0; i < axNames.length; i++) {
axName = axNames[i];
Expand Down Expand Up @@ -249,8 +254,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
handleTypeDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions);
handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut);

var unifiedHover = layoutIn.hovermode && ['x unified', 'y unified'].indexOf(layoutIn.hovermode) !== -1;
var unifiedSpike = unifiedHover && axLetter === layoutIn.hovermode.charAt(0);
var unifiedSpike = unifiedHover && axLetter === hovermode.charAt(0);
var spikecolor = coerce2('spikecolor', unifiedHover ? axLayoutOut.color : undefined);
var spikethickness = coerce2('spikethickness', unifiedHover ? 1.5 : undefined);
var spikedash = coerce2('spikedash', unifiedHover ? 'dot' : undefined);
Expand Down
28 changes: 28 additions & 0 deletions test/jasmine/tests/hover_label_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4093,6 +4093,34 @@ describe('hovermode: (x|y)unified', function() {
.then(done);
});

it('unified hover modes should work for x/y cartesian traces via template', function(done) {
var mockCopy = Lib.extendDeep({}, mock);
delete mockCopy.layout.hovermode;
mockCopy.layout.template = {
layout: {
hovermode: 'y unified'
}
};
Plotly.newPlot(gd, mockCopy)
.then(function(gd) {
expect(gd._fullLayout.hovermode).toBe('y unified');
var ax = gd._fullLayout.yaxis;
expect(ax.showspike).toBeTrue;
expect(ax.spikemode).toBe('across');
expect(ax.spikethickness).toBe(1.5);
expect(ax.spikedash).toBe('dot');
expect(ax.spikecolor).toBe('#444');
expect(ax.spikesnap).toBe('hovered data');
expect(gd._fullLayout.xaxis.showspike).toBeFalse;

_hover(gd, { yval: 6 });

assertLabel({title: '6', items: ['trace 0 : 2', 'trace 1 : 5']});
})
.catch(failTest)
.then(done);
});

it('x unified should work for x/y cartesian traces with legendgroup', function(done) {
var mockLegendGroup = require('@mocks/legendgroup.json');
var mockCopy = Lib.extendDeep({}, mockLegendGroup);
Expand Down