
本文详解如何在 Plotly 图表中为时间序列数据(如眼动轨迹)添加与事件标签(e ∈ {0–5})严格对齐、颜色可区分的底部矩形背景,提供基于 add_shape 的逐段渲染与 Heatmap 高效替代两种专业方案。
本文详解如何在 plotly 图表中为时间序列数据(如眼动轨迹)添加与事件标签(e ∈ {0–5})严格对齐、颜色可区分的底部矩形背景,提供基于 `add_shape` 的逐段渲染与 `heatmap` 高效替代两种专业方案。
在可视化眼动追踪等时序行为数据时,常需将原始坐标(x, y)曲线与底层事件标注(如 e = 0 表示眨眼,1=注视,2=扫视等)同步呈现。理想效果是:在时间轴(t)下方或图底区域,用连续、无间隙、颜色随 e 值变化的矩形条直观标示各时刻所属事件类型。然而,Plotly 的 add_shape(type="rect") 不支持数组型 fillcolor——直接传入 e 或其映射色列表会触发 ValueError: Invalid value of type 'numpy.ndarray' received for the 'fillcolor' property。
✅ 方案一:使用 add_shape 逐段绘制(精确控制,适合中小规模数据)
核心思想是将整个时间区间 [t[0], t[-1]] 拆分为 len(e) 个等宽子区间(每个对应一个采样点),为每个区间单独添加一个矩形。关键在于正确计算每个矩形的 x0 和 x1 边界:
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px
# 示例数据(同问题中)
t = np.arange(30)
x = np.array([125.9529, 124.6142, 125.0569, 125.3117, 126.7498, 127.035, 125.4822, 125.6249, 126.9371, 127.6047,
129.031 , 128.2419, 121.521 , 114.7071, 109.4141, 100.5057, 94.9606, 95.2231, 95.9032, 96.4991,
101.2602, 103.9582, 108.2527, 108.8801, 110.3254, 112.8205, 113.0079, 113.3547, 113.0962, 113.2508])
y = np.array([31.218 , 31.236 , 31.147 , 31.2614, 30.806 , 30.8423, 31.727, 32.2256, 32.0504, 32.7774,
34.7089, 37.0671, 46.309 , 55.9716, 62.4481, 68.0248, 75.4912, 79.0622, 81.2176, 83.191 ,
83.7656, 84.6713, 83.9343, 82.4546, 81.1652, 80.7981, 80.2136, 80.7405, 80.4398, 80.0738])
e = np.array([1., 1., 1., 1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.,
3., 3., 3., 3., 3., 3., 4., 4., 4., 4.])
# 创建主图(双曲线 + 底部事件条)
fig = make_subplots()
fig.add_trace(go.Scatter(x=t, y=x, mode='lines', name='X (gaze)'), secondary_y=False)
fig.add_trace(go.Scatter(x=t, y=y, mode='lines', name='Y (gaze)'), secondary_y=False)
# 定义离散色板(确保 e 的每个整数值有唯一颜色)
palette = px.colors.qualitative.D3 # 10色,足够覆盖 0-5
colors = [palette[int(val)] for val in e] # 映射 e → 颜色
# 计算每个矩形的 x 边界:n 个点 → n+1 个分界点(中心对齐)
t_res = np.diff(t).mean() if len(t) > 1 else 1.0 # 时间分辨率(假设等距)
posts = np.append(t, t[-1] + t_res) - t_res / 2 # [t0-Δ/2, t1-Δ/2, ..., t_end+Δ/2]
# 逐段添加矩形(底部高度设为 y 轴范围的 5%)
y_max = max(np.max(x), np.max(y))
y_bottom = 0
y_top = 0.05 * y_max
for i in range(1, len(posts)):
fig.add_shape(
type="rect",
x0=posts[i-1], x1=posts[i],
y0=y_bottom, y1=y_top,
line=dict(color="black", width=0.5), # 细边框提升可读性
fillcolor=colors[i-1],
layer="below" # 确保矩形在曲线之下
)
# 可选:隐藏 y 轴次要刻度以简化底部区域
fig.update_yaxes(range=[-0.02*y_max, 1.05*y_max], secondary_y=False)
fig.update_layout(height=400, title="Eye Tracking: X/Y Trajectory with Event-Based Background")
fig.show()⚠️ 注意事项:
- 此方法生成 len(e) 个独立 shape,当 len(e) > 10⁴ 时可能影响渲染性能;
- posts 构造必须保证相邻矩形无缝拼接(使用 t_res/2 偏移实现“中心采样”语义);
- 若 t 非等距,请改用 np.interp() 或 pd.cut() 动态计算边界。
✅ 方案二:使用 go.Heatmap(高性能、原生支持离散映射)
更优雅且高效的方式是将事件序列 e 视为单行热力图(z = [e]),其 x 轴为 t,y 轴压缩为单层高度。Plotly 热力图天然支持离散色阶与自定义 colorbar:
# 在同一 fig 中添加热力图(替代手动矩形)
fig.add_trace(
go.Heatmap(
z=[e], # 1×N 矩阵
x=t, # 时间轴
y=[0, y_top], # y 范围:从 0 到矩形高度
zmin=0, zmax=5, # 强制色阶覆盖全部事件值
colorscale=[[i/5, palette[i]] for i in range(6)], # 精确映射 0→5
colorbar=dict(
title="Gaze Event",
tickvals=list(range(6)),
ticktext=["Blink", "Fixation", "Saccade", "Pursuit", "Smooth", "Other"],
len=0.4,
yanchor="bottom", y=0.02
),
showscale=True,
hovertemplate="t=%{x:.1f}s<br>Event=%{z}<extra></extra>"
),
secondary_y=False
)
# 关键:设置 yaxis 范围,使热力图紧贴横轴底部
fig.update_yaxes(range=[-0.02*y_max, 1.05*y_max], secondary_y=False)✅ 优势总结:
- 渲染性能优异,万级时间点仍流畅;
- 自动处理坐标对齐与抗锯齿;
- 内置 hovertemplate 与 colorbar 配置,交互体验更专业;
- 支持 colorscale 精确绑定离散值(推荐用 [[v, color], ...] 格式)。
? 最终建议
- 中小数据量( → 选用 add_shape 方案;
- 大数据量、强调性能与可维护性 → 首选 Heatmap 方案;
- 无论哪种方案,务必统一 t 的分辨率逻辑,并在 colorbar 或图例中明确定义 e 值语义(如 Fixation=1),这是科学可视化的基础规范。
通过以上任一方法,你都能在 Plotly 中构建出专业、准确、可解释的眼动事件-轨迹联合视图,为行为分析提供坚实可视化支撑。










