Updating plotly express imshow data in clientside callback

Hello all,

I’m trying to set up a plot that displays an image, and the contents of this image should be updated fairly often (some 10 frames per second or so). At the moment I have this minimum working example, which uses only serverside callbacks:

from dash import Dash, dcc, html
from dash.dependencies import Input, Output, State
import plotly.graph_objects as go
import plotly.express as px
import numpy as np

app = Dash(__name__)

figsize = 100
buffersize = 5
buffer = np.random.random(size=(buffersize, figsize, figsize))  # some random image, quite literally
fig = px.imshow(buffer[0])

colors = {
    'background': '#111111',
    'text': '#7FDBFF'
}

app.layout = html.Div(style={'backgroundColor': colors['background']}, children=[
    html.H1(
        children='Example dashboard',
        style={
            'textAlign': 'center',
            'color': colors['text']
        }
    ),

    html.Div(
        id='div-test',
        children='subtitle, i guess',
        style={
            'textAlign': 'center',
            'color': colors['text']
        }
    ),

    # trigger data updates
    dcc.Interval(
        id='server-interval',
        interval=1000, 
        n_intervals=0      
    ),
    
    # trigger plot updates
    dcc.Interval(
        id='client-interval',
        interval=200,  
        n_intervals=0 
    ),
    
    # we'll store the plot data here from the server side
    dcc.Store(
        id='buffer',
        data=buffer
    ),

    # our plot
    dcc.Graph(
        id='example-graph',
        figure=fig
    )
])

# now we update the buffer from the server side at a modestly slow speed
@app.callback(Output('buffer', 'data'),
              Input('server-interval', 'n_intervals'))
def update_data(n_intervals):
    data = np.random.random(size=(buffersize, figsize, figsize))
    return data

# and finally we use the buffer to update the plot at a higher frequency.
# this is what I'd like to turn into a clientside callback. it works for now, but
#only up to a certain interval size.
@app.callback(Output('example-graph', 'figure'),
              Input('client-interval', 'n_intervals'),
              State('buffer', 'data'),
              State('example-graph', 'figure'))
def update_figure(n_intervals, data, figure):
    figure['data'][0]['z'] = data[n_intervals]
    return figure

# this is what I've tried, but it doesn't work. 
'''
app.clientside_callback(
    """
    function(n_intervals, figure, data){ 
        if (figure === undefined){
            return window.dash_clientside.no_update
        }
        console.log('hello');
        figure['data'][0]['z'] = data[n_intervals];
        return figure;
    }
    """,
    Output('example-graph', 'figure'),
    Input('client-interval', 'n_intervals'),
    State('example-graph', 'figure'),
    State('buffer', 'data')
)
'''

At the end of the code block, I wrote what I’ve tried so far. However, writing the clientside callback as shown there results in a “cannot read properties of undefined (reading ‘figure’)” error. What would be the right way of doing this? If the approach is wrong altogether, feel free to give suggestions on how to approach the problem instead. For extra context, the function that generated data at random will in the future be replaced by something else that obtains actual data, either through a socket or grpc or something of the sort.

Any comments are welcome. Thanks!

Hi @3dd_P and welcome to the Dash community :slight_smile:

You might find this example helpful:

Thanks for the info! I’ve seen this example before, and I have a couple of questions. How would the dict structure change for a 2D image? All of the examples I have found deal with 1D plots with data along the x and y axes. Also, how exactly do the parameters in extendData work? I know there are three parameters and the first and second are the data dictionary and the “trace”, which in my case should probably be [0]. How about the “number of points” parameter in the 2D case? What should this be set to to delete the previous data and keep only the new one?

As a brief update: going by the example shared by AnnMarieW and playing around with the dictionary structure of figures generated via px.imshow, I’ve determined the data is a dictionary of the form

fig_data = {
        'coloraxis': 'coloraxis',
        'name': '0',
        'z': some_data,
        'type': 'heatmap',
        'xaxis': 'x',
        'yaxis': 'y',
        'hovertemplate': 'x: %{x}<br>y: %{y}<br>color: %{z}<extra></extra>'
    }

where ‘z’: data is the quantity I want to change. However, I haven’t managed to get this working with callbacks. I’ve tried

@app.callback(Output('example-graph', 'extendData'),
              Input('client-interval', 'n_intervals'),
              State('buffer', 'data'))
