plotly / plotly.js

Open-source JavaScript charting library behind Plotly and Dash
https://plotly.com/javascript/
MIT License
16.72k stars 1.83k forks source link

Customized Click, Hover, and Selection Styles or Traces #1847

Open chriddyp opened 7 years ago

chriddyp commented 7 years ago

Plotly.js has default styles for hovering (tooltips) and for selection (dimmed traces).

In Dash, users want to be able to customize these styles. While they can customize these styles themselves through Dash callbacks, it's slow (roundtrip to server). And while I could write this behaviour into the Dash Graph component, it would be great if this was standard behaviour that everyone could benefit from.

Here are some examples that customized interaction styles or traces could enable.


As requested in the Dash community forum

d3827b8cd525b5f9b5111d4b93f1e336534043c5

For this example, there was some "animation fading" implemented, this is, why the line seems to be slightly behind the mouse. But I guess it's clear to see what kind of performance, I was hoping to get by Dash. I think, it would be really nice, if there are efforts to improve these kind of animations, because I think they are used in many data visualization tools (having a moving maker I mean). There is this built-in Plotly "Toggle Spike Lines" function in each Graph, that does nearly what I want and is pretty fast, but not customizable.

In addition to this example, users might want to:

capture d ecran 2017-07-04 a 13 04 29


In this Uber Rides Dash demo created by @alishobeiri, the selected bars are replotted to be white. This persistent style informs the user which bars are selected and also matches the color palette of app itself.

selected-bars

While selected markers have a "dimming" effect, some users will want to customize the style of the selected points and the unselected points. For example, they might want to:

selected-regions


In BI platforms, "clicking" is often used for drill downs. While we support click events, we don't modify the look and feel of the graph after clicking. For example, users might want to display constant text when clicking on a point.

In addition, there are a few other default style changes for clickable points (this could be a separate issue):

capture d ecran 2017-07-04 a 13 05 19 capture d ecran 2017-07-04 a 13 05 16 capture d ecran 2017-07-04 a 13 05 51


I'm sure there are many more examples out there.

cc @alexcjohnson @etpinard @cpsievert @monfera @jackparmer

chriddyp commented 7 years ago

I haven't thought about the attribute specs that much yet. One way to get some of these interactions would be through a :hover suffix like in CSS:

{
    marker: {
        color: "blue",
        "color:hover": "orange",
        "color:click": "green",
        "color:selected": "purple",
        "color:unselected": "grey"
    }
}

That doesn't cover the case for drawing a vertical or horizontal line over points.

chriddyp commented 7 years ago

With custom hover attributes, perhaps users could create their tooltips by using a combination of annotations, shapes, and images that are triggered on and off through custom hover styles.

chriddyp commented 7 years ago

cc @charleyferrari who might have some additional examples too

chriddyp commented 7 years ago

Example from @cpsievert 's work. In this case, highlighting across subplots. That type of linking hover across traces might be out of the scope of this.

https://plotcon17.cpsievert.me/workshop/day2/#18

highlight

monfera commented 7 years ago

Indeed a solution for this would be useful (needed) for the crossfilter as we discussed in https://github.com/plotly/plotly.js/issues/1762

WARNING: totally my personal ramblings and biases only; in part informed by the crossfiltered dashboard work:

If possible, it'd be great to separate the activating affordances (e.g. Click, Hover, Selection, or crossfiltering some points away from another plot) from the specific temporary restyling ie. visual channel updates, as it may depend, as @chriddyp you write, on the intent such as clicking could indicate various intents, e.g. drilling down, including/excluding from filter, showing atomic data for point or jumping to an external link. A dashboard may have various inputs external to the plotly widgets or concepts, e.g. streaming stock prices would highlight the ticker (and time series line) of the last traded stock, or a UI tour or storytelling, so it might be beneficial to make these work and be assignable depending on how the context requires it.

Having made this separation, I think the affordances part is trivial as these plotly.js events mostly exist or can be extended. So the task is reduced to applying arbitrary styling changes. This is the harder part:

With the current plotly.js API

