Sharing examples of partial update & allow_duplicate=True from Dash 2.9.2!

Hi all,

recently I started playing around with partial updates and the allow_duplicate=True parameter. For my app the benefit of using the new features is huge.

I thought I share two MRE do demonstrate the capabilities.

  • change the trace color of selected traces, uses partial updates and duplicated outputs
  • add annotations to a image displayed with px.imshow(), here you should take a look on the data size traveling from client to server depending on the button you click.

First example, change trace colors:

import dash
from dash import Input, Output, html, dcc, Patch
import plotly.graph_objects as go
import numpy as np

# generate some data
TRACES = 7
DATAPOINTS = 10
COLORS = [['darkgreen', 'gold'][c % 2] for c in range(DATAPOINTS)]

# create figure
figure = go.Figure(layout={'width': 1000, 'height': 800})
for idx in range(TRACES):
    figure.add_scatter(
        x=np.arange(DATAPOINTS),
        y=np.arange(DATAPOINTS) + 10*idx,
        mode='markers',
        marker={'color': 'crimson', 'size': 10},
        name=idx
    )

app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Div(
            [
                dcc.Dropdown(id='drop_post', options=[*range(TRACES)]),
                dcc.Dropdown(id='drop_post_2', options=[*range(TRACES)]),
            ],
            style={'width': '10%'}
        ),
        html.Div([
            dcc.Graph(
                id='graph_post',
                figure=figure,
            )
        ]),
    ]
)


@app.callback(
    Output('graph_post', 'figure', allow_duplicate=True),   # <-- allow component property
    Input('drop_post', 'value'),                            #     to be updated from different
    prevent_initial_call=True                               #     callbacks
)
def new(i):
    # Creating a Patch object
    patched_figure = Patch()

    # update all marker colors for selected trace index (drop down selection)
    patched_figure["data"][i].update({"marker": {'color': 'darkblue', 'size': 20}})
    return patched_figure


@app.callback(
    Output('graph_post', 'figure'),
    Input('drop_post_2', 'value'),
    prevent_initial_call=True
)
def update(j):
    # Creating a Patch object
    patched_figure = Patch()

    # update single marker colors with list of colors
    patched_figure["data"][j].update({"marker": {'color': COLORS, 'size': 20}})
    return patched_figure


if __name__ == '__main__':
    app.run(debug=True, port=8051)

Second example, add annotations to figure:

import dash
from dash import Input, Output, html, dcc, State, Patch
import plotly.express as px
import numpy as np

# create quite big color image
img = np.ones(shape=(4000, 6000, 3))
img[:, :, 0] = 173
img[:, :, 1] = 21
img[:, :, 2] = 25

# pre define a shape to add
new_shape = {
    'editable': True,
    'xref': 'x',
    'yref': 'y',
    'layer': 'above',
    'opacity': 1,
    'line':
        {
            'color': '#fabd00',
            'width': 2,
            'dash': 'solid'
        },
    'fillcolor': '#fabd00',
    'fillrule': 'evenodd',
    'type': 'rect',
    'x0': 0,
    'y0': 0,
    'x1': 3000,
    'y1': 4000,
    'name': 'default_name'
}

# create figure
figure = px.imshow(img, height=600, width=900)

app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Div(
            [
                html.Button('< plotly 2.9.0', id='btn_pre'),
                html.Button('>= plotly 2.9.0', id='btn_post'),
            ],
            style={'width': '10%'}
        ),
        html.Div([
            dcc.Graph(
                id='graph_pre',
                figure=figure,
                style={'display': 'inline-block'}
            ),
            dcc.Graph(
                id='graph_post',
                figure=figure,
                style={'display': 'inline-block'}
            )
        ]),
    ]
)


@app.callback(
    Output('graph_post', 'figure'),
    Input('btn_post', 'n_clicks'),
    prevent_initial_call=True
)
def new(_):
    # Creating a Patch object
    patched_figure = Patch()

    # add shape to patch
    patched_figure["layout"].update({"shapes": [new_shape]})
    return patched_figure


