[BUG] `Patch` does not seem to work correctly for data updates of figures created with `make_subplots`
jonasvdd opened this issue · comments
Describe your context
Hi, I am Jonas, one of the core developers of plotly-resampler
.
For our library, we created a custom component i.e., traceUpdater
, to perform partial trace updates.
But as Patch
was released, we think that this traceUpdater
component can be omitted (since we also got some issues which were easily resolved by utilizing patch) - see predict-idlab/plotly-resampler#271.
Describe the bug
However, when updating the plotly-resampler
codebase to solely utilize Patch
(PR: predict-idlab/plotly-resampler#281) I remarked that Patch
does not work when the figure consists of subplots (i.e. made with make_subplots
). Is this expected behavior? see 📷 ⬇️
Environment
- clone the
refactor/remove-trace-updater
plotly-resampler branch and toy around with the example provided in - predict-idlab/plotly-resampler#281
Examples & Screenshots
from plotly_resampler import FigureResampler
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np
x = np.arange(2_000_000)
noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000
fig = FigureResampler(
make_subplots(rows=2, cols=2, shared_xaxes="columns", horizontal_spacing=0.03)
)
log = noisy_sin * 0.9999995**x
exp = noisy_sin * 1.000002**x
fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=log)
fig.add_trace(go.Scattergl(name="exp"), hf_x=x, hf_y=exp)
fig.add_trace(go.Scattergl(name="-log"), hf_x=x, hf_y=-exp, row=1, col=2)
fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=-log, row=2, col=1)
fig.add_trace(go.Scattergl(name="3-exp"), hf_x=x, hf_y=3 - exp, row=2, col=1)
fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=log**2, row=2, col=2)
fig.show_dash(mode="inline")
hi @jonasvdd
This is an interesting issue. Thank you for sharing. I saw how you're implementing Patch in your code, but I'm not sure why it's not working yet.
So I though we could try to work on a separate subplot code example with Patch to see if we can replicate the error.
If you run the app below, you'll see two graphs. Once a country is selected in the dropdown, Patch
is used to update the country marker color (to red) of the graph on the right. If you hashtag in the last 4 lines of code, Patch would be use to update the country marker color for both subplots.
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from dash import Dash, html, dcc, Input, Output, Patch, callback, no_update, State
import plotly.express as px
app = Dash(__name__)
# Getting our data
df = px.data.gapminder()
df = df.loc[df.year == 2002].reset_index()
# Creating our figure
fig = make_subplots(rows=1, cols=2)
fig.add_trace(
go.Scatter(x=df.lifeExp, y=df.gdpPercap, mode='markers'),
row=1, col=1
).update_traces(marker=dict(color="black"))
fig.add_trace(
go.Scatter(x=df.lifeExp, y=df.gdpPercap, mode='markers'),
row=1, col=2
).update_traces(marker=dict(color="black"))
app.layout = html.Div(
[
html.H4("Updating Point Colors"),
dcc.Dropdown(id="dropdown", options=df.country.unique(), multi=True),
dcc.Graph(id="graph-update-example", figure=fig),
]
)
@callback(
Output("graph-update-example", "figure"),
Input("dropdown", "value"),
prevent_initial_call=True
)
def update_markers(countries):
country_count = list(df[df.country.isin(countries)].index)
patched_figure = Patch()
updated_markers_right_graph = [
"red" if i in country_count else "black" for i in range(len(df) + 1)
]
patched_figure['data'][1]['marker']['color'] = updated_markers_right_graph
# updated_markers_left_graph = [
# "red" if i in country_count else "black" for i in range(len(df) + 1)
# ]
# patched_figure['data'][0]['marker']['color'] = updated_markers_left_graph
return patched_figure
if __name__ == '__main__':
app.run(debug=True)
As you can see, it works, but it's not exactly how you used Patch in your example.
Are you able to use my example code above to create the same error you faced with Plotly Resampler?
Thank you for looking into this; I got it working now (however, further testing is needed whether this also works for all edge cases) :)
The thing I did wrong:
I assumed that the patch would just call "restyle" on the plotly.js
front-end figure and thus only replace the values that I specify in the underlying dict. However applying this code:
patched_figure["data"][trace_index] = trace_dict
Causes all trace
-index data to be completely replaced by trace dict (which actually makes sense)!
see 📷 ⬇️ how trace-data object properties were removed by using patch this way.
How to solve it
By now utilizing Patch
as specifically as possible, relevant properties don't get removed (but the code is a little less elegant).
for trace in update_data[1:]: # skip first item as it contains the relayout
trace_index = trace.pop("index") # the index of the corresponding trace
# All the other items are the trace data which needs to be updated
for key, v in trace.items():
# Be more specific
patched_figure["data"][trace_index][key] = v
return patched_figure
I will close this issue for now,
Thank you for your help,
Jonas