Skip to content
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

Persistent point selection #2135

Merged
merged 46 commits into from
Nov 20, 2017
Merged

Persistent point selection #2135

merged 46 commits into from
Nov 20, 2017

Conversation

rreusser
Copy link
Contributor

This PR addresses #1848. There is useful code, but it's not yet fully functional. In particular the things that need doing:

  1. invoke plot to make the draw updates happen. I think the best thing, at least for scatter, is to piggyback on the transition capability that bypasses a full replot. Many properties can be handled via that pathway for scatter. For non-scatter traces, I'm not 100% certain what the right solution is yet. We might need module.drawSelect to mutate the DOM colors/opacities/etc directly instead of calling a full replot and in order to keep, say, bar drawing logic inside the bar trace (as opposed to worrying about drawing bars within the cartesian plot select code)
  2. Make selectedids work the same as selectepoints. Right now that's partially omitted.

/cc @etpinard

@alexcjohnson
Copy link
Collaborator

scatter and some other types have a style method, and a corresponding editType: 'style' that gets picked up by restyle to short-circuit a full redraw. I'd think it would be pretty good performance just to use that pathway - and that would make it easy to put deselected points back to the normal style.

@rreusser
Copy link
Contributor Author

@alexcjohnson — @etpinard and I talked a bit about this and decided the best option for a start is to style them directly. I was concerned we'd lose out on a lot of properties, but if we start with just opacity and color, then direct styling (as opposed to a full plot pass) seems best.

@etpinard
Copy link
Contributor

etpinard commented Oct 31, 2017

Here's what I come up with:

https://github.com/plotly/plotly.js/compare/persistent-point-selection-etienne

  • selectPoints long longer update DOM nodes. It now only calls polygon.contains and updates the selected calcdata accordingly
  • DOM node updates are now in the style method
  • Selection are made persistent via @rreusser 's updatedSelectedState in cartesian/select.js and scatter/apply_selection_to_calcdata.js where the selected calcdata fields are filled in.
  • Note that I made Scatter.style accept single-trace selection (as opposed to graph-wide only) for 🐎

@etpinard etpinard added this to the v1.32.0 milestone Oct 31, 2017
@rreusser
Copy link
Contributor Author

@etpinard Feel free to file a separate PR or just seize control of this PR.

- namely, hoveredpoints,
  marker.size and marker.lin selected/unselected attributes.
- and coerce if module's `selectPoints` method exists
- don't dive into `selectedpoints` array (leave that for calc, later).
- make distinction between
  selectedpoints: [] and selectedpoint: undefined stats,
  where the former mean all pts are unselected and the latter means
  no selection at all.
- use trace module style method to update DOM nodes
  (or call relevent gl update methods for gl-based trace types)
- add *new* call signature for style methods to style per-trace
  inside of per-graph (as not all traces in one graph are selected
  in general).
- coerce selected/unselected marker.opacity, marker.color and
  textfont.color
- add Drawing.selectedPointStyle and Drawing.selectedTextStyle
  to update DOM nodes when `selectedpoints` exists
