Skip to content

Persistent point selection #2135

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 46 commits into from
Nov 20, 2017
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8b60b4e
Mirror point selection in graph data
rreusser Oct 24, 2017
b209a55
Partial stateful select
rreusser Oct 25, 2017
2a04371
Merge branch 'master' into persistent-point-selection
etpinard Nov 2, 2017
6903d31
remove we won't do in this PR
etpinard Nov 2, 2017
3ef323f
mv 'selectedpoints' & 'selectedids' to global trace attrs/defaults
etpinard Nov 2, 2017
1efe144
improve updateSelectedState
etpinard Nov 2, 2017
9f3f58d
introduce new on-select update strategy:
etpinard Nov 2, 2017
782bc66
persistent selections for 'scatter' traces
etpinard Nov 2, 2017
ec0578b
persistent selection for 'scattercarpet' and 'scatteternary'
etpinard Nov 2, 2017
1bd832e
persistent selections for 'box' and 'violin' traces
etpinard Nov 2, 2017
10c5b7b
persistent selection for 'scattergeo'
etpinard Nov 2, 2017
eeace5b
persistent selections for 'choropleth'
etpinard Nov 2, 2017
bf039ab
resolve #1862 - add `marker.opacity` to bar (& histogram) traces
etpinard Nov 2, 2017
5cfadbd
persistent selection for 'bar' & 'histogram' traces
etpinard Nov 2, 2017
f54a08a
persistent selections for 'scattermapbox'
etpinard Nov 2, 2017
d4d1728
don't coerce selected/unselected in scatter3d traces
etpinard Nov 2, 2017
eeaca74
do not implement persistent selections in scattergl traces (for now)
etpinard Nov 2, 2017
5ca8b36
improve point-selection mock
etpinard Nov 2, 2017
4e83ef7
fixup bar/histogram tests
etpinard Nov 2, 2017
67cefad
fixup bar path selector
etpinard Nov 3, 2017
10a2fa9
add selected.marker.size support
etpinard Nov 3, 2017
2fcdb7c
Merge branch 'master' into persistent-point-selection
etpinard Nov 13, 2017
54cc67b
make hover AND select use same event data pipeline
etpinard Nov 15, 2017
638d03d
adapt Histogram.eventData so that it works with selection pts
etpinard Nov 15, 2017
b30fab4
add Scattercarpet.eventData method
etpinard Nov 15, 2017
949b311
add ScatterTernary.eventData method
etpinard Nov 15, 2017
315e632
add Lib.tagSelected made to make selectedpoints work w/ transforms
etpinard Nov 15, 2017
9b79b3f
add support for selection of aggregations
etpinard Nov 15, 2017
9210278
update point-selection baseline
etpinard Nov 15, 2017
261387d
update event-data jasmine tests (mostly adding pointIndex)
etpinard Nov 15, 2017
1859256
add Lib.coerceSelectionMarkerOpacity
etpinard Nov 15, 2017
d3cdd6f
update Drawing.selectedPointStyle
etpinard Nov 16, 2017
451778b
speed up is-pt-index-valid check
etpinard Nov 16, 2017
c8d715b
emit 'normalized' a/b/c coords in scatterternary event data
etpinard Nov 16, 2017
7543dc6
move ptNumber2cdIndex computation to main binning loop
etpinard Nov 16, 2017
a95e47b
Merge pull request #2163 from plotly/persistent-point-selection-compa…
etpinard Nov 16, 2017
9d86a2f
:hocho: selectedids (for now)
etpinard Nov 16, 2017
6437081
add delay in bar select test (to pass on CI)
etpinard Nov 16, 2017
9bbf55b
assert gd.data[i].selectedpoints in per-trace-type select tests
etpinard Nov 16, 2017
9ccccd3
(fixup) Lib.coerceSelectionMarkerOpacity for all selectable traces
etpinard Nov 17, 2017
3d2049c
:lock: down [un]selected styles in mocks
etpinard Nov 17, 2017
a7f06fa
(fixup) rm selectedpoints from scattercarpet for select test
etpinard Nov 17, 2017
80eee22
add selections persist test
etpinard Nov 17, 2017
aff9dbf
fixup selectedpoints -> calcdata for box/violin
etpinard Nov 17, 2017
ab7b8f1
update baseline (box/violin selectedpoints ordering)
etpinard Nov 17, 2017
88fb812
aj-proof [un]selected logic
etpinard Nov 20, 2017
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
65 changes: 59 additions & 6 deletions src/components/drawing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var svgTextUtils = require('../../lib/svg_text_utils');
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
var alignment = require('../../constants/alignment');
var LINE_SPACING = alignment.LINE_SPACING;
var DESELECTDIM = require('../../constants/interactions').DESELECTDIM;

