Black Lives Matter. Please consider donating to Black Girls Code today.
Learn about the upcoming Dash Enterprise 4.0 release in the August 5th webinar with Chris Parmer, the Inventor of Dash.

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
    }
}
3 Likes

:trophy: :trophy: Very nice!!