Skip to content
Open
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
16 changes: 14 additions & 2 deletions src/components/fx/click.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var hover = require('./hover').hover;

module.exports = function click(gd, evt, subplot) {
var annotationsDone = Registry.getComponentMethod('annotations', 'onClick')(gd, gd._hoverdata);
var fullLayout = gd._fullLayout;

// fallback to fail-safe in case the plot type's hover method doesn't pass the subplot.
// Ternary, for example, didn't, but it was caught because tested.
Expand All @@ -14,9 +15,20 @@ module.exports = function click(gd, evt, subplot) {
hover(gd, evt, subplot, true);
}

function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata, event: evt}); }
function emitClick() {
var clickData = {points: gd._hoverdata, event: evt};

// get coordinate values from latest hover call, if available
clickData.xaxes ??= gd._hoverXAxes;
clickData.yaxes ??= gd._hoverYAxes;
clickData.xvals ??= gd._hoverXVals;
clickData.yvals ??= gd._hoverYVals;

if(gd._hoverdata && evt && evt.target) {
gd.emit('plotly_click', clickData);
}

if((gd._hoverdata || fullLayout.clickanywhere) && evt && evt.target) {
if(!gd._hoverdata) gd._hoverdata = [];
if(annotationsDone && annotationsDone.then) {
annotationsDone.then(emitClick);
} else emitClick();
Expand Down
17 changes: 17 additions & 0 deletions src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,12 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
}
}

// Save coordinate values so clickanywhere can be used without hoveranywhere
gd._hoverXVals = xvalArray;
gd._hoverYVals = yvalArray;
gd._hoverXAxes = xaArray;
gd._hoverYAxes = yaArray;

// the pixel distance to beat as a matching point
// in 'x' or 'y' mode this resets for each trace
var distance = Infinity;
Expand Down Expand Up @@ -778,6 +784,17 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
createSpikelines(gd, spikePoints, spikelineOpts);
}
}

if (fullLayout.hoveranywhere && !noHoverEvent && eventTarget) {
gd.emit('plotly_hover', {
event: evt,
points: [],
xaxes: xaArray,
yaxes: yaArray,
xvals: xvalArray,
yvals: yvalArray
});
}
return result;
}

