Programmatically trigger hover events with Dash

I was trying for a little while to programmatically trigger hover events with Dash to do something like in this post.

The problem was: I have a map with a route and to each point on this route correspond a certain number of metrics like speed and elevation as a function of time. I wanted to be able to hover the map and have the info about my metrics shown in the other plot as though I was hovering both at the same time.

I had seen the excellent codepen from @etienne in this post that would allow me to do this with Plotly.js but it was still a step further to get it to work with Dash. However after some trial and error I finally managed to use the Plotly.js functions from a clientside_callback in Dash to do what I intended and I would like to share it with the community.

And here’s the code (and in particular the clientside function) to get this minimal example working :slight_smile:

app.py

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ClientsideFunction

import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots


app = dash.Dash()
app.config.suppress_callback_exceptions = True
app.css.config.serve_locally = True
app.scripts.config.serve_locally = True

time = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
distance = [0,0.004,0.007,0.011,0.016,0.022,0.028,0.036,0.042,0.048,0.052,0.059,0.066,0.072,0.078,0.084,0.09,0.099,0.109,0.118,0.125]
speed = [14.696,15.328,10.439,19.156,18.624001,17.870001,26.912001,27.907,22.976,16.390001,18.188002,27.021,21.201,23.300001,20.500999,22.988001,23.959999,31.730001,31.956001,31.43,21.372]
latitude = [-38.17793652,-38.17790629,-38.17790202,-38.17791691,-38.17793357,-38.17796016,-38.17800478,-38.17805941,-38.17810737,-38.17814195,-38.17817029,-38.17817777,-38.17820753,-38.17824037,-38.17826873,-38.17827853,-38.17828449,-38.17830426,-38.17837713,-38.17843687,-38.17848226]
longitude = [176.3022128,176.3021731,176.3021367,176.3020748,176.3020026,176.3019411,176.3018562,176.3018142,176.3017969,176.3017735,176.3017327,176.3016405,176.3015786,176.3015091,176.3014366,176.3013507,176.3012592,176.3011339,176.3010612,176.30099,176.3009314]
elevation = [587.94,586.76,583.71,580.86,577.35,571.41,563.5,557.79,553.01,550.37,545.63,541.79,536.48,534.84,531.5,530.14,529.69,526.94,523.21,521.83,519.1]


map_figure = px.line_mapbox(
    lat=latitude,
    lon=longitude,
    custom_data=[time],
    mapbox_style="open-street-map",
    zoom=17
)
map_figure.update_layout(margin=dict(b=0,t=0,r=0,l=0))

metrics_figure = make_subplots(specs=[[{"secondary_y": True}]])
metrics_figure.add_trace(
    go.Scatter(
        x=time,
        y=elevation,
        name="Elevation",
        hovertemplate="Elevation: %{y}m",
    )
)
metrics_figure.add_trace(
    go.Scatter(
        x=time,
        y=speed,
        name="Speed",
        hovertemplate="Speed: %{y}m",
    ),
    secondary_y=True,
)
metrics_figure.update_layout(hovermode="x unified", margin=dict(b=20,t=20,r=20,l=20))

app.layout = html.Div(children=[
    html.H2("Metrics"),
    dcc.Graph(id="metrics_graph", figure=metrics_figure),
    html.H2("Map"),
    dcc.Graph(id="map_graph", figure=map_figure),
    html.Div(id="dummy"),
])

app.clientside_callback(
    ClientsideFunction(namespace="clientside", function_name="trigger_hover"),
    Output("dummy", "data-hover"),
    [Input("map_graph", "hoverData")],
)

if __name__ == "__main__":
    app.run_server(debug=True)

assets/script.js

if (!window.dash_clientside) {
    window.dash_clientside = {};
}
window.dash_clientside.clientside = {
    trigger_hover: function(hoverData) {
        var myPlot = document.getElementById("metrics_graph")
        if (!myPlot.children[1]) {
            return window.dash_clientside.no_update
        }
        myPlot.children[1].id = "metrics_graph_js"

        if (hoverData) {
            if (Array.isArray(hoverData.points[0].customdata)) {
                var t = hoverData.points[0].customdata[0]
            } else {
                var t = hoverData.points[0].customdata
            }
            t = Math.round(t*10)/10
            Plotly.Fx.hover("metrics_graph_js", {xval: t, yval:0})
        }
        return window.dash_clientside.no_update
    }
}
7 Likes

:trophy: :trophy: Very nice!!

Hi I want to use your code, but I can’t understand from “assets/script.js”

I’m using Jupyter notebook, I could make two graphs (map and graph) on Dash.

Can you explain how I can use your code below assets/script.js?

map_figure = px.line_mapbox(df_GPS,
    lat=df_GPS["Lat[1]"].tolist(),
    lon=df_GPS["Long[1]"].tolist(),
    hover_name= df_GPS["dnt"].dt.strftime("%m-%d %H:%M:%S"),
    custom_data=["time"],
    mapbox_style="open-street-map",
    zoom=10
)

