Black Lives Matter. Please consider donating to Black Girls Code today.

Streaming Graphs -- weird behavior

I have been experimenting with streaming data to graphs. I use an interval element’s n_clicks property as the input and I output using a graph’s extendData property (+ just a div I can write text to).

Using regular Python callbacks I can get this working, but it is not as smooth as I would like (I tried both with animate=True and animate=False on the graph but do not see much difference).

I suspected this behavior was because I was making so many server requests, so I changed it to be entirely client-side (using synthetic data for now, that I can easily generate either on the server or client). However, things get much worse in this case, and the graph only seems to be updating at about 1Hz even though the callback is being triggered much more frequently (and my text in the div is clearly getting update more frequently).

Any suggestions for how to make these updates appear more smooth? Why does setting animate=True not appear to have any effect? Why do I see these slow figure updates when using client side callbacks?

Thanks!

For reference here is my code I am playing with

import datetime

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from dash.dependencies import Input, Output, ClientsideFunction
import datetime
import numpy as np
import time

CLIENTSIDE = True

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

fig = go.Figure()
fig.add_trace(go.Scattergl(x=[n * .1 for n in range(100)], y=[None for _ in range(100)], mode='lines'))
fig.update_yaxes(range=[-1, 1])

app.layout = html.Div(
    html.Div([
        html.H4('Stream Test'),
        html.Div(id='live-update-text'),
        dcc.Graph(id='live-update-graph', figure=fig, animate=False),
        dcc.Interval(
            id='interval-component',
            interval=50, # in milliseconds
            n_intervals=0
        )
    ])
)

if not CLIENTSIDE:
    start_time = None
    app_start_time = time.time()
    @app.callback([Output('live-update-graph', 'extendData'), Output('live-update-text', 'children')],
                  [Input('interval-component', 'n_intervals')])
    def update_graph_live(n):
        global start_time
        if start_time:
            elapsed = time.time() - start_time
            values = np.arange(start_time - app_start_time, start_time + elapsed - app_start_time, 0.05)
            y=[np.sin(i) for i in values]
            return_values = (dict(y=[y], x=[values]), 0, 200), str(n) + ' -- ' + datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
        else:
            return_values = (dash.no_update, dash.no_update)
        start_time = time.time()
        return return_values

else:

    app.clientside_callback(
        ClientsideFunction(
                namespace='clientside',
                function_name='updateGraph'
            ),
       [Output('live-update-graph', 'extendData'), Output('live-update-text', 'children')],
       [Input('interval-component', 'n_intervals')]
    )

if __name__ == '__main__':
    app.run_server(debug=True, host='0.0.0.0')
    

Javascript:

if(!window.dash_clientside) {window.dash_clientside = {};}
window.dash_clientside.clientside = {


    updateGraph: function (n) {
        if (!window.loadStartTime) {
            window.loadStartTime = Math.floor(Date.now() / 1000);
        }
        
        if (window.startTime) {
            let elapsed = Math.floor(Date.now() / 1000) - window.startTime;
            var values = new Array(Math.floor(elapsed / 0.05));
            var y = new Array(Math.floor(elapsed / 0.05));
            for(var i = 0; i < values.length; i++){
              values[i] = window.startTime - window.loadStartTime + i * 0.05;
              y[i] = Math.sin(values[i]);
            }
            returnValues = [[{'y': [y], 'x': [values]}, 0, 200], n.toString()]
        } else {
            returnValues = [window.dash_clientside.no_update, window.dash_clientside.no_update]
        }
        window.startTime = Math.floor(Date.now() / 1000);
        //console.log(returnValues);
        return returnValues;
    }
};

I realized that I made some errors, and over-complicated things with the timing, with the following JavaScript code this is working much better:

if(!window.dash_clientside) {window.dash_clientside = {};}

const intervalLength = 0.1; // seconds
const samplePeriod = 0.05;
const xRange = 30;
const maxPoints = xRange / samplePeriod;

window.dash_clientside.clientside = {
    updateGraph: function (n) {
        let elapsed = n * intervalLength;
        let numValues = Math.floor(intervalLength / samplePeriod);

        let x = new Array(numValues);
        let y = new Array(numValues);

        for(let i = 0; i < numValues; i++){
            x[i] = elapsed + i * samplePeriod;
            y[i] = Math.sin(x[i]);
        }
        return [[{'y': [y], 'x': [x]}, 0, maxPoints], n.toString()];
    }
};

Even though this updating more smoothly and almost what I would like, there is still quite a bit of flicker in the display. Is it possible to somehow double buffer updates like this so that it appears more smoothly?