Expand Down
2 changes: 2 additions & 0 deletions src/components/fx/hovermode_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ module.exports = function handleHoverModeDefaults(layoutIn, layoutOut) {

coerce('clickmode');
coerce('hoversubplots');
coerce('hoveranywhere');
coerce('clickanywhere');
return coerce('hovermode');
};
22 changes: 22 additions & 0 deletions src/components/fx/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ module.exports = {
'when `hovermode` is set to *x*, *x unified*, *y* or *y unified*.',
].join(' ')
},
hoveranywhere: {
valType: 'boolean',
dflt: false,
editType: 'none',
description: [
'If true, `plotly_hover` events will fire for any cursor position',
'within the plot area, not just over traces.',
'When the cursor is not over a trace, the event will have an empty `points` array',
'but will include `xvals` and `yvals` with cursor coordinates in data space.'
].join(' ')
},
clickanywhere: {
valType: 'boolean',
dflt: false,
editType: 'none',
description: [
'If true, `plotly_click` events will fire for any click position',
'within the plot area, not just over traces.',
'When clicking where there is no trace data, the event will have an empty `points` array',
'but will include `xvals` and `yvals` with click coordinates in data space.'
].join(' ')
},
hoverdistance: {
valType: 'integer',
min: -1,
Expand Down
137 changes: 137 additions & 0 deletions test/jasmine/tests/hover_click_anywhere_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
var Plotly = require('../../../lib/index');
var Fx = require('../../../src/components/fx');
var Lib = require('../../../src/lib');

var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var click = require('../assets/click');


function makePlot(gd, layoutExtras) {
return Plotly.newPlot(gd, [{
x: [1, 2, 3],
y: [1, 3, 2],
type: 'scatter',
mode: 'markers'
}], Lib.extendFlat({
width: 400, height: 400,
margin: {l: 50, t: 50, r: 50, b: 50},
xaxis: {range: [0, 10]},
yaxis: {range: [0, 10]},
hovermode: 'closest'
}, layoutExtras || {}));
}

describe('hoveranywhere', function() {
'use strict';

var gd;

beforeEach(function() { gd = createGraphDiv(); });
afterEach(destroyGraphDiv);

function _hover(xPixel, yPixel) {
var bb = gd.getBoundingClientRect();
var s = gd._fullLayout._size;
Fx.hover(gd, {
clientX: xPixel + bb.left + s.l,
clientY: yPixel + bb.top + s.t,
target: gd.querySelector('.nsewdrag')
}, 'xy');
Lib.clearThrottle();
}

it('emits plotly_hover with coordinate data on empty space', function(done) {
var hoverData;

makePlot(gd, {hoveranywhere: true}).then(function() {
gd.on('plotly_hover', function(d) { hoverData = d; });

// hover over empty area (no data points nearby)
_hover(250, 50);

expect(hoverData).toBeDefined();
expect(hoverData.points).toEqual([]);
expect(hoverData.xvals.length).toBe(1);
expect(hoverData.yvals.length).toBe(1);
expect(typeof hoverData.xvals[0]).toBe('number');
})
.then(done, done.fail);
});

it('does not fire on empty space by default', function(done) {
var hoverData;

makePlot(gd).then(function() {
gd.on('plotly_hover', function(d) { hoverData = d; });
_hover(250, 50);
expect(hoverData).toBeUndefined();
})
.then(done, done.fail);
});

it('still returns normal point data on traces', function(done) {
var hoverData;

makePlot(gd, {hoveranywhere: true}).then(function() {
gd.on('plotly_hover', function(d) { hoverData = d; });

// hover near (2, 3)
_hover(60, 210);

expect(hoverData.points.length).toBe(1);
expect(hoverData.points[0].x).toBe(2);
expect(hoverData.points[0].y).toBe(3);
})
.then(done, done.fail);
});

it('respects hovermode:false', function(done) {
var hoverData;

makePlot(gd, {hoveranywhere: true, hovermode: false}).then(function() {
gd.on('plotly_hover', function(d) { hoverData = d; });
_hover(250, 50);
expect(hoverData).toBeUndefined();
})
.then(done, done.fail);
});
});

describe('clickanywhere', function() {
'use strict';

var gd;

beforeEach(function() { gd = createGraphDiv(); });
afterEach(destroyGraphDiv);

it('emits plotly_click with empty points on empty space', function(done) {
var clickData;

makePlot(gd, {clickanywhere: true}).then(function() {
gd.on('plotly_click', function(d) { clickData = d; });

var s = gd._fullLayout._size;
click(s.l + 250, s.t + 50);

expect(clickData).toBeDefined();
expect(clickData.points).toEqual([]);
})
.then(done, done.fail);
});

it('does not fire on empty space by default', function(done) {
var clickData;

makePlot(gd).then(function() {
gd.on('plotly_click', function(d) { clickData = d; });

var s = gd._fullLayout._size;
click(s.l + 250, s.t + 50);

expect(clickData).toBeUndefined();
})
.then(done, done.fail);
});
});
12 changes: 12 additions & 0 deletions test/plot-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,12 @@
"ummalqura"
]
},
"clickanywhere": {
"description": "If true, `plotly_click` events will fire for any click position within the plot area, not just over traces. When clicking where there is no trace data, the event will have an empty `points` array but will include `xvals` and `yvals` with click coordinates in data space.",
"dflt": false,
"editType": "none",
"valType": "boolean"
},
"clickmode": {
"description": "Determines the mode of single click interactions. *event* is the default value and emits the `plotly_click` event. In addition this mode emits the `plotly_selected` event in drag modes *lasso* and *select*, but with no event data attached (kept for compatibility reasons). The *select* flag enables selecting single data points via click. This mode also supports persistent selections, meaning that pressing Shift while clicking, adds to / subtracts from an existing selection. *select* with `hovermode`: *x* can be confusing, consider explicitly setting `hovermode`: *closest* when using this feature. Selection events are sent accordingly as long as *event* flag is set as well. When the *event* flag is missing, `plotly_click` and `plotly_selected` events are not fired.",
"dflt": "event",
Expand Down Expand Up @@ -2807,6 +2813,12 @@
"editType": "plot",
"valType": "boolean"
},
"hoveranywhere": {
"description": "If true, `plotly_hover` events will fire for any cursor position within the plot area, not just over traces. When the cursor is not over a trace, the event will have an empty `points` array but will include `xvals` and `yvals` with cursor coordinates in data space.",
"dflt": false,
"editType": "none",
"valType": "boolean"
},
"hoverdistance": {
"description": "Sets the default distance (in pixels) to look for data to add hover labels (-1 means no cutoff, 0 means no looking for data). This is only a real distance for hovering on point-like objects, like scatter points. For area-like objects (bars, scatter fills, etc) hovering is on inside the area and off outside, but these objects will not supersede hover on point-like objects in case of conflict.",
"dflt": 20,
Expand Down