def update_figure(n_intervals, data):
    #make a new data dict to be appended to the figure:
    #print(len(data), len(data[n_intervals]), len(data[n_intervals][0]))
    fig_data = {
        'coloraxis': 'coloraxis',
        'name': '0',
        'z': data[n_intervals],
        'type': 'heatmap',
        'xaxis': 'x',
        'yaxis': 'y',
        'hovertemplate': 'x: %{x}<br>y: %{y}<br>color: %{z}<extra></extra>'
    }
    return [[fig_data], [0]]

where I have tested that data[n_intervals] contains the nested list that I want. However, the function does nothing. No errors either. The plot just doesn’t change. Any tips on how to go about this?

@3dd_P,

It looks like your data isnt big enough for the n_intervals.

The below I got to work by pulling a random int between 0 and the buffersize-1.


import random

from dash import Dash, dcc, html
from dash.dependencies import Input, Output, State
import plotly.graph_objects as go
import plotly.express as px
import numpy as np

app = Dash(__name__)

figsize = 100
buffersize = 5
buffer = np.random.random(size=(buffersize, figsize, figsize))  # some random image, quite literally
fig = px.imshow(buffer[0])

colors = {
    'background': '#111111',
    'text': '#7FDBFF'
}

app.layout = html.Div(style={'backgroundColor': colors['background']}, children=[
    html.H1(
        children='Example dashboard',
        style={
            'textAlign': 'center',
            'color': colors['text']
        }
    ),

    html.Div(
        id='div-test',
        children='subtitle, i guess',
        style={
            'textAlign': 'center',
            'color': colors['text']
        }
    ),

    # trigger data updates
    dcc.Interval(
        id='server-interval',
        interval=1000,
        n_intervals=0
    ),

    # trigger plot updates
    dcc.Interval(
        id='client-interval',
        interval=200,
        n_intervals=0
    ),

    # we'll store the plot data here from the server side
    dcc.Store(
        id='buffer',
        data=buffer
    ),

    # our plot
    dcc.Graph(
        id='example-graph',
        figure=fig
    )
])


# now we update the buffer from the server side at a modestly slow speed
@app.callback(Output('buffer', 'data'),
              Input('server-interval', 'n_intervals'))
def update_data(n_intervals):
    data = np.random.random(size=(buffersize, figsize, figsize))
    return data


# and finally we use the buffer to update the plot at a higher frequency.
# this is what I'd like to turn into a clientside callback. it works for now, but
# only up to a certain interval size.
@app.callback(Output('example-graph', 'figure'),
              Input('client-interval', 'n_intervals'),
              State('buffer', 'data'),
              State('example-graph', 'figure'))
def update_figure(n_intervals, data, figure):
    i = random.randint(0, buffersize-1)
    figure['data'][0]['z'] = data[i]
    return figure

app.run()

heyo, thanks for the input! this looks exactly the same as my original post though, which already worked. the only difference being that you draw a frame of data randomly instead of following a sequence. that was already working in the original code.,the problem really is only about turning this into a clientside callback, possibly (but not necessarily) by using extendData. in the full code, i have another callback that resets the n_intervals and keeps the value within bounds, this was just a MWE

@3dd_P

Ah, my mistake.

app.clientside_callback(
    """function (n_intervals, data, figure) {
        newFig = JSON.parse(JSON.stringify(figure))
        newFig['data'][0]['z'] = data[n_intervals]
        return newFig
    }""",
    Output('example-graph', 'figure'),
    Input('client-interval', 'n_intervals'),
    State('buffer', 'data'),
    State('example-graph', 'figure')
)

I also added the below reset to make sure the data didnt get out of bounds.

@app.callback(Output('buffer', 'data'),
              Output('client-interval', 'n_intervals'),
              Input('server-interval', 'n_intervals'))
def update_data(n_intervals):
    data = np.random.random(size=(buffersize, figsize, figsize))
    return data, 0

Try this.

1 Like

Ah excellent, this works! Before I mark it as a solution, I just have one more question. It seems this still doesn’t manage to get a high frame rate, it starts stuttering if I make the client interval smaller (after adjusting the buffer size appropriately based on the server and client intervals). Is there any method that could let me achieve a higher frame rate (e.g. with extendData, if used correctly), or is this about as good as it gets?

More than likely depends on the amount of data that you are passing and the client machine. Plus, some of the stuttering may be due to the intervals not resetting in time before restarting. Make sure that you have quite a bit of buffersize to be able to have all the updates you want to use. Also, it looks like it will pause when you are pulling new data from the server, due to the blocking nature of the requests.

You could also run into where the dataset is not quite populated when you are trying to update the figure.