Skip to content

Commit a95e47b

Browse files
authored
Merge pull request #2163 from plotly/persistent-point-selection-compat-transforms
Persistent point selection with transforms compatibility
2 parents d3cdd6f + 7543dc6 commit a95e47b

File tree

24 files changed

+310
-201
lines changed

24 files changed

+310
-201
lines changed

src/components/fx/helpers.js

+57
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,63 @@ exports.quadrature = function quadrature(dx, dy) {
8585
};
8686
};
8787

88+
/** Fill event data point object for hover and selection.
89+
* Invokes _module.eventData if present.
90+
*
91+
* N.B. note that point 'index' corresponds to input data array index
92+
* whereas 'number' is its post-transform version.
93+
*
94+
* If the hovered/selected pt corresponds to an multiple input points
95+
* (e.g. for histogram and transformed traces), 'pointNumbers` and 'pointIndices'
96+
* are include in the event data.
97+
*
98+
* @param {object} pt
99+
* @param {object} trace
100+
* @param {object} cd
101+
* @return {object}
102+
*/
103+
exports.makeEventData = function makeEventData(pt, trace, cd) {
104+
// hover uses 'index', select uses 'pointNumber'
105+
var pointNumber = 'index' in pt ? pt.index : pt.pointNumber;
106+
107+
var out = {
108+
data: trace._input,
109+
fullData: trace,
110+
curveNumber: trace.index,
111+
pointNumber: pointNumber
112+
};
113+
114+
if(trace._indexToPoints) {
115+
var pointIndices = trace._indexToPoints[pointNumber];
116+
117+
if(pointIndices.length === 1) {
118+
out.pointIndex = pointIndices[0];
119+
} else {
120+
out.pointIndices = pointIndices;
121+
}
122+
} else {
123+
out.pointIndex = pointNumber;
124+
}
125+
126+
if(trace._module.eventData) {
127+
out = trace._module.eventData(out, pt, trace, cd, pointNumber);
128+
} else {
129+
if('xVal' in pt) out.x = pt.xVal;
130+
else if('x' in pt) out.x = pt.x;
131+
132+
if('yVal' in pt) out.y = pt.yVal;
133+
else if('y' in pt) out.y = pt.y;
134+
135+
if(pt.xa) out.xaxis = pt.xa;
136+
if(pt.ya) out.yaxis = pt.ya;
137+
if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal;
138+
}
139+
140+
exports.appendArrayPointValue(out, trace, pointNumber);
141+
142+
return out;
143+
};
144+
88145
/** Appends values inside array attributes corresponding to given point number
89146
*
90147
* @param {object} pointData : point data object (gets mutated here)

src/components/fx/hover.js

+1-20
Original file line numberDiff line numberDiff line change
@@ -417,26 +417,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
417417
// other people and send it to the event
418418
for(itemnum = 0; itemnum < hoverData.length; itemnum++) {
419419
var pt = hoverData[itemnum];
420-
421-
var out = {
422-
data: pt.trace._input,
423-
fullData: pt.trace,
424-
curveNumber: pt.trace.index,
425-
pointNumber: pt.index
426-
};
427-
428-
if(pt.trace._module.eventData) out = pt.trace._module.eventData(out, pt);
429-
else {
430-
out.x = pt.xVal;
431-
out.y = pt.yVal;
432-
out.xaxis = pt.xa;
433-
out.yaxis = pt.ya;
434-
435-
if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal;
436-
}
437-
438-
helpers.appendArrayPointValue(out, pt.trace, pt.index);
439-
newhoverdata.push(out);
420+
newhoverdata.push(helpers.makeEventData(pt, pt.trace, pt.cd));
440421
}
441422

442423
gd._hoverdata = newhoverdata;

src/lib/index.js

+51
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,57 @@ lib.extractOption = function(calcPt, trace, calcKey, traceKey) {
466466
if(!Array.isArray(traceVal)) return traceVal;
467467
};
468468

469+
/** Tag selected calcdata items
470+
*
471+
* N.B. note that point 'index' corresponds to input data array index
472+
* whereas 'number' is its post-transform version.
473+
*
474+
* @param {array} calcTrace
475+
* @param {object} trace
476+
* - selectedpoints {array}
477+
* - _indexToPoints {object}
478+
* @param {ptNumber2cdIndex} ptNumber2cdIndex (optional)
479+
* optional map object for trace types that do not have 1-to-1 point number to
480+
* calcdata item index correspondence (e.g. histogram)
481+
*/
482+
lib.tagSelected = function(calcTrace, trace, ptNumber2cdIndex) {
483+
var selectedpoints = trace.selectedpoints;
484+
var indexToPoints = trace._indexToPoints;
485+
var ptIndex2ptNumber;
486+
487+
// make pt index-to-number map object, which takes care of transformed traces
488+
if(indexToPoints) {
489+
ptIndex2ptNumber = {};
490+
for(var k in indexToPoints) {
491+
var pts = indexToPoints[k];
492+
for(var j = 0; j < pts.length; j++) {
493+
ptIndex2ptNumber[pts[j]] = k;
494+
}
495+
}
496+
}
497+
498+
function isPtIndexValid(v) {
499+
return isNumeric(v) && v >= 0 && v % 1 === 0;
500+
}
501+
502+
function isCdIndexValid(v) {
503+
return v !== undefined && v < calcTrace.length;
504+
}
505+
506+
for(var i = 0; i < selectedpoints.length; i++) {
507+
var ptIndex = selectedpoints[i];
508+
509+
if(isPtIndexValid(ptIndex)) {
510+
var ptNumber = ptIndex2ptNumber ? ptIndex2ptNumber[ptIndex] : ptIndex;
511+
var cdIndex = ptNumber2cdIndex ? ptNumber2cdIndex[ptNumber] : ptNumber;
512+
513+
if(isCdIndexValid(cdIndex)) {
514+
calcTrace[cdIndex].selected = 1;
515+
}
516+
}
517+
}
518+
};
519+
469520
/** Returns target as set by 'target' transform attribute
470521
*
471522
* @param {object} trace : full trace object

src/plots/cartesian/select.js

+21-21
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var polybool = require('polybooljs');
1313
var polygon = require('../../lib/polygon');
1414
var throttle = require('../../lib/throttle');
1515
var color = require('../../components/color');
16-
var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue;
16+
var makeEventData = require('../../components/fx/helpers').makeEventData;
1717

1818
var axes = require('./axes');
1919
var constants = require('./constants');
@@ -240,9 +240,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
240240
traceSelection = searchInfo.selectPoints(searchInfo, testPoly);
241241
traceSelections.push(traceSelection);
242242

243-
var thisSelection = fillSelectionItem(
244-
traceSelection, searchInfo
245-
);
243+
var thisSelection = fillSelectionItem(traceSelection, searchInfo);
246244
if(selection.length) {
247245
for(var j = 0; j < thisSelection.length; j++) {
248246
selection.push(thisSelection[j]);
@@ -294,30 +292,36 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
294292
};
295293

296294
function updateSelectedState(gd, searchTraces, eventData) {
297-
var i, searchInfo;
295+
var i, searchInfo, trace;
298296

299297
if(eventData) {
300298
var pts = eventData.points || [];
301299

302300
for(i = 0; i < searchTraces.length; i++) {
303-
searchInfo = searchTraces[i];
304-
searchInfo.cd[0].trace.selectedpoints = [];
305-
searchInfo.cd[0].trace._input.selectedpoints = [];
301+
trace = searchTraces[i].cd[0].trace;
302+
trace.selectedpoints = [];
303+
trace._input.selectedpoints = [];
306304
}
307305

308306
for(i = 0; i < pts.length; i++) {
309307
var pt = pts[i];
310-
var ptNumber = pt.pointNumber;
311-
312-
pt.data.selectedpoints.push(ptNumber);
313-
pt.fullData.selectedpoints.push(ptNumber);
308+
var data = pt.data;
309+
var fullData = pt.fullData;
310+
311+
if(pt.pointIndices) {
312+
data.selectedpoints = data.selectedpoints.concat(pt.pointIndices);
313+
fullData.selectedpoints = fullData.selectedpoints.concat(pt.pointIndices);
314+
} else {
315+
data.selectedpoints.push(pt.pointIndex);
316+
fullData.selectedpoints.push(pt.pointIndex);
317+
}
314318
}
315319
}
316320
else {
317321
for(i = 0; i < searchTraces.length; i++) {
318-
searchInfo = searchTraces[i];
319-
delete searchInfo.cd[0].trace.selectedpoints;
320-
delete searchInfo.cd[0].trace._input.selectedpoints;
322+
trace = searchTraces[i].cd[0].trace;
323+
delete trace.selectedpoints;
324+
delete trace._input.selectedpoints;
321325
}
322326
}
323327

@@ -355,15 +359,11 @@ function mergePolygons(list, poly, subtract) {
355359

356360
function fillSelectionItem(selection, searchInfo) {
357361
if(Array.isArray(selection)) {
362+
var cd = searchInfo.cd;
358363
var trace = searchInfo.cd[0].trace;
359364

360365
for(var i = 0; i < selection.length; i++) {
361-
var sel = selection[i];
362-
363-
sel.curveNumber = trace.index;
364-
sel.data = trace._input;
365-
sel.fullData = trace;
366-
appendArrayPointValue(sel, trace, sel.pointNumber);
366+
selection[i] = makeEventData(selection[i], trace, cd);
367367
}
368368
}
369369

src/plots/plots.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -2241,7 +2241,21 @@ plots.doCalcdata = function(gd, traces) {
22412241

22422242
if(trace.visible === true) {
22432243
_module = trace._module;
2244-
if(_module && _module.calc) cd = _module.calc(gd, trace);
2244+
2245+
// keep ref of index-to-points map object of the *last* enabled transform,
2246+
// this index-to-points map object is required to determine the calcdata indices
2247+
// that correspond to input indices (e.g. from 'selectedpoints')
2248+
var transforms = trace.transforms || [];
2249+
for(j = transforms.length - 1; j >= 0; j--) {
2250+
if(transforms[j].enabled) {
2251+
trace._indexToPoints = transforms[j]._indexToPoints;
2252+
break;
2253+
}
2254+
}
2255+
2256+
if(_module && _module.calc) {
2257+
cd = _module.calc(gd, trace);
2258+
}
22452259
}
22462260

22472261
// Make sure there is a first point.

src/traces/box/calc.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ module.exports = function calc(gd, trace) {
114114
}
115115
}
116116

117+
calcSelection(cd, trace);
117118
Axes.expand(valAxis, val, {padded: true});
118119

119120
if(cd.length > 0) {
@@ -193,13 +194,13 @@ function arraysToCalcdata(pt, trace, i) {
193194
pt[trace2calc[k]] = trace[k][i];
194195
}
195196
}
197+
}
196198

197-
var selectedpoints = trace.selectedpoints;
198-
199-
// TODO this is slow
200-
if(Array.isArray(selectedpoints)) {
201-
if(selectedpoints.indexOf(pt.i) !== -1) {
202-
pt.selected = 1;
199+
function calcSelection(cd, trace) {
200+
if(Array.isArray(trace.selectedpoints)) {
201+
for(var i = 0; i < cd.length; i++) {
202+
var pts = cd[i].pts || [];
203+
Lib.tagSelected(pts, trace);
203204
}
204205
}
205206
}

src/traces/histogram/calc.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ var Lib = require('../../lib');
1515
var Axes = require('../../plots/cartesian/axes');
1616

1717
var arraysToCalcdata = require('../bar/arrays_to_calcdata');
18-
var calcSelection = require('../scatter/calc_selection');
1918
var binFunctions = require('./bin_functions');
2019
var normFunctions = require('./norm_functions');
2120
var doAvg = require('./average');
2221
var cleanBins = require('./clean_bins');
2322
var oneMonth = require('../../constants/numerical').ONEAVGMONTH;
2423
var getBinSpanLabelRound = require('./bin_label_vals');
2524

26-
2725
module.exports = function calc(gd, trace) {
2826
// ignore as much processing as possible (and including in autorange) if bar is not visible
2927
if(trace.visible !== true) return;
@@ -114,11 +112,13 @@ module.exports = function calc(gd, trace) {
114112
};
115113
}
116114

115+
// bin the data
116+
// and make histogram-specific pt-number-to-cd-index map object
117117
var nMax = size.length;
118118
var uniqueValsPerBin = true;
119119
var leftGap = Infinity;
120120
var rightGap = Infinity;
121-
// bin the data
121+
var ptNumber2cdIndex = {};
122122
for(i = 0; i < pos0.length; i++) {
123123
var posi = pos0[i];
124124
n = Lib.findBin(posi, bins);
@@ -128,6 +128,7 @@ module.exports = function calc(gd, trace) {
128128
uniqueValsPerBin = false;
129129
}
130130
inputPoints[n].push(i);
131+
ptNumber2cdIndex[i] = n;
131132

132133
leftGap = Math.min(leftGap, posi - binEdges[n]);
133134
rightGap = Math.min(rightGap, binEdges[n + 1] - posi);
@@ -197,7 +198,10 @@ module.exports = function calc(gd, trace) {
197198
}
198199

199200
arraysToCalcdata(cd, trace);
200-
calcSelection(cd, trace);
201+
202+
if(Array.isArray(trace.selectedpoints)) {
203+
Lib.tagSelected(cd, trace, ptNumber2cdIndex);
204+
}
201205

202206
return cd;
203207
};

src/traces/histogram/event_data.js

+27-12
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,38 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
109
'use strict';
1110

12-
13-
module.exports = function eventData(out, pt) {
11+
module.exports = function eventData(out, pt, trace, cd, pointNumber) {
1412
// standard cartesian event data
15-
out.x = pt.xVal;
16-
out.y = pt.yVal;
17-
out.xaxis = pt.xa;
18-
out.yaxis = pt.ya;
19-
20-
// specific to histogram
21-
// CDFs do not have pts (yet?)
22-
if(pt.pts) {
23-
out.pointNumbers = pt.pts;
13+
out.x = 'xVal' in pt ? pt.xVal : pt.x;
14+
out.y = 'yVal' in pt ? pt.yVal : pt.y;
15+
16+
if(pt.xa) out.xaxis = pt.xa;
17+
if(pt.ya) out.yaxis = pt.ya;
18+
19+
// specific to histogram - CDFs do not have pts (yet?)
20+
if(!(trace.cumulative || {}).enabled) {
21+
var pts = Array.isArray(pointNumber) ?
22+
cd[0].pts[pointNumber[0]][pointNumber[1]] :
23+
cd[pointNumber].pts;
24+
25+
out.pointNumbers = pts;
2426
out.binNumber = out.pointNumber;
2527
delete out.pointNumber;
28+
delete out.pointIndex;
29+
30+
var pointIndices;
31+
if(trace._indexToPoints) {
32+
pointIndices = [];
33+
for(var i = 0; i < pts.length; i++) {
34+
pointIndices = pointIndices.concat(trace._indexToPoints[pts[i]]);
35+
}
36+
} else {
37+
pointIndices = pts;
38+
}
39+
40+
out.pointIndices = pointIndices;
2641
}
2742

2843
return out;

src/traces/histogram/hover.js

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
2525
var posLetter = trace.orientation === 'h' ? 'y' : 'x';
2626

2727
pointData[posLetter + 'Label'] = hoverLabelText(pointData[posLetter + 'a'], di.p0, di.p1);
28-
pointData.pts = di.pts;
2928
}
3029

3130
return pts;

0 commit comments

Comments
 (0)