# way to add annotations pre dash 2.9.0
@app.callback(
    Output('graph_pre', 'figure'),
    Input('btn_pre', 'n_clicks'),
    State('graph_pre', 'figure'),  # <-- state of current figure needed
    prevent_initial_call=True
)
def old(_, current_figure):
    # add shape to figure
    current_figure["layout"].update({"shapes": [new_shape]})
    return current_figure


if __name__ == '__main__':
    app.run(debug=True, port=8050)

See also the release announcement

mred patch

4 Likes

Here is another MRE where I use duplicate outputs for emulation a background callback:

Thank you @AIMPED for sharing these examples.

These do a great job highlighting the advantage of Patch() and allow_duplicate.

1 Like

What’s really cool about the second example, is that the syntax of changing the figure and the patch is identical. It has never been easier to reduce the data traveling between client and server.

1 Like

I wanted to share another cool example of partial update and allow_duplicate=True

This example is from @jinnyzor amazing Dash Chart Editor component library. You can find the code for this app (and others using the Dash Chart Editor) in GitHub. You can also find more info in this post: Dash Chart Editor

This app includes updating details in cards created with pattern matching callbacks. There could be dozens of cards with figures in this app. Prior to Dash 2.9 if you wanted to update a figure or delete a card, all the cards would have to make the round trip between the client and server. Now you can target just the thing you want to change.

For example, this callback deletes one of the cards. The only data needed in the callback is which delete button was clicked and the id’s of all the cards in the layout. Then we patch the container with the cards to delete the selected card.


@app.callback(
    Output("pattern-match-container", "children", allow_duplicate=True),
    Input({"type": "dynamic-delete", "index": ALL}, "n_clicks"),
    State({"type": "dynamic-card", "index": ALL}, "id"),
    prevent_initial_call=True,
)
def remove_card(_, ids):
    cards = Patch()
    if ctx.triggered[0]["value"] > 0:
        for i in range(len(ids)):
            if ids[i]["index"] == ctx.triggered_id["index"]:
                del cards[i]
                return cards
    return no_update

This callback saves the figure in the dcc.Graph in the card after it’s edited in the Dash Chart Editor component.


@app.callback(
    Output("pattern-match-container", "children", allow_duplicate=True),
    Input("editor", "figure"),
    State("chartId", "value"),
    State({"type": "dynamic-card", "index": ALL}, "id"),
    prevent_initial_call=True,
)
def save_to_card(f, v, ids):
    if f:
        figs = Patch()
        for i in range(len(ids)):
            if ids[i]["index"] == v:
                figs[i]["props"]["children"][1]["props"]["figure"] = f
                return figs
    return no_update


ezgif.com-resize

5 Likes

Hi!

I wanted to add another example of using partial updates.

Imagine you want to update the markers of all traces in your figure. You could use the following:

import dash
from dash import Input, Output, html, dcc, Patch
import plotly.graph_objects as go
import numpy as np
import random

# generate some data
TRACES = 7
DATAPOINTS = 10

# create figure
figure = go.Figure(layout={'width': 1000, 'height': 800})
for idx in range(TRACES):
    figure.add_scatter(
        x=np.arange(DATAPOINTS),
        y=np.arange(DATAPOINTS) + 10*idx,
        mode='markers',
        marker={'color': 'crimson', 'size': 10},
        name=idx
    )

app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Div(
            [
                html.Button(id='btn', children='reset')
            ],
            style={'width': '10%'}
        ),
        html.Div([
            dcc.Graph(
                id='graph_post',
                figure=figure,
            )
        ]),
    ]
)


@app.callback(
    Output('graph_post', 'figure'),
    Input('btn', 'n_clicks'),
    prevent_initial_call=True
)
def update(_):
    # Creating a Patch object
    patched_figure = Patch()

    # new color for markers
    color = random.choice(['crimson', 'darkgreen', 'black', 'yellow', 'blue', 'orange'])

    for k in range(20):     # note that there are only 7 traces in the figure
        patched_figure["data"][k].update({"marker": {'color': color, 'size': 10}})
    return patched_figure


if __name__ == '__main__':
    app.run(debug=True, port=8051)

There are two drawbacks here:

  • the range has to be greater than the number of traces in the figure
  • the update process gets slower with increasing range
2 Likes

You can find another example for the patch method here:

1 Like