Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Click anywhere feature #5443

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

Open
sleighsoft wants to merge 13 commits into plotly:master
base: master
Choose a base branch
Loading
from sleighsoft:click_v1
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
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
28 changes: 23 additions & 5 deletions src/components/fx/click.js
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,30 @@ 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(data) { gd.emit('plotly_click', {points: data, event: evt}); }

if(gd._hoverdata && evt && evt.target) {
if(annotationsDone && annotationsDone.then) {
annotationsDone.then(emitClick);
} else emitClick();
var clickmode = gd._fullLayout.clickmode;
var data;
if(evt && evt.target) {
if(gd._hoverdata) {
data = gd._hoverdata;
} else if(clickmode.indexOf('anywhere') > -1) {
if(gd._fullLayout.geo) {
Copy link
Collaborator

@alexcjohnson alexcjohnson Sep 17, 2021

Choose a reason for hiding this comment

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

This logic means that if the plot has a geo subplot, we're assuming the click was on that, even if the plot has other subplots too. But we can do a lot better than that, and there are more subplot types than just geo and 2D cartesian. And fortunately we already have a mechanism to detect this: the third arg to click is subplot and it should tell us exactly which subplot the click came from.

2D cartesian, ternary, and polar subplots report this correctly (eg 'x2y3', 'ternary2', 'polar3'), geo doesn't but it should be easy to fix that. Then what we need to do is find the actual subplot object, and depending on its type calculate the appropriate coordinates within that particular subplot.

Pie, sankey, and funnelarea also all reach this point but don't give a subplot. We could have them give the trace number I guess, but is there anything useful to report for them? Just the raw coordinates within the plot?

3D, parcoords, and parcats don't even get here, I'm happy to ignore those for now.

archmoj reacted with thumbs up emoji
Copy link
Contributor Author

@sleighsoft sleighsoft Sep 20, 2021

Choose a reason for hiding this comment

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

Is that something that you intend to do in a second round or within this PR. If it is something I should fix, then I'd like a short example ideally for each plot type as I am not familiar with most of the ones you listed.

Copy link
Contributor

@archmoj archmoj Oct 5, 2021

Choose a reason for hiding this comment

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

var lat = gd._fullLayout.geo._subplot.xaxis.p2c();
Copy link

Choose a reason for hiding this comment

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

not sure if I implemented incorrectly, but to get this code to work in my case I changed part of it to

 if(evt) { //no target unless you click on an object
 if(gd._hoverdata) {
 data = gd._hoverdata;
 } else { //the proposed object parsing did not work for me in the if statements or getting the coords
 var lat = gd._fullLayout.map._subplot.xaxis.p2c();
 var lon = gd._fullLayout.map._subplot.yaxis.p2c();
 data = [{lat: lat, lon: lon}];
 }

I am using scattermap and plotly.js v3.0.0

var lon = gd._fullLayout.geo._subplot.yaxis.p2c();
data = [{lat: lat, lon: lon}];
} else {
var bb = evt.target.getBoundingClientRect();
var x = gd._fullLayout.xaxis.p2d(evt.clientX - bb.left);
var y = gd._fullLayout.yaxis.p2d(evt.clientY - bb.top);
data = [{x: x, y: y}];
}
}
if(data) {
if(annotationsDone && annotationsDone.then) {
annotationsDone.then(function() { emitClick(data); });
} else emitClick(data);
}

// why do we get a double event without this???
if(evt.stopImmediatePropagation) evt.stopImmediatePropagation();
Expand Down
9 changes: 7 additions & 2 deletions src/components/fx/layout_attributes.js
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ fontAttrs.size.dflt = constants.HOVERFONTSIZE;
module.exports = {
clickmode: {
valType: 'flaglist',
flags: ['event', 'select'],
flags: ['event', 'select', 'anywhere'],
dflt: 'event',
editType: 'plot',
extras: ['none'],
Expand All @@ -29,7 +29,12 @@ module.exports = {
'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.'
'events are not fired.',
'The *anywhere* flag extends the *select* flag by allowing to trigger a',
'click event anywhere in the plot. The click event will always include *x*',
'and *y* coordinates and if a data point is below the cursor it will also',
'include information about the data point. When specifying *anywhere* the',
'*select* flag becomes superfluous.'
].join(' ')
},
dragmode: {
Expand Down
206 changes: 206 additions & 0 deletions test/jasmine/tests/anywhere_test.js
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
var Plotly = require('@lib/index');
var Lib = require('@src/lib');
var click = require('../assets/click');

var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var DBLCLICKDELAY = require('@src/plot_api/plot_config').dfltConfig.doubleClickDelay;

var clickEvent;
var clickedPromise;

function resetEvents(gd) {
clickEvent = null;

gd.removeAllListeners();

clickedPromise = new Promise(function(resolve) {
gd.on('plotly_click', function(data) {
clickEvent = data.points[0];
resolve();
});
});
}

describe('Click-to-select', function() {
var mock14PtsScatter = {
'in-margin': { x: 28, y: 28 },
'point-0': { x: 92, y: 102 },
'between-point-0-and-1': { x: 117, y: 110 },
'point-11': { x: 339, y: 214 },
};
var expectedEventsScatter = {
'in-margin': false,
'point-0': {
curveNumber: 0,
pointIndex: 0,
pointNumber: 0,
x: 0.002,
y: 16.25
},
'between-point-0-and-1': { x: 0.002990379231567056, y: 14.169142943944111 },
'point-11': {
curveNumber: 0,
pointIndex: 11,
pointNumber: 11,
x: 0.125,
y: 2.125
},
};

var mockPtsGeoscatter = {
'start': {lat: 40.7127, lon: -74.0059},
'end': {lat: 51.5072, lon: 0.1275},
};
var mockPtsGeoscatterClick = {
'in-margin': { x: 28, y: 28 },
'start': {x: 239, y: 174},
'end': {x: 426, y: 157},
'iceland': {x: 322, y: 150},
};
var expectedEventsGeoscatter = {
'in-margin': false,
'start': {
curveNumber: 0,
pointIndex: 0,
pointNumber: 0,
lat: 40.7127,
lon: -74.0059,
},
'end': {
curveNumber: 0,
pointIndex: 1,
pointNumber: 1,
lat: 51.5072,
lon: 51.5072,
},
'iceland': {lat: -18.666562962962963, lon: 56.66635185185185},
};

var gd;

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

afterEach(function() {
resetEvents(gd);
destroyGraphDiv();
});

function plotMock14Anywhere(layoutOpts) {
var mock = require('@mocks/14.json');
var defaultLayoutOpts = {
layout: {
clickmode: 'event+anywhere',
hoverdistance: 1
}
};
var mockCopy = Lib.extendDeep(
{},
mock,
defaultLayoutOpts,
{ layout: layoutOpts });

return Plotly.newPlot(gd, mockCopy.data, mockCopy.layout);
}

function plotMock14AnywhereSelect(layoutOpts) {
var mock = require('@mocks/14.json');
var defaultLayoutOpts = {
layout: {
clickmode: 'select+event+anywhere',
hoverdistance: 1
}
};
var mockCopy = Lib.extendDeep(
{},
mock,
defaultLayoutOpts,
{ layout: layoutOpts });

return Plotly.newPlot(gd, mockCopy.data, mockCopy.layout);
}

function plotGeoscatterAnywhere() {
var layout = {
clickmode: 'event+anywhere',
hoverdistance: 1
};
var data = [{
type: 'scattergeo',
lat: [ mockPtsGeoscatter.start.lat, mockPtsGeoscatter.end.lat ],
lon: [ mockPtsGeoscatter.start.lon, mockPtsGeoscatter.end.lat ],
mode: 'lines',
line: {
width: 2,
color: 'blue'
}
}];
return Plotly.newPlot(gd, data, layout);
}

function isSubset(superObj, subObj) {
return superObj === subObj ||
typeof superObj === 'object' &&
typeof subObj === 'object' && (
subObj.valueOf() === superObj.valueOf() ||
Object.keys(subObj).every(function(k) { return isSubset(superObj[k], subObj[k]); })
);
}

/**
* Executes a click and before resets event handlers.
* Returns the `clickedPromise` for convenience.
*/
function _click(x, y, clickOpts) {
resetEvents(gd);
setTimeout(function() {
click(x, y, clickOpts);
}, DBLCLICKDELAY * 1.03);
return clickedPromise;
}

function clickAndTestPoint(mockPts, expectedEvents, pointKey, clickOpts) {
var x = mockPts[pointKey].x;
var y = mockPts[pointKey].y;
var expectedEvent = expectedEvents[pointKey];
var result = _click(x, y, clickOpts);
if(expectedEvent) {
result.then(function() {
expect(isSubset(clickEvent, expectedEvent)).toBe(true);
});
} else {
expect(clickEvent).toBe(null);
result = null;
}
return result;
}

it('selects point and/or coordinate when clicked - scatter - event+anywhere', function(done) {
plotMock14Anywhere()
.then(function() { return clickAndTestPoint(mock14PtsScatter, expectedEventsScatter, 'in-margin'); })
.then(function() { return clickAndTestPoint(mock14PtsScatter, expectedEventsScatter, 'point-0'); })
.then(function() { return clickAndTestPoint(mock14PtsScatter, expectedEventsScatter, 'between-point-0-and-1'); })
.then(function() { return clickAndTestPoint(mock14PtsScatter, expectedEventsScatter, 'point-11'); })
.then(done, done.fail);
});

it('selects point and/or coordinate when clicked - scatter - select+event+anywhere', function(done) {
plotMock14AnywhereSelect()
.then(function() { return clickAndTestPoint(mock14PtsScatter, expectedEventsScatter, 'in-margin'); })
.then(function() { return clickAndTestPoint(mock14PtsScatter, expectedEventsScatter, 'point-0'); })
.then(function() { return clickAndTestPoint(mock14PtsScatter, expectedEventsScatter, 'between-point-0-and-1'); })
.then(function() { return clickAndTestPoint(mock14PtsScatter, expectedEventsScatter, 'point-11'); })
.then(done, done.fail);
});

it('selects point and/or coordinate when clicked - geoscatter - event+anywhere', function(done) {
plotGeoscatterAnywhere()
.then(function() { return clickAndTestPoint(mockPtsGeoscatterClick, expectedEventsGeoscatter, 'in-margin'); })
.then(function() { return clickAndTestPoint(mockPtsGeoscatterClick, expectedEventsGeoscatter, 'start'); })
.then(function() { return clickAndTestPoint(mockPtsGeoscatterClick, expectedEventsGeoscatter, 'end'); })
.then(function() { return clickAndTestPoint(mockPtsGeoscatterClick, expectedEventsGeoscatter, 'iceland'); })
.then(done, done.fail);
});
});
5 changes: 3 additions & 2 deletions test/plot-schema.json
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1037,15 +1037,16 @@
]
},
"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.",
"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. The *anywhere* flag extends the *select* flag by allowing to trigger a click event anywhere in the plot. The click event will always include *x* and *y* coordinates and if a data point is below the cursor it will also include information about the data point. When specifying *anywhere* the *select* flag becomes superfluous.",
"dflt": "event",
"editType": "plot",
"extras": [
"none"
],
"flags": [
"event",
"select"
"select",
"anywhere"
],
"valType": "flaglist"
},
Expand Down

AltStyle によって変換されたページ (->オリジナル) /