One option is to simply call Plotly.restyle and/or Plotly.relayout - which has the following issues:

  1. These APIs weren't desinged for high FPS interactions, each call can trigger a lot of recalculations, whether they're needed or not
  2. Often, the desired effect needs multiple restyle and relayout calls; currently can't be batched
  3. The alternative, e.g. Plotly.newPlot pretty much throws out everything so it's not fast either, and resets user selections or other user navigated state (zoom, pan etc.)
  4. The plotly.js-style column array updates are great when only one thing changes, but (I think) it's not possible to update one or a few points out of eg. 1000 data points, and even if we make such an API, making it faster than a full vector update is still a challenge.
  5. The internal architecture isn't (yet?) reactive, i.e. while the above constraints can be mitigated eg. by batching, the real solution would be the principle of not recalculating things that have no need to change, which is an operational benefit of reactive data flows (most.js, kefir, flyd, vega-dataflow, or our compact crosslink library)

With CSS restyling

It's a quick win b/c the desired effect can be achieved (limited only by the standard), and no code change is needed. It can be made as fast as the DOM allows. Yet it has some drawbacks:

  1. The plotly.js scenegraph, e.g. structuring, classnames etc. may change
  2. Future plot versions may specify or override the very CSS styles that are used
  3. User interactions (zoom, pan, selection, or anything that yields even a partial rerender) may interfere with the styling, e.g. fully or partially resetting/removing the custom styling
  4. It's a non-specified API from the viewpoint of plotly.js ie. not part of a 'contract' with the user

Yet I think it's an attractive direction as

  1. it's a quick win
  2. the scene graph, class names and CSS usage don't change much
  3. can be made fast; easy to integrate into reactive dataflows
  4. not limited to what we preconceive
  5. if the user so wishes, it can even be a browser-specific styling which we naturally can't do ourselves

OK then what?

Maybe @etpinard @alexcjohnson @chriddyp @jackparmer @cpsievert @rreusser @charleyferrari or someone else perhaps has clearer views than I. One option is to provide preconceived restyles (e.g. 'increase salience' - 'decrease salience' etc). Another option could be to somehow open up and declaratively expose the scene graph and commit to class names and using / not using certain CSS styles on certain elements - sounds very laborious.

Solving the incremental recalc with reactive streams

Recalculate data and update the DOM for only what's needed. E.g. move the sliders under Model tweaks or Layout tweaks in this older experiment

Vega is also using reactive streams for much the same purpose. There are very nice papers and videos that describe it, e.g. this one from Arvind Satyanarayan, Ryan Russell, Jane Hoffswell and Jeffrey Heer - thanks Arvind for reminding me of this paper recently.

We've started to use a similar solution with the crossfilter work but as it uses plotly.js charts it won't make those faster. For this reason, there's a user exit facility (SAP terminology which is a huge standard system that needs highly client dependent extensions) that'd allow custom styling and even support for adding and updating non-plotly.js plots.

What about react and redux

Despite the name, react is not reactive in itself. It can be made reactive, but even then, by its nature, it still regenerates the entire scene graph (as virtual DOM) and does a lot of DOM diffing even where these won't lead to an update. There are some solutions e.g. using immutability and identity equality for shouldComponentUpdate checks but using an immutable library for our rather large JSON specs (incl. data) and more importantly, the large amounts of scenegraph data generated, is quite expensive too. Caching vdom fragments (subtrees) is also a possibility to reduce vdom tree construction. It could work but it has speed limits.

A more serious limitation with redux from a viewpoint of continuous interactions - it's great with predictable changes to an essentially global object, yet due to its conventions (e.g. using strings for dispatching actions), best practices eg. immutability, and our need to perform cascading changes, it gets expensive fast. Basically, each mouse move would rebuild the entire dashboard state. For this reason, redux isn't often used for high-frequency interactions, and a half-solution won't be a solution. I wrote on this in more detail in the last three paragraphs of this and here and here so it deff. makes me an FRP fanboy; glad to learn about other approaches though.

There are some attempts to solve the core issue with redux involved, e.g. reselect and various caching approaches which might have some memory leak risks. Btw. redux can be combined with reactive data streams / FRP just fine, so having redux for overall app state while doing data updates and user interactions via FRP is OK but redux doesn't add much for the interactivity / 60FPS update aspect so it's not relevant for this discussion.

etpinard commented 7 years ago

With custom hover attributes, perhaps users could create their tooltips by using a combination of annotations, shapes, and images that are triggered on and off through custom hover styles.

That sounds painful. We could do better.

etpinard commented 7 years ago

