Streaming sensor data at >100 points per second

We’ve been unable to get plotly-dash to smoothly display a constant stream of data.

A few examples we’ve been following so far all use Interval to update the graph:

But in our experience with higher data rates the interval callback can take too long to process. When the interval timer comes due it then re-enters the callback for a second time (while the first callback is processing), and when this happens plotly basically crashes, nothing renders. Using print statements, we can see that the callbacks get backed up and don’t run at smooth intervals.

I made an attempt to work around the re-entrant callback problem using an interval callback which either triggers the graphing callback using a dcc.Store update or raises PreventUpdate depending on whether the last update ran. But I got quirky results where the dcc.Store values, which I used to indicate that the graphing callback completed, weren’t getting updated or weren’t visible to the interval callback.

That approach seems like a hack anyway.

I’m looking for ideas on what to try next. Is there hope for Plotly/Dash to be able to stream 100+ data points per second smoothly? Or perhaps plotly just isn’t made for this use case?

2 Likes

What refresh rate do you need to get what you would consider a smooth update? 5s? 1s? 0.1s?

0.1s is 10 frames per second, that’s generally about the minimum refresh rate that looks smooth to a human. I only want to show 5 seconds of data, so ~500 points to plot as the data scrolls by. If the update happened every second then 100 new data points would show up, so 1/5th of the graph, and when we do that it’s clearly choppy.

I was hoping that a single render of say 10 seconds of data with quick updates to a range property would be possible, but that doesn’t seem like an option.

I also noticed that Plotly V3 had a Streaming API (https://plotly.com/python/v3/streaming-tutorial/) which looks closer to what is needed to achieve this, but as far as I can tell that doesn’t exist in V4, only the interval callbacks which are quite limited at higher frame rates needed to smoothly stream data.

There’s also this old streaming demos plotly repo that hasn’t been updated in 2 years and all the demos don’t work now. But there’s a video showing a streaming voltage trace which is exactly what we’re trying to accomplish.

Have you tried using extendData? that’s the state of the art for dash at the moment

I have not used extendData yet, this sounds promising, but I’m not sure I understand how to apply it.

In the plotly dash docs (https://dash.plotly.com/dash-core-components/graph) I see that extendData is a property of the graph object. Does the callback look like this:

@app.callback(
  output=Output('my-graph', 'extendData'),
  inputs=[Input('interval-component', 'n_intervals')])
def extend_graph(n_intervals):
  return [get_new_data(), get_trace_indices(), get_max_points()]

I just did a few tests. With normal callbacks, i can get down to around 100 ms (which is not perfectly smooth). With clientside callbacks, i can get to updates around 10 ms, which looks smooth to be. I posted an example on the use of extendData in your stack overflow thread.

2 Likes

Oh brilliant, thank you so much for that. We can incorporate the client side callback once we get the server-side method working. 100ms is plenty good enough for a quick proof of concept.

And here is a giff using the clientside update mechanism :slight_smile: . I guess with client side updates, the data flow would be,

  • Stream the data (server->client) in chunks into a Store component every second (or 5s or whatever)
  • Stream the data (client->client) smoothly from the Store to the Graph every 10 ms (or maybe just 50 ms)

Note that the 100 ms i mentioned before was for my setup, it may vary depending on network speed, hardware and update complexity. In general, for anything faster than 1s refresh rate, i would prefer client side updates.

3 Likes

Beautiful example @Emil! Thanks for digging in here, cool strategy with slower store updates coupled with faster clientside updates

Thanks, @chriddyp. I have now updated my answer on stackoverflow with the exmple code :slight_smile:

Hello , I am new to dash, I am currently stuck at a point. I need to stream the video on dashboard and then pause the video with the help of stop button and then wants to save that paused frame of video in my local directory. I am able to stream the video but I am unable to put a stop button in ordert to pause the video and simultaneously save this frame to my local directory. Below is my code. It will be a great help if any help is provided.
import dash
from dash.dependencies import Output, Input, State
import dash_core_components as dcc
import dash_html_components as html
import cv2
import numpy as np
from dash.exceptions import PreventUpdate
import plotly.express as px
from PIL import Image
app = dash.Dash()
server = app.server

app.layout = html.Div(children = [

html.Div([
    html.Div([
        dcc.Upload(id='upload_file_1', children=[html.Button('Upload File 1', id='file_upload_btn_1', n_clicks=0, style={'width': '240px', 'height':'40px'})]
    )], style={'display': 'inline-block'}),
    html.Div(id='file_upload_success_1', style={'display': 'inline-block'}),
]),



html.Button('Submit', id='submit_btn', n_clicks=0,
            style={'width': '300px', 'height':'60px'}),

html.Button('Save', id='submit', n_clicks=0,
            style={'width': '300px', 'height':'60px'}),
html.Video(
    controls=True,
    id='video_player',
    src={},
    autoPlay=False,
    style={'width': "70%"}
),

html.Div(id='display_output')

])

#@app.callback(Output(‘file_upload_success_1’, ‘children’),
#Input(‘file_upload_btn_1’, ‘n_clicks’),
#Input(‘upload_file_1’, ‘contents’),
#State(‘upload_file_1’, ‘filename’))
#def update_success_message(n_clicks, contents, filename):
#if n_clicks and contents:
#return “File ‘{}’ uploaded”.format(filename)

@app.callback(Output(‘video_player’, ‘src’),
Input(‘submit_btn’, ‘n_clicks’),
#Input(‘upload_file_1’, ‘contents’),
)
def update_output(n_clicks):
if n_clicks:
#if video_1 :
video_src=‘http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
else:
dash.no_update
return video_src
@app.callback(Output(‘display_output’, ‘children’),
Input(‘submit’, ‘n_clicks’),
#Input(‘upload_file_1’, ‘contents’),
)
def output(n_clicks):
video_src=‘http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
if n_clicks:
cap = cv2.VideoCapture(video_src)
i = 0 # frame index to save frames
while cap.isOpened():
ret, frame = cap.read()
if ret:
cv2.imwrite(r’C:\Users\FQTQ3VZ\DL_Visualization_Dsahboard\Local Version\testimage\ref’+str(i)+’.jpg’, frame)

       else: 
           break
try:
    img = np.array(Image.open(r'testimage\ref.jpg'))
except OSError:
    raise PreventUpdate

fig = px.imshow(img, color_continuous_scale="gray")
fig.update_layout(coloraxis_showscale=False)
fig.update_xaxes(showticklabels=False)
fig.update_yaxes(showticklabels=False)

return dcc.Graph(figure=fig)

if name == ‘main’:
app.run_server()