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

Open
chriddyp opened this issue Jul 4, 2017 · 15 comments

Comments

Projects
None yet
5 participants
@chriddyp
Copy link
Member

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

This comment has been minimized.

Copy link
Member

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

This comment has been minimized.

Copy link
Member

commented Jul 4, 2017

See also #257

@jackparmer

This comment has been minimized.

Copy link
Contributor

commented Oct 15, 2017

@rreusser

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Member

commented Oct 17, 2017

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

This comment has been minimized.

Copy link
Contributor

commented Oct 17, 2017

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

This comment has been minimized.

Copy link
Member

commented Oct 17, 2017

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

This comment has been minimized.

Copy link
Member Author

commented Oct 18, 2017

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

This comment has been minimized.

Copy link
Member Author

commented Oct 18, 2017

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

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link
Member

commented Oct 18, 2017

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

This comment has been minimized.

Copy link
Member

commented Nov 20, 2017

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

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.