We could maybe attach method and args attributes to traces similar to updatemenus buttons and slider steps which act as a callback on hover and/or click. For example,

Plotly.newPlot('graph', [{
  mode: 'markers',
  x: [1, 2, 3],
  y: [2, 1, 2],
  marker: {
    color: 'red'
  },
  interaction: {
      type: 'hover', // or 'click' or 'select'
      method: 'restyle',
      args: ['marker.color', 'blue']
    }
}])
etpinard commented 7 years ago

I think we should merge this ticket with https://github.com/plotly/plotly.js/issues/1762

Any objections @monfera ?

monfera commented 7 years ago

@etpinard yes I'll close #1762 as this one pretty much supersedes that

monfera commented 7 years ago

1762 is closed; an illustration of the crossfiltered styling in plots other than the one where we do the box selection is here

monfera commented 6 years ago

https://github.com/plotly/plotly.js/issues/1943 is now closed but its comments are pertinent to this discussion.

jackparmer commented 6 years ago

I'd love to be able to set the color of an individual bar or scatter point on hover.

For example, in a monotone chart like this:

image

I'd love to set the hover color of a bar to an accent color like pink.

chriddyp commented 6 years ago

In Dash, I'd like mode to a style customizable under the selected property so that you can set mode: 'markers+text' while selecting but keep it as markers when unselected. We used to achieve this just by plotting a new trace on top of it.

alexcjohnson commented 6 years ago

mode is going to be tough, because it changes what objects need to be created, not just how those objects are styled, so it would need to be a much slower update pathway. Would it work to give unselected text a transparent font color?

chriddyp commented 6 years ago

Would it work to give unselected text a transparent font color?

That works, thanks!

scottlittle commented 5 years ago

Are there any examples of highlighting a trace upon hover in plotly for python? Or has this functionality not been added to python yet?

merges commented 5 years ago

Any chance there's a pending update with some of these improvements? :)

scottlittle commented 5 years ago

This stackoverflow question and answer may be of interest to some.

RutanR commented 5 years ago

how to implement this one 33997429-6b424ee4-e0f5-11e7-97ad-b381c1ecba87

cholman commented 4 years ago

In addition, there are a few other default style changes for clickable points (this could be a separate issue):

Modify the hover interaction of the point to make it seem more "clickable": adding cursor: pointer and making the point a little bit larger

Is this possible? I have markers that open a modal when clicked, but it isn't clear to the user that they are clickable. I'm trying to figure out how to get the cursor to change to a hand pointer e.g. cursor: pointer when hovering over the marker. I am using react

seabass011 commented 4 years ago

Would love to know how I can help. Is this under active development?

pfbuxton commented 4 years ago

Hi Sebastian,

We have a fairly well developed code for analyzing data from our experiments. Each experiment lasts just less than 1 second, then we have a 15 minutes down time where our power supplies are re-charged, during this down time we have to analyze data, and based on that analysis make some changes before the next experiment.

This data visualization tool which we have developed is under active development.

For our purposes we have a single large app with several pages (the management is quite easy as there is little communication between pages).

Attached is an example of a feature which we often use in various different pages. As this isn’t a multi-page app I haven’t got some of the triggering to work correctly, so if you to start click “>” the app will start. In the app there are two graphs:

  1. interactive_graph Has an editable shape which we use as a slider to select a time which is of interest to us.
  2. graph2 Then displays some information based on where this slider is. There are also two buttons which we can click which will skip a pre-defined amount through time. We also display the time where the slider has been mode to.

All of this code is functional and working, but if the interactive_graph has a lot of data it can take a long time to re-plot (which is unnecessary). It is also quite cumbersome – needing several callbacks which have to be modified every time we use this feature. We also have to do the placement to the next and back buttons which is a bit annoying.

What we would ideally like is for a trigger to come from the client side which tells us to re-plot graph2.

Do we have a time which we can talk tomorrow?

Thanks, Peter

