plotly dash 实时更新图形数据的两种方法

4,049 阅读5分钟

使用dash构建可实时刷新图标的数据可视化网页,两种可用的回调模式:

第一种:

方法是直接替换整个figure,示例代码

参考来源:

官方文档参考:

dash.plotly.com/basic-callb…

dash.plotly.com/live-update…

实例1:community.plotly.com/t/3d-surfac…

import random
import plotly.graph_objects as go
import numpy as np
import pandas as pd
import dash_html_components as html
import dash_core_components as dcc
import dash
import plotly.graph_objects as go
import dash
from dash.dependencies import Output, Input
import dash_core_components as dcc
import dash_html_components as html
import plotly
import random


# Read data from a csv
z_data = pd.read_csv(
    'https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv')


import numpy as np 
mu, sigma = 0, 50 
# creating a noise with the same dimension as the dataset (2,2) 
noise = np.random.normal(mu, sigma, z_data.shape)
s1 = go.Surface(z=((z_data+noise)/(z_data+noise).max().max()).values)
noise = np.random.normal(mu, sigma, z_data.shape)
s2 = go.Surface(z=((z_data+noise)/(z_data+noise).max().max()).values)
noise = np.random.normal(mu, sigma, z_data.shape)
s3 = go.Surface(z=((z_data+noise)/(z_data+noise).max().max()).values)
noise = np.random.normal(mu, sigma, z_data.shape)
s4 = go.Surface(z=((z_data+noise)/(z_data+noise).max().max()).values)
noise = np.random.normal(mu, sigma, z_data.shape)
s5 = go.Surface(z=((z_data+noise)/(z_data+noise).max().max()).values)

frames = [{"data":[s1]},{"data":[s2]},{"data":[s3]},{"data":[s4]},{"data":[s5]}]

fig = go.Figure(data=[s1],layout=go.Layout(
        xaxis=dict(range=[0, 16], autorange=False),
        yaxis=dict(range=[0, 13], autorange=False),
        hovermode="closest",
        title="Start Title"
    ))


app = dash.Dash(__name__)
app.layout = html.Div(
    [
        dcc.Graph(id='live-graph'),
        dcc.Interval(
            id='graph-update',
            interval=100
        ),
    ]
)

@app.callback(Output('live-graph', 'figure'),
              [Input('graph-update', 'n_intervals')])
def update_graph_scatter(input_data):
    if input_data is None:
        return dash.no_update
    return frames[input_data % 5]


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

实例2:stackoverflow.com/questions/4…

import dash
from dash.dependencies import Output, Event
import dash_core_components as dcc
import dash_html_components as html
from random import random
import plotly

app = dash.Dash(__name__)
app.layout = html.Div(
    html.Div([
        dcc.Graph(id='live-update-graph-scatter', animate=True),
        dcc.Graph(id='live-update-graph-bar'),
        dcc.Interval(
            id='interval-component',
            interval=1*1000
        )
    ])
)


@app.callback(Output('live-update-graph-scatter', 'figure'),
              events=[Event('interval-component', 'interval')])
def update_graph_scatter():

    traces = list()
    for t in range(2):
        traces.append(plotly.graph_objs.Scatter(
            x=[1, 2, 3, 4, 5],
            y=[(t + 1) * random() for i in range(5)],
            name='Scatter {}'.format(t),
            mode= 'lines+markers'
            ))
    return {'data': traces}

@app.callback(Output('live-update-graph-bar', 'figure'),
              events=[Event('interval-component', 'interval')])
def update_graph_bar():

    traces = list()
    for t in range(2):
        traces.append(plotly.graph_objs.Bar(
            x=[1, 2, 3, 4, 5],
            y=[(t + 1) * random() for i in range(5)],
            name='Bar {}'.format(t)
            ))
    layout = plotly.graph_objs.Layout(
    barmode='group'
)
    return {'data': traces, 'layout': layout}


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

第二种:

方法是仅更新图标中的数据部分,利用figuire中的extendData这个API

参考来源:

实例1:stackoverflow.com/questions/6…

题主使用更新整张figuire的办法,效率过低

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go
from dash.dependencies import Input, Output, State
import numpy as np

app = dash.Dash(__name__)

