Changing data of figure wrapped in Plotly Resampler

Hey all,

I recently started using the Plotly Resampler as I am working with data with over 38 million datapoints and, in my initial tests, it works amazingly well! As I was trying to implement more and more functionality into my Dash app, however, I could not figure out how to update the y data (multiplying it by a scaling factor) of my traces using dash.Patch() as I had prior. More specifically, while the initial changing of the figure using dash.Patch() worked well, when I pan or zoom, the figure reverts back to the original. I also tried updating the hf_data of the stored FigureResampler object with no success. The reduced code example I adapted from a code example provided on the Plotly Resampler git below demonstrates the problem.

Is there an official way to update the data of a figure used with the FigureResampler that I am missing, or do I have to fully re-initialize the respective figure every time? Interestingly, I found that when I update other β€˜data’ components such as patched_fig['data'][1]['marker']['color'] = 'black', the changes do persist after a relayout event such as panning/zooming (see the commented-out line).

Thank you in advance for any input!

import numpy as np
import plotly.graph_objects as go
from dash import Input, Output, State, callback_context, dcc, html, no_update, Patch
from dash_extensions.enrich import DashProxy, Serverside, ServersideOutputTransform

from plotly_resampler import FigureResampler
from plotly_resampler.aggregation import MinMaxLTTB

# Data that will be used for the plotly-resampler figures
x = np.arange(2_000_000)
noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000

# --------------------------------------Globals ---------------------------------------
app = DashProxy(__name__, transforms=[ServersideOutputTransform()])

app.layout = html.Div(
    [
        html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}),
        html.Button("plot chart", id="plot-button", n_clicks=0),
        html.Button("change data", id="change-button", n_clicks=0),
        html.Hr(),
        # The graph object - which we will empower with plotly-resampler
        dcc.Graph(id="graph-id"),
        # Note: we also add a dcc.Store component, which will be used to link the
        #       server side cached FigureResampler object
        dcc.Loading(dcc.Store(id="store")),
    ]
)


# ------------------------------------ DASH logic -------------------------------------
# The callback used to construct and store the FigureResampler on the serverside
@app.callback(
    [Output("graph-id", "figure"), Output("store", "data")],
    Input("plot-button", "n_clicks"),
    prevent_initial_call=True,
)
def plot_graph(n_clicks):
    ctx = callback_context
    if len(ctx.triggered) and "plot-button" in ctx.triggered[0]["prop_id"]:
        fig: FigureResampler = FigureResampler(
            go.Figure(), default_downsampler=MinMaxLTTB(parallel=True)
        )

        # Figure construction logic
        fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=noisy_sin * 0.9999995**x)
        fig.add_trace(go.Scattergl(name="exp"), hf_x=x, hf_y=noisy_sin * 1.000002**x)

        return fig, Serverside(fig)
    else:
        return no_update


# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
# As we use the figure again as output, we need to set: allow_duplicate=True
@app.callback(
    Output("graph-id", "figure", allow_duplicate=True),
    Input("graph-id", "relayoutData"),
    State("store", "data"),  # The server side cached FigureResampler per session
    prevent_initial_call=True,
    memoize=True,
)
def update_fig(relayoutdata: dict, fig: FigureResampler):
    if fig is None:
        return no_update
    return fig.construct_update_data_patch(relayoutdata)


# # Callback for button to change trace data
@app.callback(
    Output("graph-id", "figure", allow_duplicate=True),  # Output("store", "data", allow_duplicate=True)],
    Input("change-button", "n_clicks"),
    # State("store", "data"),
    prevent_initial_call=True,
)
def change_fig(n_clicks):  # , fig):
    ctx = callback_context
    if len(ctx.triggered) and "change-button" in ctx.triggered[0]["prop_id"]:
        patched_fig = Patch()

        patched_fig['data'][1]['y'] *=2
        # fig.hf_data[1]['y'] *= 2

        # patched_fig['data'][1]['marker']['color'] = 'black'

        return patched_fig  # , Serverside(fig)
    else:
        return no_update

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
    app.run_server(debug=True, port=9024)
