I have a use case to synchronize zoom range of 3 plots and also update to URL so the states can be saved in the bookmark.
I have the sync part working. But if I add a callback to change url, some strange trigger will happen to update the plot and alter their layout.
dash.ctx shows nothing.
Is this a bug?
import json
from urllib.parse import urlencode
import dash
import numpy as np
import pandas as pd
import plotly.express as px
from dash import Dash, dcc, html
from dash.dependencies import ALL, Input, Output, State
external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
app = Dash(__name__, external_stylesheets=external_stylesheets)
server = app.server
# make a sample data frame with 6 columns
np.random.seed(0) # no-display
df = pd.DataFrame({"Col " + str(i + 1): np.random.rand(30) for i in range(6)})
df["time"] = pd.to_datetime(df.index * 20 + 1661573974, unit="s")
figs = []
for i in [0, 1, 2]:
fig = px.scatter(
df, x=df["time"], y=df["Col " + str(i + 1)], text=df.index
)
fig.layout.autosize = False
figs.append(fig)
app.layout = html.Div(
[
dcc.Location(id="url"),
dcc.Store(id="store-first-load-check"),
dcc.Input(
id={"type": "input", "id": "xrange"},
# value='{"xaxis.range[0]": "2022-08-27 04:23:14.0247", "xaxis.range[1]": "2022-08-27 04:23:41.4052"}',
value="",
size="80",
type="text",
),
html.Div(
dcc.Graph(
id={"type": "synced_graph", "id": "g1", "index": 0},
config={"displayModeBar": True},
figure=figs[0],
)
),
html.Div(
dcc.Graph(
id={"type": "synced_graph", "id": "g2", "index": 1},
config={"displayModeBar": True},
figure=figs[1],
),
),
html.Div(
dcc.Graph(
id={"type": "synced_graph", "id": "g3", "index": 2},
config={"displayModeBar": True},
figure=figs[2],
),
),
],
className="row",
)
if True:
@app.callback(
Output("url", "search"),
[Input({"type": "input", "id": "xrange"}, "value")],
)
def change_url(value):
d = {"xrange": value}
return "?" + urlencode(d)
# this callback defines 3 figures
# as a function of the intersection of their 3 selections
@app.callback(
Output({"type": "synced_graph", "index": 0, "id": "g1"}, "figure"),
Output({"type": "synced_graph", "index": 1, "id": "g2"}, "figure"),
Output({"type": "synced_graph", "index": 2, "id": "g3"}, "figure"),
Output({"type": "input", "id": "xrange"}, "value"),
Output("store-first-load-check", "data"),
Input({"type": "synced_graph", "index": ALL, "id": ALL}, "relayoutData"),
Input({"type": "input", "id": "xrange"}, "value"),
State("store-first-load-check", "modified_timestamp"),
State({"type": "synced_graph", "index": 0, "id": "g1"}, "figure"),
State({"type": "synced_graph", "index": 1, "id": "g2"}, "figure"),
State({"type": "synced_graph", "index": 2, "id": "g3"}, "figure"),
)
def callback(relayouts, text_value, ts, figure1, figure2, figure3):
# determine which input fired
component_id_property = '{"type":"input","id":"xrange"}.value' # no space
if ts is None and text_value: # first load
relayout = json.loads(text_value)
triggered_index = "text"
elif component_id_property in dash.ctx.triggered_prop_ids:
relayout = json.loads(text_value)
triggered_index = "text"
else:
if dash.ctx.triggered_id is None:
raise dash.exceptions.PreventUpdate()
triggered_index = dash.ctx.triggered_id["index"]
# identify which relayoutData to use
relayout = relayouts[triggered_index]
print(relayout)
print(triggered_index)
print(dash.ctx.triggered_prop_ids)
outputs = []
for index, fig in enumerate([figure1, figure2, figure3]):
if relayout == {"autosize": True}:
# fig["layout"]["xaxis"]["autorange"] = True
raise dash.exceptions.PreventUpdate()
if triggered_index == index:
outputs.append(dash.no_update)
continue
try:
fig["layout"]["xaxis"]["range"] = [
relayout["xaxis.range[0]"],
relayout["xaxis.range[1]"],
]
fig["layout"]["xaxis"]["autorange"] = False
except (KeyError, TypeError):
fig["layout"]["xaxis"]["autorange"] = True
outputs.append(fig)
if triggered_index == "text":
outputs.append(dash.no_update)
else:
outputs.append(json.dumps(relayout, indent=2))
return tuple(outputs) + (False,)
if __name__ == "__main__":
app.run_server(debug=True)