# plot a circle
t = np.linspace(0, 2*np.pi, 10000)  # intentionally use many points to exaggerate the issue
x = np.cos(t)
y = np.sin(t)
z = np.zeros_like(t)
marker_size = 4
fig = go.Figure()
fig.add_trace(go.Scatter3d(x=x, y=y, z=z, mode='lines'))
fig.add_trace(go.Scatter3d(
    x=[0.0, 0.0], y=[0.0, 0.0], z=[0.0, 0.0],
    marker=go.scatter3d.Marker(size=marker_size),
    line=dict(width=3.0),
    showlegend=False,
))

fig.update_layout(
    uirevision='constant',
    autosize=False,
    width=900,
    height=900,
    scene=dict(
        xaxis=dict(range=[-1, 1]),
        yaxis=dict(range=[-1, 1]),
        zaxis=dict(range=[-1, 1.0]),
        aspectratio=dict(x=2, y=2, z=2),
    ),
)

app.layout = html.Div(children=[
    dcc.Graph(
        id='example-graph',
        figure=fig
    ),
    html.Div(
        [
            dcc.Slider(
                id='slider-phi',
                min=0.0,
                max=360.0,
                step=1.0,
                value=0.0,
                marks={0: '0', 180: '180', 360: '360'},
                updatemode='drag',
            ),
        ],
        style=dict(width='50%'),
    ),
    html.Div(children='', id='output-box'),
])

@app.callback(
    Output('example-graph', 'figure'),
    [Input('slider-phi', 'value')],
    [State('example-graph', 'figure')]
)
def display_structure(phi, myfig):
    myfig['data'][1]['x'][1] = np.cos(np.radians(phi))
    myfig['data'][1]['y'][1] = np.sin(np.radians(phi))
    myfig['data'][1]['z'][1] = 0

    return myfig

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

得到建议使用extendData这个API更新,答主同时给出了在服务端和客户端处理的代码

@app.callback(Output('example-graph', 'extendData'), [Input('slider-phi', 'value')])
def update_data(phi):
    # tuple is (dict of new data, target trace index, number of points to keep)
    return dict(x=[[0, np.cos(np.radians(phi))]], y=[[0, np.sin(np.radians(phi))]]), [1], 2
app.clientside_callback(
    """
    function(phi) {
        // tuple is (dict of new data, target trace index, number of points to keep)
        return [{x: [[0, Math.cos(phi/180*Math.PI)]], y:[[0, Math.sin(phi/180*Math.PI)]]}, [1], 2]
    }
    """, Output('example-graph', 'extendData'), [Input('slider-phi', 'value')]
)
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objects as go
from dash.dependencies import Input, Output, State
import numpy as np

app = dash.Dash(__name__)

# plot a circle
t = np.linspace(0, 2*np.pi, 10000)  # intentionally use many points to exaggerate the issue
x = np.cos(t)
y = np.sin(t)
z = np.zeros_like(t)
marker_size = 4
fig = go.Figure()
fig.add_trace(go.Scatter3d(x=x, y=y, z=z, mode='lines'))
fig.add_trace(go.Scatter3d(
    x=[0.0, 0.0], y=[0.0, 0.0], z=[0.0, 0.0],
    marker=go.scatter3d.Marker(size=marker_size),
    line=dict(width=3.0),
    showlegend=False,
))

fig.update_layout(
    uirevision='constant',
    autosize=False,
    width=900,
    height=900,
    scene=dict(
        xaxis=dict(range=[-1, 1]),
        yaxis=dict(range=[-1, 1]),
        zaxis=dict(range=[-1, 1.0]),
        aspectratio=dict(x=2, y=2, z=2),
    ),
)

app.layout = html.Div(children=[
    dcc.Graph(
        id='example-graph',
        figure=fig
    ),
    html.Div(
        [
            dcc.Slider(
                id='slider-phi',
                min=0.0,
                max=360.0,
                step=1.0,
                value=0.0,
                marks={0: '0', 180: '180', 360: '360'},
                updatemode='drag',
            ),
        ],
        style=dict(width='50%'),
    ),
    html.Div(children='', id='output-box'),
])