map_figure.update_layout(margin=dict(b=0,t=0,r=0,l=0))
map_figure.show(renderer='notebook')

y_list =["Speed","Altitude"]

metrics_figure = make_subplots(specs=[[{"secondary_y": True}]])

metrics_figure.add_trace(
    go.Scatter(
        x=df_GPS["dnt"].dt.strftime("%m-%d %H:%M:%S"),
        y=df_GPS[y_list[0]],
        name=y_list[0],
        line_color=mcolors[0],
        hovertemplate= "%{y}"))

layout = {'xaxis':dict(domain=[0,1-(len(y_list)-2)*0.09]),
          'yaxis':dict(title=y_list[0],titlefont=dict(color=mcolors[0]),
                       tickfont=dict(color=mcolors[0]),showgrid=True)}
n=2
for var_y in y_list[1::]:
    metrics_figure.add_trace(
        go.Scatter( 
        x=df_GPS["dnt"].dt.strftime("%m-%d %H:%M:%S"),
        y=df_GPS[var_y],
        name=var_y,
        yaxis ='y'+str(n),
        line_color=mcolors[n],
        hovertemplate= "%{y}"
        ))
                 
    layout['yaxis'+str(n)] = dict(title=var_y, titlefont=dict(color=mcolors[n]),
                                  tickfont=dict(color=mcolors[n]),
                                  anchor="free",
                                  overlaying="y",
                                  side="right",
                                  position=1-(n-2)*0.09,
                                  showgrid=False)
    n+=1
    
metrics_figure.update_layout(**layout,hovermode="x unified", margin=dict(b=20,t=20,r=20,l=20))
app = dash.Dash(__name__)
app.config.suppress_callback_exceptions = True
app.css.config.serve_locally = True
app.scripts.config.serve_locally = True
app = dash.Dash(__name__)
app.layout = html.Div(children=[
    html.H2("Map"),
    dcc.Graph(id="map_graph",figure=map_figure),
    html.H2("Metrics"),
    dcc.Graph(id="metrics_graph",figure=metrics_figure),
    html.Div(id="dummy")
])

app.clientside_callback(
    ClientsideFunction(namespace="clientside", function_name="trigger_hover"),
    Output("dummy", "data-hover"),
    [Input("map_graph", "hoverData")],
)

if __name__ == '__main__':
    app.run_server(debug=True,use_reloader=False)
    

I could run my code by this part.
and I met an error (Cannot read property ‘trigger_hover’ of undefined) on Dash.

Plz Help me

In the same folder where your app.py lives, create an assets folder in which you add a scripts.js file where you put the code I wrote above. You need to make sure the ids match between your layout and what’s in scripts.js.

Hi.
Thank you. It works well.
I have one more question.
Is it possible to change input to output?
I mean (input :Metrics figure, output : map figure)
As I move my mouse point on a metrics figure, I want to see a location on a map by a hover pop-up.

Very nice :grinning:. Do you know if it is possible to make it works in reversal direction ?
Meaning mouse move on the metrics, programmatic fx hover on the map.
It is not obvious to find the correct properties to make Ploty.Fx.hover function works on map_graph. I’ve tried xval, yval with longitude and latitude without success. So if anybody has a idea I’ll be happy :slight_smile:

Hey @yanndavin have a look at this topic, I think you’ll have to go with curveNumber and pointNumber.

Thanks you @RenaudLN curveNumber and pointNumber looks promising. Like @azureity I’m stuck now on the Invalid LngLat obj: (NaN, ...), after quick look inside the debugger a variable is undefined for a unknown reason which causes effective calculation of the longitude to raise a exception (see screenshot).
I’ve not yet found a solution.

@RenaudLN I’ve finally found that you can simply pass longitude and latitude as xval, yval:
Plotly.Fx.hover("map_graph_js", {xval: lon, yval: lat}, "mapbox")

1 Like

I found this thread almost a year later while running into the same problem…
Unfortunately i cannot hover via xval/yval since my coords are not unique, so matching the point only via coords is not enough.

The following unfortunately does not work on the map. It creates a single hover label and a some point but not the one specified with pointNumber.

Plotly.Fx.hover(graph_id, {curveNumber: curveNumber, pointNumber:pointNumber}, "mapbox")

With this, i.e. second arg as an array, i get an Error: Invalid LngLat object: (NaN, 48.*)

Plotly.Fx.hover(graph_id, [{curveNumber: curveNumber, pointNumber:pointNumber}], "mapbox")

Anyone has a solution here?

Very nice example!

If i understand correctly, the callback is triggered for every new hover on the MapBox graph. Do you know if your MaxBox token is debited for every hover then? Or only just once to generate the map?