From: Sebastian Heyneman notifications@github.com Sent: 13 June 2020 22:51 To: plotly/plotly.js plotly.js@noreply.github.com Cc: Peter Buxton peter.buxton@tokamakenergy.co.uk; Manual manual@noreply.github.com Subject: Re: [plotly/plotly.js] Customized Click, Hover, and Selection Styles or Traces (#1847)

Would love to know how I can help. Is this under active development?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://github.com/plotly/plotly.js/issues/1847#issuecomment-643682784, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ABFSBRE4A53L25SBLW7IKQ3RWPYELANCNFSM4DRWINVQ.

kevalshah90 commented 3 years ago

In addition, there are a few other default style changes for clickable points (this could be a separate issue): Modify the hover interaction of the point to make it seem more "clickable": adding cursor: pointer and making the point a little bit larger

Is this possible? I have markers that open a modal when clicked, but it isn't clear to the user that they are clickable. I'm trying to figure out how to get the cursor to change to a hand pointer e.g. cursor: pointer when hovering over the marker. I am using react

I am trying to figure this out. I'd like to update modal popup based on scatter mapbox marker click event data. How did you do it?

html.Div([

        dbc.Modal(
            [
                dbc.ModalHeader("Lease Information"),
                dbc.ModalBody(
                    [
                        dbc.Label("Address:", id='address'),
                        dbc.Label("Name:", id='name')

                    ]
                ),
                dbc.ModalFooter(
                    [
                        dbc.Button("OK", color="primary", size="lg", className="mr-1"),
                    ]
                ),
            ],
            id="modal",
        ),

   ], style={"width": "50%"}), 

# Update modal on click event
@app.callback(Output("modal", "is_open"),
              [
                  Input("map-graph1", "clickData")],
               [State("modal", "is_open")],
              )
def display_popup(clickData, close, is_open):

    if clickData is None:

        return (no_update)

    else:

        Name = clickData['points'][0]['customdata']['Name']
        Address = clickData['points'][0]['customdata']['Address']

    return (Name, Address)
jackparmer commented 3 years ago

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:

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

inselbuch commented 3 years ago

Would like to chime in on the possibility of simply disabling the tooltips. I still want the click behavior to return the closest point. I just don't want the tooltip at all.

alexcjohnson commented 3 years ago

@inselbuch that should just be hoverinfo: 'none' https://plotly.com/javascript/reference/scatter/#scatter-hoverinfo

inselbuch commented 3 years ago

Wonderful! I had tried "skip" ... did not see "none"

You are a rock star.

Sent from my iPad

On Nov 6, 2020, at 9:14 AM, Alex Johnson notifications@github.com wrote:



@inselbuchhttps://github.com/inselbuch that should just be hoverinfo: 'none' https://plotly.com/javascript/reference/scatter/#scatter-hoverinfo

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/plotly/plotly.js/issues/1847#issuecomment-723132672, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AFMVC6MHADOK7LDBPCLM3BDSOQHDFANCNFSM4DRWINVQ.

jjrob13 commented 3 years ago

Hey sorry to jump in here, hoping my usecase is already supported as the previous commenter's was.

I often use plotly to visualize many series with different ranges on a shared timescale. One feature I've been searching for (and I'm not sure if it's part of this issue or not - so pardon my asking) is the ability to have clicks persist the hover dialog.

For example, I'd love to be able to do something like this by clicking in a few locations, to easily highlight how these many traces are changing over time (and possibly to set some default clickData to show some default hoverinfo for a few points when users first open the page): image

Please let me know if this is already supported, or if it's part of this pending feature request.

Thank you very much!

Andrej4156 commented 3 years ago

I linked here from part 4 of the tutorial .

I just wanted to chime in that it is possible to customize the hover interactions while using a selection box by changing the code in the tutorial in the "Generic Crossfilter Recipe" section:

[p['customdata'] for p in selected_data['points']]) to [p['pointNumber'] for p in selected_data['points']])

and then getting rid of customdata=df.index, in update_traces.

p['pointNumber'] already has point indices so setting p['customdata'] to df.index makes p['customdata'] identical to p['pointNumber'] and needlessly occupies p['customdata'] which prevents the user from customizing the hover interaction using the method outlined here here.

I'm not sure if this addresses all of the issues presented here or if someone else has already mentioned it before but at the very least the tutorial should probably be updated to use pointNumber instead of customdata as a matter of good practice (unless there is some other utility for it that I am missing).

nicolaskruchten commented 3 years ago

@Andrej4156 thanks! I think the pointNumber will not match up correctly with df.index if you have multiple traces in the same figure, which is why the recipe is set up this way.

EWChina999 commented 2 years ago

ECharts is powerful, has a feature of "hover on legend to highlight the respective series and dim the rest of series", please check https://github.com/apache/echarts/issues/17200.