plotly / dash

Data Apps & Dashboards for Python. No JavaScript Required.

Home Page:https://plotly.com/dash

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[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

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")

Peek 2023-12-05 12-29

commented

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?

Hi @Coding-with-Adam,

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.
setting_with_dict


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