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