app.clientside_callback(
    """
    function(phi) {
        // tuple is (dict of new data, target trace index, number of points to keep)
        return [{x: [[0, Math.cos(phi/180*Math.PI)]], y:[[0, Math.sin(phi/180*Math.PI)]]}, [1], 2]
    }
    """, Output('example-graph', 'extendData'), [Input('slider-phi', 'value')]
)

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

实例2:stackoverflow.com/questions/6…

简单记一个示例的部分代码

@app.callback(
    Output('indicators', 'extendData'),
    Input('live_interval', 'n_intervals')
)
def update_rates(n_intervals):
    #gather data into dict from dataframe
    new_data = candles[['time', 'high', 'low', 'open', 'close']].iloc[-1]
    new_data['time'] = new_data['time'] + delta*n_intervals
    new_data = new_data.to_dict()

    #data rename
    new_data['x'] = new_data['time']
    del new_data['time']

    #data reshape
    for k in new_data.keys():
        new_data[k] = [[new_data[k]]]

    return new_data, [1]

实例3:stackoverflow.com/questions/6…

详细讲解

Updating traces of a Graph component without generating a new graph object can be achieved via the extendData property. Here is a small example that appends data each second,

import dash
import dash_html_components as html
import dash_core_components as dcc
import numpy as np

from dash.dependencies import Input, Output

# Example data (a circle).
resolution = 20
t = np.linspace(0, np.pi * 2, resolution)
x, y = np.cos(t), np.sin(t)
# Example app.
figure = dict(data=[{'x': [], 'y': []}], layout=dict(xaxis=dict(range=[-1, 1]), yaxis=dict(range=[-1, 1])))
app = dash.Dash(__name__, update_title=None)  # remove "Updating..." from title
app.layout = html.Div([dcc.Graph(id='graph', figure=figure), dcc.Interval(id="interval")])


@app.callback(Output('graph', 'extendData'), [Input('interval', 'n_intervals')])
def update_data(n_intervals):
    index = n_intervals % resolution
    # tuple is (dict of new data, target trace index, number of points to keep)
    return dict(x=[[x[index]]], y=[[y[index]]]), [0], 10


if __name__ == '__main__':
    app.run_server()

Depending of the network connection between client and server (at each update, a request is exchanged between client and server), this approach works up to a refresh rate of around 1s.

If you need a higher refresh rate, i would suggest doing the graph update using a client side callback. Adopting the previous example, the code would be along the lines of

import dash
import dash_html_components as html
import dash_core_components as dcc
import numpy as np

from dash.dependencies import Input, Output, State

# Example data (a circle).
resolution = 1000
t = np.linspace(0, np.pi * 2, resolution)
x, y = np.cos(t), np.sin(t)
# Example app.
figure = dict(data=[{'x': [], 'y': []}], layout=dict(xaxis=dict(range=[-1, 1]), yaxis=dict(range=[-1, 1])))
app = dash.Dash(__name__, update_title=None)  # remove "Updating..." from title
app.layout = html.Div([
    dcc.Graph(id='graph', figure=dict(figure)), dcc.Interval(id="interval", interval=25),
    dcc.Store(id='offset', data=0), dcc.Store(id='store', data=dict(x=x, y=y, resolution=resolution)),
])
app.clientside_callback(
    """
    function (n_intervals, data, offset) {
        offset = offset % data.x.length;
        const end = Math.min((offset + 10), data.x.length);
        return [[{x: [data.x.slice(offset, end)], y: [data.y.slice(offset, end)]}, [0], 500], end]
    }
    """,
    [Output('graph', 'extendData'), Output('offset', 'data')],
    [Input('interval', 'n_intervals')], [State('store', 'data'), State('offset', 'data')]
)

if __name__ == '__main__':
    app.run_server()

Client side updates should be fast enough to achieve a smooth update. The gif below shows the above example running with 25 ms refresh rate,

Keep in mind that a client side update is only possible if the data is already present client side, i.e. another mechanism is needed to fetch the data from the server. A possible data flow could be

Use a slow Interval component (e.g. 2 s) to trigger a (normal) callback that fetches a chunk of data from the source and places it in a Store component

Use a fast Interval component (e.g. 25 ms) to trigger a client side callback that streams data from the Store component to the Graph component