- replace apply_selection_to_calcdata by calc_selection
- add support for per-trace Scatter.style call signature
- rm DOM styling in Scatter.selectPoints
- ... by simply propagation 'scatter' logic.
- coerce only 'marker.opacity' and 'marker.color',
  (textfont.color isn't a thing yet for these trace types).
- in which we needed to expose a 'style' method.
- note that geo trace cannot rely on the the style method loop
  in Plots.style because it needs to call style in the
  fetch-topojson callback.
- so ScatterGeo.style only work per-trace!
- in which a new style per-trace-only style() method is added
  (similar to 'scattergeo')  AND a new attribute: 'marker.opacity'
  to make selection behavior be attribute-describable.
- by doing so, we can use Drawing.pointStyle straight-up!
- note that Drawing.font needs to be called in Bar.plot to
  find out if the text label fit inside the bars.
- support only 'marker.opacity' until we bump mapbox-gl
@etpinard
Copy link
Contributor

etpinard commented Nov 2, 2017

Ok, tagging this as status: reviewable, even though test coverage is limited and some tests are failing on CI. I'd recommend reviewing this thing commit-by-commit. I've put down some addition info in each commit message.

peek 2017-11-02 15-36

In brief, much of the work here was put into replacing the DOM node styling calls in the trace module selectPoints methods by selected-and -unselected-attribute-dependent node updates in the trace module style methods - which are now called in every plotly_selecting interactions as well as on plotly_deselect.

For most trace types, marker.opacity, marker.color and textfont.color have now selected and unselected flavours. marker.opacity and textfont.color mimic the currently dimming plotly_selecting updates, while marker.color adds the ability to highlight points using an arbitrary color (which should make @monfera 😃 )

I've use selectedpoints as the attribute which list the indices of the coordinates in gd.data that are selected. Perhaps, selectedindices would be better. Thoughts? Note that selectedpoints: [] means an empty selection (i.e. all pts are styled using the unselected attributes) and selectedpoints: undefined means no selection at all (i.e. all pts are styled using the normal trace-level attributes). Oh and I should add: Plotly.restyle(gd, 'selectedpoints', [[/* new selected indiced */]]) just worksTM.

In #1848 (comment), there was also talks about adding a selectedids attributes which would look into the traces ids array to determine the selected indices. I was thinking about skipping this part in this PR, unless someone really wants it included.

cc @monfera @dfcreative @alexcjohnson @bpostlethwaite @cpsievert


// 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.

@etpinard
Copy link
Contributor

Ok, I think I'm now happy with this PR functionality-wise 🚀 as well as test-coverage-wise 🔒

There's a lot of things going on here, so let me recap what happened since #2135 (comment):

@monfera
Copy link
Contributor

monfera commented Nov 17, 2017

I looked into this grand PR two weeks ago and a lot of improvements were put in place since, and it's the single biggest step toward fully representing plot state, I'm voting for 💃

Caution: we shouldn't immediately use it in streambed as the crossfilter restyling needs to be updated. Currently, resetting the crosssfilter filter on a plot will still leave it with deemphasized points, I guess because of the permanence of the selection:

permanent

So, putting it in streambed immediately wouldn't be good for crossfilter. cc @jackparmer

@jackparmer
Copy link
Contributor

@etpinard Let's keep this off streambed for a few weeks if possible

- now works for unsorted sample arrays.
ptNumber2cdIndex[pts[j].i] = j;
}

Lib.tagSelected(pts, trace, ptNumber2cdIndex);
Copy link
Contributor

Choose a reason for hiding this comment

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

Quick fixup. I noticed that selectedpoints -> calcdata was off for box and violins while making

peek 2017-11-17 14-26

var gen = v => Math.round(Math.random(v) * 10)
var n = 100
var x = new Array(n).fill(0).map(gen)
var y = new Array(n).fill(0).map(gen)
var c = '#984ea3'
var usmo = 0

Plotly.newPlot(gd, [{
  mode: 'markers',
  x: x,
  y: y,
  marker: {color: c},
  unselected: { marker: {opacity: usmo} }
}, {
  type: 'violin',
  name: 'x',
  x: x,
  points: 'all',
  pointpos: 1.5,
  box: {visible: true},
  yaxis: 'y2',
  marker: {color: c},
  unselected: { marker: {opacity: usmo} }
}, {
  type: 'violin',
  name: 'y',
  y: y,
  points: 'all',
  pointpos: 1.5,
  box: {visible: true},
  xaxis: 'x2',
  marker: {color: c},
  unselected: { marker: {opacity: usmo} }
}], {
  xaxis: {
    domain: [0, 0.48]
  },
  yaxis: {
    domain: [0, 0.48]
  },
  xaxis2: {
    domain: [0.52, 1]
  },
  yaxis2: {
    domain: [0.52, 1]
  },
  showlegend: false,
  dragmode: 'lasso',
  hovermode: 'closest',
  width: 500,
  height: 500
})
gd.on('plotly_selected', d => {
  if(d && d.points && d.points.length) {
    var sel = d.points.map(p => p.pointIndex)
    Plotly.restyle(gd, 'selectedpoints', [sel.slice(), sel.slice(), sel.slice()])
  } else {
    Plotly.restyle(gd, 'selectedpoints', null)
  }
})

@jackparmer
Copy link
Contributor

One question that came up in the workshop is whether this will work with animations. E.g., will points selected with the persistent selections API stay selected through frame transitions?

@etpinard
Copy link
Contributor

will points selected with the persistent selections API stay selected through frame transitions?

yes

@alexcjohnson
Copy link
Collaborator

Tons of great stuff here, nice job! 💃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants