You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免Plotly多回调场景下的竞态条件?

解决Dash仪表盘竞态条件与部分更新问题

问题背景

我有一个类似TERRA卫星实时Feed的Dash仪表盘,包含三个独立输入触发回调:

  • dcc.Interval定时器:更新fig['data']
  • 图表缩放触发的relayoutData:更新fig['layout']['xaxis']
  • 按钮点击:更新fig['layout']['yaxis']

当前存在竞态风险:比如Interval触发后开始更新数据,此时用户缩放图表触发relayout回调,之后Interval返回完整figure对象,会覆盖掉relayout的修改,导致界面显示异常。

解决方案

1. 使用Dash部分属性更新(Partial Property Updates)

Dash支持直接返回要修改的figure属性子集,而非整个figure对象,这样不同回调只会更新各自负责的部分,不会互相覆盖。

修改回调结构,针对不同触发源返回对应属性的更新:

import datetime
import dash
from dash import dcc, html, ctx
import plotly
from dash.dependencies import Input, Output, State
from pyorbital.orbital import Orbital

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

# 初始化figure
satellite = Orbital('TERRA')
def init_figure():
    data = {
        'time': [],
        'Latitude': [],
        'Longitude': [],
        'Altitude': []
    }
    for i in range(180):
        time = datetime.datetime.now() - datetime.timedelta(seconds=i*20)
        lon, lat, alt = satellite.get_lonlatalt(time)
        data['Longitude'].append(lon)
        data['Latitude'].append(lat)
        data['Altitude'].append(alt)
        data['time'].append(time)
    
    fig = plotly.tools.make_subplots(rows=2, cols=1, vertical_spacing=0.2)
    fig['layout']['margin'] = {'l': 30, 'r': 10, 'b': 30, 't': 10}
    fig['layout']['legend'] = {'x': 0, 'y': 1, 'xanchor': 'left'}
    fig.append_trace({
        'x': data['time'],
        'y': data['Altitude'],
        'name': 'Altitude',
        'mode': 'lines+markers',
        'type': 'scatter'
    }, 1, 1)
    fig.append_trace({
        'x': data['Longitude'],
        'y': data['Latitude'],
        'text': data['time'],
        'name': 'Longitude vs Latitude',
        'mode': 'lines+markers',
        'type': 'scatter'
    }, 2, 1)
    return fig

app.layout = html.Div([
    html.H4('TERRA Satellite Live Feed'),
    html.Div(id='live-update-text'),
    dcc.Graph(id='live-update-graph', figure=init_figure()),
    dcc.Interval(
        id='interval-component',
        interval=1*1000,
        n_intervals=0
    ),
    html.Button('调整Y轴', id='yaxis-btn')
])

# Interval触发:仅更新data部分
@app.callback(
    Output('live-update-graph', 'figure.data'),
    Input('interval-component', 'n_intervals')
)
def update_data(n):
    satellite = Orbital('TERRA')
    data = {
        'time': [],
        'Latitude': [],
        'Longitude': [],
        'Altitude': []
    }
    for i in range(180):
        time = datetime.datetime.now() - datetime.timedelta(seconds=i*20)
        lon, lat, alt = satellite.get_lonlatalt(time)
        data['Longitude'].append(lon)
        data['Latitude'].append(lat)
        data['Altitude'].append(alt)
        data['time'].append(time)
    
    # 返回两个trace的data,对应原figure的data列表
    return [
        {'x': data['time'], 'y': data['Altitude'], 'name': 'Altitude', 'mode': 'lines+markers', 'type': 'scatter'},
        {'x': data['Longitude'], 'y': data['Latitude'], 'text': data['time'], 'name': 'Longitude vs Latitude', 'mode': 'lines+markers', 'type': 'scatter'}
    ]

# relayout触发:仅更新xaxis布局
@app.callback(
    Output('live-update-graph', 'figure.layout.xaxis'),
    Input('live-update-graph', 'relayoutData')
)
def update_xaxis(relayout_data):
    if relayout_data and 'xaxis.range[0]' in relayout_data:
        # 根据relayout数据设置x轴范围,这里可按需调整逻辑
        return {
            'range': [relayout_data['xaxis.range[0]'], relayout_data['xaxis.range[1]']],
            'autorange': False
        }
    # 无缩放时保持自动缩放
    return {'autorange': True}

# 按钮触发:仅更新yaxis布局
@app.callback(
    Output('live-update-graph', 'figure.layout.yaxis'),
    Input('yaxis-btn', 'n_clicks')
)
def update_yaxis(n_clicks):
    if n_clicks:
        # 示例:点击按钮后设置Y轴范围为特定值,可自定义逻辑
        return {'range': [0, 1000], 'autorange': False}
    return {'autorange': True}

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

2. 结合dcc.Store维护全局状态

如果需要更复杂的状态管理,可以用dcc.Store存储当前figure的完整状态,每次回调基于最新状态修改后再存回,确保所有操作都基于最新数据:

# 在layout中添加dcc.Store
app.layout = html.Div([
    # ... 原有组件 ...
    dcc.Store(id='figure-store', data=init_figure())
])

# 修改回调,基于Store中的状态更新
@app.callback(
    Output('figure-store', 'data'),
    Output('live-update-graph', 'figure'),
    Input('interval-component', 'n_intervals'),
    Input('live-update-graph', 'relayoutData'),
    Input('yaxis-btn', 'n_clicks'),
    State('figure-store', 'data'),
    prevent_initial_call=False
)
def update_figure(n, relayout_data, clicks, current_fig):
    triggered_id = ctx.triggered_id
    fig = plotly.graph_objects.Figure(current_fig)
    
    if triggered_id == 'interval-component':
        # 更新data部分
        satellite = Orbital('TERRA')
        data = {
            'time': [],
            'Latitude': [],
            'Longitude': [],
            'Altitude': []
        }
        for i in range(180):
            time = datetime.datetime.now() - datetime.timedelta(seconds=i*20)
            lon, lat, alt = satellite.get_lonlatalt(time)
            data['Longitude'].append(lon)
            data['Latitude'].append(lat)
            data['Altitude'].append(alt)
            data['time'].append(time)
        
        fig.data[0].x = data['time']
        fig.data[0].y = data['Altitude']
        fig.data[1].x = data['Longitude']
        fig.data[1].y = data['Latitude']
        fig.data[1].text = data['time']
    
    elif triggered_id == 'live-update-graph':
        # 更新xaxis
        if relayout_data and 'xaxis.range[0]' in relayout_data:
            fig.layout.xaxis.range = [relayout_data['xaxis.range[0]'], relayout_data['xaxis.range[1]']]
            fig.layout.xaxis.autorange = False
    
    elif triggered_id == 'yaxis-btn':
        # 更新yaxis
        fig.layout.yaxis.range = [0, 1000]
        fig.layout.yaxis.autorange = False
    
    return fig.to_dict(), fig

核心原理

  • 部分属性更新:Dash允许直接针对figure.data、figure.layout.xaxis等子属性设置输出,每个回调只修改自己负责的部分,不会覆盖其他回调的修改。
  • 状态存储:通过dcc.Store保存最新的figure状态,所有回调都基于这个最新状态修改,避免使用过期数据,彻底解决竞态问题。

内容的提问来源于stack exchange,提问作者Cauder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 15:41:13