1 Like

Follow-up to my question: Just now, I found out that if I do have a color on the trace set at initialization, changes induced by patched_fig['data'][1]['marker']['color'] = 'black' actually do not persist after relayout events either (see code below). This has me highly confused as to how to initialize properties of a figure with the resampler and then update these properties later on in callbacks.

Any feedback is highly appreciated!

import random
import numpy as np
import plotly.graph_objects as go
from dash import Input, Output, State, callback_context, dcc, html, no_update, Patch
from dash_extensions.enrich import DashProxy, Serverside, ServersideOutputTransform

from plotly_resampler import FigureResampler
from plotly_resampler.aggregation import MinMaxLTTB

# Data that will be used for the plotly-resampler figures
x = np.arange(2_000_000)
noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000

# --------------------------------------Globals ---------------------------------------
app = DashProxy(__name__, transforms=[ServersideOutputTransform()])

app.layout = html.Div(
    [
        html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}),
        html.Button("plot chart", id="plot-button", n_clicks=0),
        html.Button("change data", id="change-button", n_clicks=0),
        html.Hr(),
        # The graph object - which we will empower with plotly-resampler
        dcc.Graph(id="graph-id"),
        # Note: we also add a dcc.Store component, which will be used to link the
        #       server side cached FigureResampler object
        dcc.Loading(dcc.Store(id="store")),
    ]
)


# ------------------------------------ DASH logic -------------------------------------
# The callback used to construct and store the FigureResampler on the serverside
@app.callback(
    [Output("graph-id", "figure"), Output("store", "data")],
    Input("plot-button", "n_clicks"),
    prevent_initial_call=True,
)
def plot_graph(n_clicks):
    ctx = callback_context
    if len(ctx.triggered) and "plot-button" in ctx.triggered[0]["prop_id"]:
        fig: FigureResampler = FigureResampler(
            go.Figure(), default_downsampler=MinMaxLTTB(parallel=True)
        )

        # Figure construction logic
        fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=noisy_sin * 0.9999995**x)
        fig.add_trace(go.Scattergl(name="exp", marker={'color': 'red'}), hf_x=x, hf_y=noisy_sin * 1.000002**x)

        return fig, Serverside(fig)
    else:
        return no_update


# The plotly-resampler callback to update the graph after a relayout event (= zoom/pan)
# As we use the figure again as output, we need to set: allow_duplicate=True
@app.callback(
    Output("graph-id", "figure", allow_duplicate=True),
    Input("graph-id", "relayoutData"),
    State("store", "data"),  # The server side cached FigureResampler per session
    prevent_initial_call=True,
    memoize=True,
)
def update_fig(relayoutdata: dict, fig: FigureResampler):
    print(relayoutdata)
    if fig is None:
        print('no update')
        return no_update
    patched_fig = fig.construct_update_data_patch(relayoutdata)
    return patched_fig


# # Callback for button to change trace data
@app.callback(
    Output("graph-id", "figure", allow_duplicate=True),  # Output("store", "data", allow_duplicate=True)],
    Input("change-button", "n_clicks"),
    # State("store", "data"),
    prevent_initial_call=True,
)
def change_fig(n_clicks):  # , fig):
    ctx = callback_context
    if len(ctx.triggered) and "change-button" in ctx.triggered[0]["prop_id"]:
        patched_fig = Patch()

        # patched_fig['data'][1]['y'] *=2
        # fig.hf_data[1]['y'] *= 2

        patched_fig['data'][1]['marker']['color'] = random.choice(['black', 'blue', 'green', 'yellow'])

        return patched_fig  # , Serverside(fig)
    else:
        return no_update

# --------------------------------- Running the app ---------------------------------
if __name__ == "__main__":
    app.run_server(debug=True, port=9024)
1 Like