var subTypes = require('../../traces/scatter/subtypes');
var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func');
Expand Down Expand Up @@ -248,8 +249,6 @@ drawing.symbolNumber = function(v) {
};

function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine, gd) {
// only scatter & box plots get marker path and opacity
// bars, histograms don't
if(Registry.traceIs(trace, 'symbols')) {
var sizeFn = makeBubbleSizeFn(trace);

Expand All @@ -276,12 +275,13 @@ function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerL

return drawing.symbolFuncs[xBase](r) +
(x >= 200 ? DOTPATH : '');
})
.style('opacity', function(d) {
return (d.mo + 1 || marker.opacity + 1) - 1;
});
}

sel.style('opacity', function(d) {
return (d.mo + 1 || marker.opacity + 1) - 1;
});

var perPointGradient = false;

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

singlePointStyle(d, sel, trace, markerScale, lineScale, marker, marker.line, gd);

};

drawing.pointStyle = function(s, trace, gd) {
Expand All @@ -426,6 +425,35 @@ drawing.pointStyle = function(s, trace, gd) {
});
};

drawing.selectedPointStyle = function(s, trace) {
if(!s.size() || !trace.selectedpoints) return;

var selectedAttrs = trace.selected || {};
var unselectedAttrs = trace.unselected || {};

s.style('opacity', function(d) {
return d.selected ?
(selectedAttrs.marker || {}).opacity :
(unselectedAttrs.marker || {}).opacity;
});

// which is slightly different than:
// ((d.mo + 1 || opacity + 1) - 1) * (d.dim ? DESELECTDIM : 1);
// in https://github.com/plotly/plotly.js/blob/master/src/traces/scatter/select.js
Copy link
Collaborator

Choose a reason for hiding this comment

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

This means that if marker opacity is an array, you won't be able to keep that during selection. I feel like we can do better than that:

  • Only give [un]selected.marker.opacity a default value if you don't set any other [un]selected attributes - so if you want to keep opacity intact but just change marker color during selection, you can do that.
  • If only one of the [un]selected opacities is set, the other state should revert to the arrayOk logic (d.mo + 1 || marker.opacity + 1) - 1
  • The same is true for colors: if only one is used and the base attribute is an array, the other state should revert to the arrayOk logic - perhaps we want to stash this color in d though, as this logic is a bit more involved? Anyway, that would allow selections like in parcoords (deselected is gray, selected keeps its color) or like our S&P notifiers (deselected keeps its color, selected is bright red)
  • And for both opacity and color, if neither is set you don't even need to do that loop.
  • Another minor 🐎 - this will probably naturally happen with the above, but you can move (selectedAttrs.marker || {}).opacity calculation out of the .each loop - also smc and usmc (Semper Fi!) below. That said I'd love it if some day these attributes all become arrayOk in which case they'd have to move at least partially back into the loop... But that's not for this PR, single-value is great for now.
  • My suggestions may make a mess with gradients... perhaps for now if you have a gradient and only one selection color we just revert to uniform marker.color for the other state?

Copy link
Contributor

Choose a reason for hiding this comment

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

Only give [un]selected.marker.opacity a default value if you don't set any other [un]selected attributes - so if you want to keep opacity intact but just change marker color during selection, you can do that.

But then [un]selected.marker.opacity won't appear in _fullData, and @monfera 😏 won't be able to populate his style panels. Personally, I think keeping the same default selection behavior is more important, so thanks @alexcjohnson for pointing this out!

My suggestions may make a mess with gradients... perhaps for now if you have a gradient and only one selection color we just revert to uniform marker.color for the other state?

Good point here. I haven't tested any of the mocks with marker.gradient turned on.

Copy link
Contributor

Choose a reason for hiding this comment

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

Revised logic in 1859256 and d3cdd6f with tests.


s.each(function(d) {
var pt = d3.select(this);
var smc = (selectedAttrs.marker || {}).color;
var usmc = (unselectedAttrs.marker || {}).color;

if(d.selected) {
if(smc) Color.fill(pt, smc);
} else {
if(usmc) Color.fill(pt, usmc);
}
});
};

