Skip to content

Commit 29df012

Browse files
authored
Merge pull request #2135 from plotly/persistent-point-selection
Persistent point selection
2 parents 4c9458b + 88fb812 commit 29df012

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+2135
-576
lines changed

src/components/drawing/index.js

+154-34
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var svgTextUtils = require('../../lib/svg_text_utils');
2222
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
2323
var alignment = require('../../constants/alignment');
2424
var LINE_SPACING = alignment.LINE_SPACING;
25+
var DESELECTDIM = require('../../constants/interactions').DESELECTDIM;
2526

2627
var subTypes = require('../../traces/scatter/subtypes');
2728
var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func');
@@ -247,18 +248,22 @@ drawing.symbolNumber = function(v) {
247248
return Math.floor(Math.max(v, 0));
248249
};
249250

251+
function makePointPath(symbolNumber, r) {
252+
var base = symbolNumber % 100;
253+
return drawing.symbolFuncs[base](r) + (symbolNumber >= 200 ? DOTPATH : '');
254+
}
255+
250256
function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine, gd) {
251-
// only scatter & box plots get marker path and opacity
252-
// bars, histograms don't
253257
if(Registry.traceIs(trace, 'symbols')) {
254258
var sizeFn = makeBubbleSizeFn(trace);
255259

256260
sel.attr('d', function(d) {
257261
var r;
258262

259263
// handle multi-trace graph edit case
260-
if(d.ms === 'various' || marker.size === 'various') r = 3;
261-
else {
264+
if(d.ms === 'various' || marker.size === 'various') {
265+
r = 3;
266+
} else {
262267
r = subTypes.isBubble(trace) ?
263268
sizeFn(d.ms) : (marker.size || 6) / 2;
264269
}
@@ -267,21 +272,20 @@ function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerL
267272
d.mrc = r;
268273

269274
// turn the symbol into a sanitized number
270-
var x = drawing.symbolNumber(d.mx || marker.symbol) || 0,
271-
xBase = x % 100;
275+
var x = drawing.symbolNumber(d.mx || marker.symbol) || 0;
272276

273277
// save if this marker is open
274278
// because that impacts how to handle colors
275279
d.om = x % 200 >= 100;
276280

277-
return drawing.symbolFuncs[xBase](r) +
278-
(x >= 200 ? DOTPATH : '');
279-
})
280-
.style('opacity', function(d) {
281-
return (d.mo + 1 || marker.opacity + 1) - 1;
281+
return makePointPath(x, r);
282282
});
283283
}
284284

285+
sel.style('opacity', function(d) {
286+
return (d.mo + 1 || marker.opacity + 1) - 1;
287+
});
288+
285289
var perPointGradient = false;
286290

287291
// 'so' is suspected outliers, for box plots
@@ -409,7 +413,6 @@ drawing.singlePointStyle = function(d, sel, trace, markerScale, lineScale, gd) {
409413
var marker = trace.marker;
410414

411415
singlePointStyle(d, sel, trace, markerScale, lineScale, marker, marker.line, gd);
412-
413416
};
414417

415418
drawing.pointStyle = function(s, trace, gd) {
@@ -426,6 +429,84 @@ drawing.pointStyle = function(s, trace, gd) {
426429
});
427430
};
428431

432+
drawing.selectedPointStyle = function(s, trace) {
433+
if(!s.size() || !trace.selectedpoints) return;
434+
435+
var selectedAttrs = trace.selected || {};
436+
var unselectedAttrs = trace.unselected || {};
437+
438+
var marker = trace.marker || {};
439+
var selectedMarker = selectedAttrs.marker || {};
440+
var unselectedMarker = unselectedAttrs.marker || {};
441+
442+
var mo = marker.opacity;
443+
var smo = selectedMarker.opacity;
444+
var usmo = unselectedMarker.opacity;
445+
var smoIsDefined = smo !== undefined;
446+
var usmoIsDefined = usmo !== undefined;
447+
448+
s.each(function(d) {
449+
var pt = d3.select(this);
450+
var dmo = d.mo;
451+
var dmoIsDefined = dmo !== undefined;
452+
var mo2;
453+
454+
if(dmoIsDefined || smoIsDefined || usmoIsDefined) {
455+
if(d.selected) {
456+
if(smoIsDefined) mo2 = smo;
457+
} else {
458+
if(usmoIsDefined) mo2 = usmo;
459+
else mo2 = DESELECTDIM * (dmoIsDefined ? dmo : mo);
460+
}
461+
}
462+
463+
if(mo2 !== undefined) pt.style('opacity', mo2);
464+
});
465+
466+
var smc = selectedMarker.color;
467+
var usmc = unselectedMarker.color;
468+
469+
if(smc || usmc) {
470+
s.each(function(d) {
471+
var pt = d3.select(this);
472+
var mc2;
473+
474+
if(d.selected) {
475+
if(smc) mc2 = smc;
476+
} else {
477+
if(usmc) mc2 = usmc;
478+
}
479+
480+
if(mc2) Color.fill(pt, mc2);
481+
});
482+
}
483+
484+
var sms = selectedMarker.size;
485+
var usms = unselectedMarker.size;
486+
var smsIsDefined = sms !== undefined;
487+
var usmsIsDefined = usms !== undefined;
488+
489+
if(Registry.traceIs(trace, 'symbols') && (smsIsDefined || usmsIsDefined)) {
490+
s.each(function(d) {
491+
var pt = d3.select(this);
492+
var mrc = d.mrc;
493+
var mx = d.mx || marker.symbol || 0;
494+
var mrc2;
495+
496+
if(d.selected) {
497+
mrc2 = (smsIsDefined) ? sms / 2 : mrc;
498+
} else {
499+
mrc2 = (usmsIsDefined) ? usms / 2 : mrc;
500+
}
501+
502+
pt.attr('d', makePointPath(drawing.symbolNumber(mx), mrc2));
503+
504+
// save for selectedTextStyle
505+
d.mrc2 = mrc2;
506+
});
507+
}
508+
};
509+
429510
drawing.tryColorscale = function(marker, prefix) {
430511
var cont = prefix ? Lib.nestedProperty(marker, prefix).get() : marker,
431512
scl = cont.colorscale,
@@ -439,8 +520,39 @@ drawing.tryColorscale = function(marker, prefix) {
439520
else return Lib.identity;
440521
};
441522

442-
// draw text at points
443523
var TEXTOFFSETSIGN = {start: 1, end: -1, middle: 0, bottom: 1, top: -1};
524+
525+
function textPointPosition(s, textPosition, fontSize, markerRadius) {
526+
var group = d3.select(s.node().parentNode);
527+
528+
var v = textPosition.indexOf('top') !== -1 ?
529+
'top' :
530+
textPosition.indexOf('bottom') !== -1 ? 'bottom' : 'middle';
531+
var h = textPosition.indexOf('left') !== -1 ?
532+
'end' :
533+
textPosition.indexOf('right') !== -1 ? 'start' : 'middle';
534+
535+
// if markers are shown, offset a little more than
536+
// the nominal marker size
537+
// ie 2/1.6 * nominal, bcs some markers are a bit bigger
538+
var r = markerRadius ? markerRadius / 0.8 + 1 : 0;
539+
540+
var numLines = (svgTextUtils.lineCount(s) - 1) * LINE_SPACING + 1;
541+
var dx = TEXTOFFSETSIGN[h] * r;
542+
var dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r +
543+
(TEXTOFFSETSIGN[v] - 1) * numLines * fontSize / 2;
544+
545+
// fix the overall text group position
546+
s.attr('text-anchor', h);
547+
group.attr('transform', 'translate(' + dx + ',' + dy + ')');
548+
}
549+
550+
function extracTextFontSize(d, trace) {
551+
var fontSize = d.ts || trace.textfont.size;
552+
return (isNumeric(fontSize) && fontSize > 0) ? fontSize : 0;
553+
}
554+
555+
// draw text at points
444556
drawing.textPointStyle = function(s, trace, gd) {
445557
s.each(function(d) {
446558
var p = d3.select(this);
@@ -451,35 +563,43 @@ drawing.textPointStyle = function(s, trace, gd) {
451563
return;
452564
}
453565

454-
var pos = d.tp || trace.textposition,
455-
v = pos.indexOf('top') !== -1 ? 'top' :
456-
pos.indexOf('bottom') !== -1 ? 'bottom' : 'middle',
457-
h = pos.indexOf('left') !== -1 ? 'end' :
458-
pos.indexOf('right') !== -1 ? 'start' : 'middle',
459-
fontSize = d.ts || trace.textfont.size,
460-
// if markers are shown, offset a little more than
461-
// the nominal marker size
462-
// ie 2/1.6 * nominal, bcs some markers are a bit bigger
463-
r = d.mrc ? (d.mrc / 0.8 + 1) : 0;
464-
465-
fontSize = (isNumeric(fontSize) && fontSize > 0) ? fontSize : 0;
566+
var pos = d.tp || trace.textposition;
567+
var fontSize = extracTextFontSize(d, trace);
466568

467569
p.call(drawing.font,
468570
d.tf || trace.textfont.family,
469571
fontSize,
470572
d.tc || trace.textfont.color)
471-
.attr('text-anchor', h)
472573
.text(text)
473-
.call(svgTextUtils.convertToTspans, gd);
574+
.call(svgTextUtils.convertToTspans, gd)
575+
.call(textPointPosition, pos, fontSize, d.mrc);
576+
});
577+
};
474578

475-
var pgroup = d3.select(this.parentNode);
476-
var numLines = (svgTextUtils.lineCount(p) - 1) * LINE_SPACING + 1;
477-
var dx = TEXTOFFSETSIGN[h] * r;
478-
var dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r +
479-
(TEXTOFFSETSIGN[v] - 1) * numLines * fontSize / 2;
579+
drawing.selectedTextStyle = function(s, trace) {
580+
if(!s.size() || !trace.selectedpoints) return;
581+
582+
var selectedAttrs = trace.selected || {};
583+
var unselectedAttrs = trace.unselected || {};
584+
585+
s.each(function(d) {
586+
var tx = d3.select(this);
587+
var tc = d.tc || trace.textfont.color;
588+
var tp = d.tp || trace.textposition;
589+
var fontSize = extracTextFontSize(d, trace);
590+
var stc = (selectedAttrs.textfont || {}).color;
591+
var utc = (unselectedAttrs.textfont || {}).color;
592+
var tc2;
593+
594+
if(d.selected) {
595+
if(stc) tc2 = stc;
596+
} else {
597+
if(utc) tc2 = utc;
598+
else if(!stc) tc2 = Color.addOpacity(tc, DESELECTDIM);
599+
}
480600

481-
// fix the overall text group position
482-
pgroup.attr('transform', 'translate(' + dx + ',' + dy + ')');
601+
if(tc2) Color.fill(tx, tc2);
602+
textPointPosition(tx, tp, fontSize, d.mrc2 || d.mrc);
483603
});
484604
};
485605

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;

0 commit comments

Comments
 (0)