Patch in Clientside Callbacks

Hey everyone!

I was wondering if it’s possible to use Patch() in clientside callbacks? I’m working on a Dash app where I want to highlight part of a figure (with a purple or grey bar) whenever a bar is selected.

The only problem is that the chart flashes, even though I’m using Patch to try to make the update smoother. Any ideas on how to fix this?

Here’s the code I’m using along with a GIF to show what I mean:

Recording-20241107_115750

Serverside Callback, which is working:

@dash_app.callback(
    Output("bar_chart", "figure", allow_duplicate=True),
    Input("bar_chart", "clickData"),
    Input("reload-chart", "n_clicks"),
    prevent_initial_call=True
)   
def draw_on_chart(click, reload):
    INDICATOR_TYPE = 'purple' # 'grey'
    trigger_id = ctx.triggered_id
    patched_figure = Patch()

    if trigger_id == 'bar_chart':

        patched_figure.layout.shapes.clear()

        if INDICATOR_TYPE == 'purple':
            x_index_range = (click['points'][0]['pointIndex'] - .3, click['points'][0]['pointIndex'] + .3)
            patched_figure.layout.shapes.append({'type': 'rect', 'xref': 'x', 'yref': 'paper', 'x0': x_index_range[0], 'x1': x_index_range[1], 'y0': -0.2, 'y1': -0.22, 'line': {'color': '#6F326A'}, 'fillcolor': '#6F326A', 'opacity': 1})
        elif INDICATOR_TYPE == 'grey':
            x_index_range = (click['points'][0]['pointIndex'] - .45, click['points'][0]['pointIndex'] + .45)
            patched_figure.layout.shapes.append({'type': 'rect', 'xref': 'x', 'yref': 'paper', 'x0': x_index_range[0], 'x1': x_index_range[1], 'y0': -0.02, 'y1': -0.2, 'line': {'color': '#DCDCDC'}, 'fillcolor': '#DCDCDC', 'opacity': 0.3})

    elif trigger_id == 'reload-chart':
        patched_figure.layout.shapes.clear()
    else:
        return no_update
    
    return patched_figure

Clientside Callback, which didn’t worked:

dash_app.clientside_callback(
    """
    function(clickData, n_clicks) {
        let INDICATOR_TYPE = 'grey';
        let patchedFigure = { layout: { shapes: [] } };
        let trigger_id = dash_clientside.callback_context.triggered[0].prop_id;

        console.log(trigger_id);

        if (trigger_id === "bar_chart.clickData") {
            if (clickData && clickData.points && clickData.points.length > 0) {
                const pointIndex = clickData.points[0].pointIndex;
                patchedFigure.layout.shapes = [];  // Clear existing shapes

                if (INDICATOR_TYPE === 'purple') {
                    let x_index_range = [pointIndex - 0.3, pointIndex + 0.3];
                    patchedFigure.layout.shapes.push({
                        type: 'rect',
                        xref: 'x',
                        yref: 'paper',
                        x0: x_index_range[0],
                        x1: x_index_range[1],
                        y0: -0.2,
                        y1: -0.22,
                        line: { color: '#6F326A' },
                        fillcolor: '#6F326A',
                        opacity: 1
                    });
                } else if (INDICATOR_TYPE === 'grey') {
                    let x_index_range = [pointIndex - 0.45, pointIndex + 0.45];
                    patchedFigure.layout.shapes.push({
                        type: 'rect',
                        xref: 'x',
                        yref: 'paper',
                        x0: x_index_range[0],
                        x1: x_index_range[1],
                        y0: -0.02,
                        y1: -0.2,
                        line: { color: '#DCDCDC' },
                        fillcolor: '#DCDCDC',
                        opacity: 0.3
                    });
                }
            }
        } else if (trigger_id === "reload-chart.n_clicks") {
            patchedFigure.layout.shapes = [];  // Clear shapes
        }
        return patchedFigure;
    }
    """,
        Output("bar_chart", "figure", allow_duplicate=True),
        [Input("bar_chart", "clickData"),
        Input("reload-chart", "n_clicks")],
        prevent_initial_call=True
    )

Thanks for your patience :slight_smile:

Hello @rvanzelotti,

Welcome to the community!

There isnt current a way to use Patch on the clientside, however, you can create a new figure:

patchedFigure = JSON.parse(JSON.stringify(figure))

Make your adjustments to it and then return the figure. You’ll need to pull the state of the component.

As far as the flicker you are seeing, I’m wondering if you have a loading spinner on it. If so, you can target specific props instead or even have a delay on the timing of it.

2 Likes

I have a dmc.LoadringOverlay component on it, which was a frustrated attempt to smooth things out, but the flick behavior does not vanish without the Loading component. Thanks for the suggestion on creating a new figure, but I don’t believe that’s the way to go on this one.

Do you know if there are any way to stop the flickering? I’ve seen a similar issue here:
Screen flickering while live updating - Dash Python - Plotly Community Forum
But I can’t disable the CSS behind the project. Is that what chriddyp meants when he says “do you have loading css?”

You can use a dcc.Loading to and use targeted props of the children components.

You wouldnt be creating an entirely new figure, just creating a copy of the original (which is essentially what Patch does) and then updates the specific pieces in the figure where you are wanting to adjust things. You cant modify props in place, like manipulating the figure without using a copy will not update at all. That’s why you have to break the link.

Patch was created to help with bandwidth on the server, since you are using clientside, all of the speed is available because you are already on the client’s browser. Making this pretty quick.