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

如何在Dash分面子图间动态添加标注箭头?

如何在Dash+Plotly分面散点图中动态添加子图间的标注箭头

问题背景

我有一个单ID对应多条记录的数据集,示例构造代码如下:

import pandas as pd
import numpy as np
import random

df = pd.DataFrame({'DATE_TIME':pd.date_range('2022-11-01', '2022-11-05 23:00:00',freq='h'),
                   'SBP':[random.uniform(110, 160) for n in range(120)],
                   'DBP':[random.uniform(60, 100) for n in range(120)],
                   'ID':[random.randrange(1, 100) for n in range(120)],
                   'TIMEINTERVAL':[random.randrange(1, 200) for n in range(120)]})

df['VISIT'] = df['DATE_TIME'].dt.day

df['MODE'] = np.select([df['VISIT']==1, df['VISIT'].isin([2,3])], ['New', 'InProgress'], 'Done')

我用以下Dash代码实现了带ID下拉选择器和TIMEINTERVAL范围滑块的交互式分面散点图:

from dash import Dash, html, dcc, Input, Output, callback
import plotly.express as px

app = Dash(__name__)

app.layout = html.Div([
    html.H4('ABPM数据集交互式散点图'),
    dcc.Graph(id="scatter-plot"),
    html.P("按时间间隔筛选:"),
    dcc.Dropdown(df.ID.unique(), id='pandas-dropdown-1'), # ID选择器
    dcc.RangeSlider(
        id='range-slider',
        min=0, max=600, step=10,
        marks={0: '0', 50: '50', 100: '100', 150: '150', 200: '200', 250: '250', 300: '300', 350: '350', 400: '400', 450: '450', 500: '500', 550: '550', 600: '600'},
        value=[0, 600]
    ),
    html.Div(id='dd-output-container')
])

@app.callback(
    Output("scatter-plot", "figure"),
    Input("pandas-dropdown-1", "value"),
    Input("range-slider", "value"),
    prevent_initial_call=True)
def update_scatter_plot(value, slider_range):
    low, high = slider_range
    df1 = df.query("ID == @value & TIMEINTERVAL > @low & TIMEINTERVAL < @high").copy() 
    
    if df1.shape[0] != 0:
        fig = px.scatter(df1, x="DATE_TIME", y=["SBP","DBP"],
                         hover_data=['TIMEINTERVAL'],facet_col='VISIT',
                         facet_col_wrap=2,
                         symbol='MODE')
        
        fig.update_xaxes(matches=None, showticklabels=True)

        return fig
    else: 
        return dash.no_update

app.run_server(debug=True, use_reloader=False)

需求与问题

我希望当筛选后的df1中存在VISIT值大于1的记录时,在分面子图之间添加标注箭头辅助数据读取。尝试添加的代码无法编译:

def update_scatter_plot(value, slider_range):
    low, high = slider_range
    df1 = df.query("ID == @value & TIMEINTERVAL > @low & TIMEINTERVAL < @high").copy() 
    
    if df1.shape[0] != 0:
        fig = px.scatter(df1, x="DATE_TIME", y=["SBP","DBP"],
                         hover_data=['TIMEINTERVAL'],facet_col='VISIT',
                         facet_col_wrap=2,
                         symbol='MODE')
        
        fig.update_xaxes(matches=None, showticklabels=True)

                if df1.VISIT!=1:
                fig.add_annotation(
                xref="x domain",
                yref="y domain",
                # 箭头头部位于x轴从左数25%位置
                x=0.25,
                # 箭头头部位于y轴从下数40%位置
                y=0.4,
                arrowhead=2,
            )

                   return fig
    else: 
        return dash.no_update

当前可视化效果:
当前效果

期望实现的效果:
期望效果

需要实现箭头数量随所选ID的VISIT数量动态变化的功能。

解决方案

问题分析

原代码无法编译的核心原因:

  1. 缩进层级混乱,不符合Python语法规范
  2. 判断逻辑错误:df1.VISIT!=1是对整列的布尔判断,返回数组而非单个布尔值,不能直接作为条件

要实现动态箭头,需先获取当前ID下的有效VISIT列表,再根据分面布局(facet_col_wrap=2)计算子图位置,针对性添加箭头。

修改后的完整回调函数

@app.callback(
    Output("scatter-plot", "figure"),
    Input("pandas-dropdown-1", "value"),
    Input("range-slider", "value"),
    prevent_initial_call=True)
def update_scatter_plot(value, slider_range):
    low, high = slider_range
    df1 = df.query("ID == @value & TIMEINTERVAL > @low & TIMEINTERVAL < @high").copy() 
    
    if df1.shape[0] == 0:
        return dash.no_update
    
    fig = px.scatter(df1, x="DATE_TIME", y=["SBP","DBP"],
                     hover_data=['TIMEINTERVAL'],facet_col='VISIT',
                     facet_col_wrap=2,
                     symbol='MODE')
    
    fig.update_xaxes(matches=None, showticklabels=True)
    
    # 获取当前ID下存在的VISIT值并排序
    unique_visits = sorted(df1['VISIT'].unique())
    # 仅当VISIT数量大于1时添加箭头
    if len(unique_visits) > 1:
        # 遍历每一组相邻VISIT
        for i in range(len(unique_visits)-1):
            current_visit = unique_visits[i]
            next_visit = unique_visits[i+1]
            
            # 计算子图的索引(Plotly分面子图索引从1开始)
            current_idx = current_visit - min(unique_visits) + 1
            next_idx = next_visit - min(unique_visits) + 1
            
            # 判断相邻子图是否在同一行
            if (current_idx - 1) // 2 == (next_idx - 1) // 2:
                # 同一行相邻子图:箭头从左子图右侧指向右子图左侧,垂直居中
                fig.add_annotation(
                    xref=f"x{current_idx} domain",
                    yref=f"y{current_idx} domain",
                    x=1.0,
                    y=0.5,
                    axref=f"x{next_idx} domain",
                    ayref=f"y{next_idx} domain",
                    ax=0.0,
                    ay=0.5,
                    arrowhead=2,
                    arrowsize=1.5,
                    arrowwidth=2,
                    arrowcolor='#666666'
                )
            else:
                # 换行后相邻子图:箭头从上子图底部指向下子图顶部,水平居中
                fig.add_annotation(
                    xref=f"x{current_idx} domain",
                    yref=f"y{current_idx} domain",
                    x=0.5,
                    y=0.0,
                    axref=f"x{next_idx} domain",
                    ayref=f"y{next_idx} domain",
                    ax=0.5,
                    ay=1.0,
                    arrowhead=2,
                    arrowsize=1.5,
                    arrowwidth=2,
                    arrowcolor='#666666'
                )
    
    return fig

代码说明

  1. 获取有效VISIT:先筛选当前ID下存在的VISIT并排序,确保箭头按访问顺序添加
  2. 子图索引计算:通过current_idx = current_visit - min(unique_visits) + 1转换为Plotly分面子图的索引(从1开始)
  3. 箭头位置逻辑:
    • 同一行相邻子图:箭头水平连接,垂直居中
    • 换行后相邻子图:箭头垂直连接,水平居中
  4. 样式统一:设置箭头的大小、宽度和颜色,保证视觉一致性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 12:55:20