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, stateful hover, click, and selection properties #1848

Closed
chriddyp opened this issue Jul 4, 2017 · 18 comments
Closed

Persistent, stateful hover, click, and selection properties #1848

chriddyp opened this issue Jul 4, 2017 · 18 comments
Labels
feature something new
Milestone

Comments

@chriddyp
Copy link
Member

chriddyp commented Jul 4, 2017

In Dash, the Graph component stores the data from hover, click, and selection events as part of the component's state and passes that to the user as hoverData, clickData, selectionData.

In some cases, the user may want to initialize their app with a point already "clicked", a region "preselected", or a point "hovered".

This is related to "Customized Click, Hover, and Selection Styles". The viewer will know that the graph has been pre-selected, clicked, or hovered through whatever custom styles the developer added.

Ideally, this API would match the same event data. For example, when hovering over a point, the event data might be:

{
    "points": [{"x": 1, "y"; 4}]
}

Ideally, the user would be able to pass that in as part of the figure to trigger those styles themselves. That's how the Dash API currently works although styles are not applied:

Graph(
    figure={...},
    hoverData={"points": [{"x": 1, "y": 4}]}
)

cc @alexcjohnson @etpinard @monfera @jackparmer @charleyferrari

@etpinard
Copy link
Contributor

etpinard commented Jul 4, 2017

So,