drawing.tryColorscale = function(marker, prefix) {
var cont = prefix ? Lib.nestedProperty(marker, prefix).get() : marker,
scl = cont.colorscale,
Expand Down Expand Up @@ -483,6 +511,31 @@ drawing.textPointStyle = function(s, trace, gd) {
});
};

drawing.selectedTextStyle = function(s, trace) {
if(!s.size() || !trace.selectedpoints) return;

var selectedAttrs = trace.selected || {};
var unselectedAttrs = trace.unselected || {};

s.each(function(d) {
var tx = d3.select(this);
var tc = d.tc || trace.textfont.color;
var stc = (selectedAttrs.textfont || {}).color;
var utc = (unselectedAttrs.textfont || {}).color;
var tc2;

if(d.selected) {
if(stc) tc2 = stc;
else tc2 = Color.addOpacity(tc, 1);
} else {
if(utc) tc2 = utc;
else tc2 = Color.addOpacity(tc, DESELECTDIM);
}

Color.fill(tx, tc2);
});
};

// generalized Catmull-Rom splines, per
// http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
var CatmullRomExp = 0.5;
Expand Down
29 changes: 29 additions & 0 deletions src/plots/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,35 @@ module.exports = {
'DOM elements'
].join(' ')
},

// N.B. these cannot be 'data_array' as they do not have the same length as
// other data arrays and arrayOk attributes in general
//
// TODO maybe add another valType:
// https://github.com/plotly/plotly.js/issues/1894
selectedpoints: {
valType: 'any',
role: 'info',
editType: 'calc',
description: [
'Array containing integer indices of selected points.',
'Has an effect only for traces that support selections.',
'Note that an empty array means an empty selection where the `unselected`',
'are turned on for all points, whereas, any other non-array values means no',
'selection all where the `selected` and `unselected` styles have no effect.'
].join(' ')
},
selectedids: {
valType: 'any',
role: 'info',
editType: 'calc',
description: [
'Array containing `ids` of selected points.',
'Has an effect only for traces that support selections.',
'...'
].join(' ')
},

hoverinfo: {
valType: 'flaglist',
role: 'info',
Expand Down
45 changes: 42 additions & 3 deletions src/plots/cartesian/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ var MINSELECT = constants.MINSELECT;
function getAxId(ax) { return ax._id; }

module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
var zoomLayer = dragOptions.gd._fullLayout._zoomlayer,
var gd = dragOptions.gd,
fullLayout = gd._fullLayout,
zoomLayer = fullLayout._zoomlayer,
dragBBox = dragOptions.element.getBoundingClientRect(),
plotinfo = dragOptions.plotinfo,
xs = plotinfo.xaxis._offset,
Expand Down Expand Up @@ -66,8 +68,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {

// find the traces to search for selection points
var searchTraces = [];
var gd = dragOptions.gd;
var throttleID = gd._fullLayout._uid + constants.SELECTID;
var throttleID = fullLayout._uid + constants.SELECTID;
var selection = [];
var i, cd, trace, searchInfo, eventData;

Expand All @@ -83,6 +84,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
) {
searchTraces.push({
selectPoints: trace._module.selectPoints,
style: trace._module.style,
cd: cd,
xaxis: dragOptions.xaxes[0],
yaxis: dragOptions.yaxes[0]
Expand All @@ -94,6 +96,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {

searchTraces.push({
selectPoints: trace._module.selectPoints,
style: trace._module.style,
cd: cd,
xaxis: axes.getFromId(gd, trace.xaxis),
yaxis: axes.getFromId(gd, trace.yaxis)
Expand Down Expand Up @@ -202,6 +205,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
}

eventData = {points: selection};
updateSelectedState(gd, searchTraces, eventData);
fillRangeItems(eventData, poly, pts);
dragOptions.gd.emit('plotly_selecting', eventData);
}
Expand All @@ -221,6 +225,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
searchInfo.selectPoints(searchInfo, false);
}

updateSelectedState(gd, searchTraces);
gd.emit('plotly_deselect', null);
Copy link
Contributor

@etpinard etpinard Nov 3, 2017

Choose a reason for hiding this comment

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

@alexcjohnson The failing test on CI

image

can be fixed by moving updateSelectedState after gd.emit. I don't think this is desired behavior though. We should emit plotly_selected and plotly_deselect after module.style() has completed.

So, there must a race condition somewhere in select_test.js. Maybe we're missing a Lib.clearThrottle call?

Copy link
Contributor

Choose a reason for hiding this comment

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

Shamefully fixed via 6437081

}
else {
Expand All @@ -230,6 +235,40 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) {
};
};

function updateSelectedState(gd, searchTraces, eventData) {
var i, searchInfo;

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

for(i = 0; i < searchTraces.length; i++) {
searchInfo = searchTraces[i];
searchInfo.cd[0].trace.selectedpoints = [];
searchInfo.cd[0].trace._input.selectedpoints = [];
}

for(i = 0; i < pts.length; i++) {
var pt = pts[i];
var ptNumber = pt.pointNumber;

pt.data.selectedpoints.push(ptNumber);
pt.fullData.selectedpoints.push(ptNumber);
}
}
else {
for(i = 0; i < searchTraces.length; i++) {
searchInfo = searchTraces[i];
delete searchInfo.cd[0].trace.selectedpoints;
delete searchInfo.cd[0].trace._input.selectedpoints;
}
}

for(i = 0; i < searchTraces.length; i++) {
searchInfo = searchTraces[i];
if(searchInfo.style) searchInfo.style(gd, searchInfo.cd);
}
}

function fillSelectionItem(selection, searchInfo) {
if(Array.isArray(selection)) {
var trace = searchInfo.cd[0].trace;
Expand Down
5 changes: 5 additions & 0 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,11 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde
traceOut.visible = !!traceOut.visible;
}

if(_module && _module.selectPoints && traceOut.type !== 'scattergl') {
coerce('selectedpoints');
coerce('selectedids');
}

plots.supplyTransformDefaults(traceIn, traceOut, layout);
}

Expand Down
2 changes: 2 additions & 0 deletions src/traces/bar/arrays_to_calcdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ var mergeArray = require('../../lib').mergeArray;

// arrayOk attributes, merge them into calcdata array
module.exports = function arraysToCalcdata(cd, trace) {
for(var i = 0; i < cd.length; i++) cd[i].i = i;

mergeArray(trace.text, cd, 'tx');
mergeArray(trace.hovertext, cd, 'htx');

Expand Down
16 changes: 14 additions & 2 deletions src/traces/bar/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,19 @@ var marker = extendFlat({
editType: 'calc'
}, colorAttributes('marker'), {
showscale: scatterMarkerAttrs.showscale,
colorbar: colorbarAttrs
colorbar: colorbarAttrs,
opacity: {
valType: 'number',
arrayOk: true,
dflt: 1,
min: 0,
max: 1,
role: 'style',
editType: 'style',
description: 'Sets the opacity of the bars.'
}
});


module.exports = {
x: scatterAttrs.x,
x0: scatterAttrs.x0,
Expand Down Expand Up @@ -150,6 +159,9 @@ module.exports = {

marker: marker,

selected: scatterAttrs.selected,
unselected: scatterAttrs.unselected,

r: scatterAttrs.r,
t: scatterAttrs.t,

Expand Down
4 changes: 2 additions & 2 deletions src/traces/bar/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ var isNumeric = require('fast-isnumeric');
var Axes = require('../../plots/cartesian/axes');
var hasColorscale = require('../../components/colorscale/has_colorscale');
var colorscaleCalc = require('../../components/colorscale/calc');

var arraysToCalcdata = require('./arrays_to_calcdata');

var calcSelection = require('../scatter/calc_selection');

module.exports = function calc(gd, trace) {
// depending on bar direction, set position and size axes
Expand Down Expand Up @@ -92,6 +91,7 @@ module.exports = function calc(gd, trace) {
}

arraysToCalcdata(cd, trace);
calcSelection(cd, trace);

return cd;
};
4 changes: 3 additions & 1 deletion src/traces/bar/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ var handleStyleDefaults = require('../bar/style_defaults');
var errorBarsSupplyDefaults = require('../../components/errorbars/defaults');
var attributes = require('./attributes');


module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
Expand All @@ -44,11 +43,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
var hasBoth = Array.isArray(textPosition) || textPosition === 'auto',
hasInside = hasBoth || textPosition === 'inside',
hasOutside = hasBoth || textPosition === 'outside';

if(hasInside || hasOutside) {
var textFont = coerceFont(coerce, 'textfont', layout.font);
if(hasInside) coerceFont(coerce, 'insidetextfont', textFont);
if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont);
coerce('constraintext');
coerce('selected.textfont.color');
coerce('unselected.textfont.color');
}

handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout);
Expand Down
13 changes: 10 additions & 3 deletions src/traces/bar/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,14 @@ module.exports = function plot(gd, plotinfo, cdbar) {
bartraces.enter().append('g')
.attr('class', 'trace bars');

bartraces.each(function(d) {
d[0].node3 = d3.select(this);
});

bartraces.append('g')
.attr('class', 'points')
.each(function(d) {
var sel = d[0].node3 = d3.select(this);
var sel = d3.select(this);
var t = d[0].t;
var trace = d[0].trace;
var poffset = t.poffset;
Expand Down Expand Up @@ -146,11 +150,13 @@ module.exports = function plot(gd, plotinfo, cdbar) {
};

function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
var textPosition;

function appendTextNode(bar, text, textFont) {
var textSelection = bar.append('text')
.text(text)
.attr({
'class': 'bartext',
'class': 'bartext bartext-' + textPosition,
transform: '',
'text-anchor': 'middle',
// prohibit tex interpretation until we can handle
Expand All @@ -170,7 +176,7 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
var text = getText(trace, i);
if(!text) return;

var textPosition = getTextPosition(trace, i);
textPosition = getTextPosition(trace, i);
if(textPosition === 'none') return;

var textFont = getTextFont(trace, i, gd._fullLayout.font),
Expand Down Expand Up @@ -201,6 +207,7 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
if(textPosition === 'auto') {
if(isOutmostBar) {
// draw text using insideTextFont and check if it fits inside bar
textPosition = 'inside';
textSelection = appendTextNode(bar, text, insideTextFont);

textBB = Drawing.bBox(textSelection.node()),
Expand Down
Loading