Plotly.newPlot('graph', {
  data: [{
    mode: 'markers',
    x: [1, 2, 3],
    y: [2, 1, 2]
  }],
  hoverdata: {
    points: [{x: 1, y: 2}]
  })

would render the graphs and one hover label at (x=1, y=2)?

@etpinard
Copy link
Contributor

etpinard commented Jul 4, 2017

See also #257

@jackparmer
Copy link
Contributor

@rreusser
Copy link
Contributor

rreusser commented Oct 16, 2017

Here's somewhat of a breakdown of how I'm thinking this can exist:

  • hoverpoints and hoverids (selectedpoints + selectedids?) seem like properties that go somewhere. My preference would be for putting them in data itself along with the traces they affect. I don't necessarily see points selected across traces as mutually exclusive or needing to be specified outside the traces themselves. A second choice would be for them to go in layout along with a trace number, but it seems like this could cause the usual trace indexing headaches.
  • these seem like general trace properties that apply to at least many types of traces, if not all. This suggests they should be defined outside particular traces and merged into specific trace attributes where they apply.
  • @etpinard what did we decide was best for the attribute structure for modified properties?
    {
      x: [1, 2, 3],
      marker: {color: 'red'},
      selected: {
        marker: {color: red}
      }
    }
    Alternatively,
    {
      x: [1, 2, 3],
      marker: {
        color: 'red',
        selectedcolor: 'red'
      }
    }
    I think selected* attributes could perhaps be arrayOk attributes since the indexing is no different than for marker.color itself, for example.
  • When you drag and lasso points, I imagine it sets this directly on gd.data without passing it back through Plotly.restyle. That is, it modifies the points directly to style them as desired and modifies the selected point data to match, rather than doing it the indirect way of restyle'ing the plot to accomplish the same. This goes back to the mutation of user data that tends to cause some minor problems, though I think we're finally developing a coherent picture for it with the react component (specifically, it just requires cloning data so that gd.data is an internal copy of user data and no longer user data itself).
  • If you're brushing across plots, the idea is that plotly_hover or plotly_select(?) event data could be plugged into another plot to accomplish the same effect as lassoing data on the plot itself.
  • I haven't decided how brushing across subplots works yet.

@monfera
Copy link
Contributor

monfera commented Oct 17, 2017

The below would be a bit of a rabbit hole to follow now, but maybe worth bringing up now as it might be input to what names and structures you end up with for the new things.

There are names that suggest gesture level events such as hover, click etc. Then there are ones that express some basic intent in the sample space, like filter a contiguous subset (maybe initiated via the lasso or box select) or do some action on a specific element (maybe initiated with a double click). It's hard to refactor for it and doesn't immediately bring new functionality, but there are some benefits when the layers are uncoupled. Examples (don't necessarily apply to current plotly.js needs):

  • allow multiple box or polygon lassoing for subsetting on the same chart, or holes in the selection (might be neat for classification and machine learning related exploration)
  • implement pan & zoom atop of drag and scrollwheel gestures plus essentially a viewMatrix restyle (this is how interactive Vega Lite implements pan&zoom, basically in the user space) - the concepts are separate anyway because a user may want to animate zoom/pan or feed in something eg. in a tour or scrollytelling feature
  • dragmode is a simple reattachment of gestures (dragging a box) to alternative intents e.g zoom vs subset filter

=================================

Other topic: how would subplots work, given current information in the JSON? For example, if (non-overlaid) subplot 1 is on variables A and B, and subplot 2 is on variablea C and D, then there's no solid information for linking the two. Conventions such as 'sample[i] is characterized by a tuple {A[i], B[i}, C[i], D[i]' would be implicit, possibly unexpected and fragile (eg. differing row count). Other conventions, eg. using the same dataset but with different filter, aggregation etc. specs would have other issues. For this reason, sometimes we talked about having a data layer that'd be analogous to, or even replacement for the webapp datatable. This is also something vega does and even our web app chart builder looks that way, you select the columns of interest.

Even if axes among subplots are shared, e.g. subplot 1 has A and B, and subplot 2 has A and C, lassoing in one subplot couldn't be reflected in the other (unless you assume the above index based samples). The only thing it'd allow is, if you retain all values such that A in [aMin, aMax] - ie. full width box selection - you can mirror it in the other subplot.

In case of an identical variable set (either separate location ie. small multiples, or overlaid subplot), any shape would of course work fine, but do we want something to work with a special case only? (No idea, just asking the question.) Also, while it'd technically work in the case of small multiples, would it necessarily make sense for the user? For example, when filtering in one specific panel of a small multiple arrangement, I'd expect only those points to be highlighted, or reflected in a table. On the contrary, in a SPLOM-like arrangement, I'd indeed expect that a selection be reflected in all subplots but it can't be done with current information because the axes are all different.

Also, doing things like this looks like crossfiltering. Currently crossfilter is in the web app in part because the plotly.js JSON expects values per drawn feature rather than a query on a larger datapool. If we go down this route, whatever the solution is in plotly.js may be used in the web app, and code duplication could be avoided.

@etpinard
Copy link
Contributor

Thanks @rreusser for the write-up 📝

hoverpoints and hoverids (selectedpoints + selectedids?)

I'd vote for either hoveredpoints / selectedpoint or hoverpoints / selectionpoints (I think I prefer the latter actually).

My preference would be for putting them in data itself along with the traces they affect.

Yeah, let's put them in their corresponding data traces. That way Plotly.restyle should just workTM - which I believe is a requirement for Dash users cc @chriddyp .

these seem like general trace properties that apply to at least many types of traces, if not all. This suggests they should be defined outside particular traces and merged into specific trace attributes where they apply.

We should declare selectionpoints / selectionids for all trace types that support dragmode lasso/select (you can grep for selectPoints in src/traces/ to find to full list). So yeah, many traces but not all. Similarly hoverpoints / hoverids should be declared for all trace types that support hover - which is almost all traces, I can only think of parcoords that does not have hover labels. To make this DRY, you can add a few attribute files in components/fx/ and require them in the trace module attributes as needed.

@etpinard what did we decide was best for the attribute structure for modified properties?

I prefer

{
  x: [1, 2, 3],
  marker: {color: 'red'},
  selected: {
    marker: {color: red}
  }
}

I think selected* attributes could perhaps be arrayOk attributes since the indexing is no different than for marker.color itself, for example.

Absolutely. arrayOk ftw!

When you drag and lasso points, I imagine it sets this directly on gd.data without passing it back through Plotly.restyle

Yes, I was thinking the same.

If you're brushing across plots, the idea is that plotly_hover or plotly_select(?) event data could be plugged into another plot to accomplish the same effect as lassoing data on the plot itself.
I haven't decided how brushing across subplots works yet.

Hmm. I'm not 100% sure of what you're referring too here. I might be unaware of some requirements. As far as I know, brushing is already doable by using plotly_selected event data, eventData.points.map(p => p.id) should give the ids of the selected points, but I must be missing something.

@rreusser
Copy link
Contributor

Hmm. I'm not 100% sure of what you're referring too here. I might be unaware of some requirements. As far as I know, brushing is already doable by using plotly_selected event data, eventData.points.map(p => p.id) should give the ids of the selected points, but I must be missing something.

I was thinking of where you have two linked subplots with points representing the same data but on two different sets of axes. You lasso one set, and the selection is reflected on both subplots. I don't see a way to get that for free short of feeding selection data back into all subplots.

@etpinard
Copy link
Contributor

Ha I see. Sounds to me we might need (down the road) some sort of shared data structure across traces:

{
  __DATA__: [{
    name: 'DATA',
    columns: [{
      name: 'a',
      values: [1, 2, 3]
    }, {
      name: 'b',
      values: [1, 2, 1]
    }, {
      name: 'c',
      values: [2, 1, 2]
    }],
    selection: [1, 2]
  }],
  // but really, data here should be 'traces'
  data: [{
    x: 'DATA.a',
    y: 'DATA.b'
  }, {
    x: 'DATA.a',
    y: 'DATA.c',
    xaxis: 'x2',
    yaxis: 'y2'
  }]
}

@chriddyp
Copy link
Member Author

My preference would be for putting them in data itself along with the traces they affect.

Yeah, let's put them in their corresponding data traces. That way Plotly.restyle should just workTM - which I believe is a requirement for Dash users cc @chriddyp .

Yeah, this works. The API in dash will probably do things a little bit differently to keep hoverData separate from figure so that users can update one property or listen to one top-level property at time:

Graph(
    figure={...},
    hoverpoints=[
         {"traceindex": 0, "pointindex": 10}
         {"traceindex": 0, "pointindex": 40}
    ]
)

and that way dash users can continue to listen to changes in the hoverpoints but not in the figure:

@app.callback(Output('some-div', 'children'), [Input('my-graph', 'hoverpoints')])
def print_hover_data(hoverpoints):
    ...

but, I can just do this de-nesting in the background with something like:

Plotly.newPlot(id, data, layout).then(() => {
    const transformedHoverPoints = transformHoverIntoTraces(hoverdata);
    Plotly.restyle(id, transformedHoverPoints);
})

and for backwards compatibility, Dash's hoverData will probably start by being the same form as what's provided by the events:

{
  "points": [
    {
      "customdata": "c.b", 
      "pointNumber": 1, 
      "text": "b", 
      "curveNumber": 0, 
      "x": 2, 
      "y": 1
    }, 
    {
      "customdata": "c.x", 
      "pointNumber": 1, 
      "text": "x", 
      "curveNumber": 1, 
      "x": 2, 
      "y": 4
    }
  ]
}

But again, I'm pretty sure that I can handle the conversion from hoverData to traces.hoverpoints appropriately so long as Plotly.newPlot and Plotly.restyle both work with hoverpoints structure.

@chriddyp
Copy link
Member Author

What is hoverids? Are these supposed to be used by the end-user? Right now Dash users use customdata for the purposes of "ids" (mostly used for row IDs when building crossfiltering applications across multiple plots).

@chriddyp
Copy link
Member Author

chriddyp commented Oct 18, 2017

And finally, just be clear, this API will look something like:

{
  x: [10, 20, 30, 40, 50],
  customdata: ['id-1', 'id-2', 'id-3', 'id-4', 'id-5'],
  ids: ['id-1', 'id-2', 'id-3', 'id-4', 'id-5'], // can these be user defined?
  marker: {size: 10},

  selected: {
    marker: {color: 'blue'}
  },
  unselected: {
    marker: {opacity: 0.5}
  },
  selectedpoints: {
    x: [30, 50],
    ids: ['id-3', 'id-5'],
    customdata: ['id-3', 'id-5'],
    mode: 'markers+text'
  },

  hovered: {
    marker: {line: {color: 'lightgrey'}}
  }
  hoveredpoints: {
    x: [10, 40],
    ids: ['id-1', 'id-4']
    customdata: ['id-1', 'id-4']
  }
}

where the selected, hovered and unselected styles would recursively merge into the base styles, resulting in e.g. {marker: {size: 10, color: blue}} for selected?

@etpinard
Copy link
Contributor

What is hoverids? Are these supposed to be used by the end-user?

hoverids will be the values in the trace's ids array corresponding the selected points.

Right now Dash users use customdata for the purposes of "ids"

Would it be too much to ask to swap customdata for ids. PR #1770 made the ids attribute available to all trace types.

To clarify the a few about @chriddyp 's last #1848 (comment), the API will more like:

{
  x: [10, 20, 30, 40, 50],
  // get rid of customdata, ids is what you want to use here
  ids: ['id-1', 'id-2', 'id-3', 'id-4', 'id-5'],
  marker: {size: 10},

  selected: {
    marker: {color: 'blue'}
  },
  unselected: {
    marker: {opacity: 0.5}
  },
 
  // selectedpoints is an array of indices
  selectedpoints: [2, 4],

  hovered: {
    marker: {line: {color: 'lightgrey'}}
  }

  // here too, an array of indices
  hoveredpoints: [0, 3]
}

Now to do something like @chriddyp 's

  selectedpoints: {
    x: [30, 50],
    ids: ['id-3', 'id-5'],
    customdata: ['id-3', 'id-5'],
    mode: 'markers+text'
  }

would be a little trickier, but not impossible as mode is not arrayOk. One would have to set mode: 'markers+text' at the trace's root level with textfont.color: 'rgba(0,0,0,0)' and then e.g. set selected.textfont.color: 'blue'. Hmm maybe we could do better?

@etpinard
Copy link
Contributor

PR #2135 is now merged, but I'll leave this issue open for future development.

In brief #2135, the new attribute selectedpoints was to all selectable traces. Box & lasso selections now mutate selectedpoints. Selected and unselected opacity level are now configurable along with a few more selected/unselected style attribute (e.g. marker.color and marker.size for scatter traces)

@chriddyp
Copy link
Member Author

chriddyp commented Feb 14, 2018

Another community request for "Selection by click": https://community.plot.ly/t/highlight-clicked-marker-in-scattermapbox-graph/8315 (actually #1852 is better for this one)

@chriddyp
Copy link
Member Author

chriddyp commented Mar 1, 2018

Another one for future reference: https://community.plot.ly/t/is-there-a-way-to-clear-clickdata/8708. actually #1852 is better for this one.

@will-moore
Copy link

Would be great to see this feature - I'd like to set selected objects from other UI components and have them update the Plot. Thanks!

@jackparmer
Copy link
Contributor

This issue has been tagged with NEEDS SPON$OR

A community PR for this feature would certainly be welcome, but our experience is deeper features like this are difficult to complete without the Plotly maintainers leading the effort.

Sponsorship range: $45k-$50k

What Sponsorship includes:

  • Completion of this feature to the Sponsor's satisfaction, in a manner coherent with the rest of the Plotly.js library and API
  • Tests for this feature
  • Long-term support (continued support of this feature in the latest version of Plotly.js)
  • Documentation at plotly.com/javascript
  • Possibility of integrating this feature with Plotly Graphing Libraries (Python, R, F#, Julia, MATLAB, etc)
  • Possibility of integrating this feature with Dash
  • Feature announcement on community.plotly.com with shout out to Sponsor (or can remain anonymous)
  • Gratification of advancing the world's most downloaded, interactive scientific graphing libraries (>50M downloads across supported languages)

Please include the link to this issue when contacting us to discuss.

@gvwilson
Copy link
Contributor

Hi - this issue has been sitting for a while, so as part of our effort to tidy up our public repositories I'm going to close it. If it's still a concern, we'd be grateful if you could open a new issue (with a short reproducible example if appropriate) so that we can add it to our stack. Cheers - @gvwilson

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

No branches or pull requests

7 participants