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

如何用Matplotlib/Plotly实现带双游标与数据提示的多Y轴时序图?

多Y轴时间序列绘图与双游标交互解决方案

需求梳理

  • X轴为时间维度,每个温度信号对应独立Y轴
  • 支持两个可交互游标(Cursor A、Cursor B),游标位置需显示所有信号的数值提示
  • 实时计算并展示双游标之间的时间差
  • X、Y轴支持缩放、平移等交互操作
  • 兼容读取.csv、.mf4、.dat格式的数据

现有代码问题分析

你提供的Matplotlib代码存在以下局限:

  • 仅实现了单组垂直游标,无法支持双游标及差值计算
  • 游标无信号数值提示功能
  • Matplotlib原生交互能力较弱,难以实现复杂的动态提示与差值展示

Plotly的交互式图表组件更适配这类需求,以下是完整实现方案:

数据读取实现

针对不同格式的数据,可通过以下方式读取:

读取.csv文件

import pandas as pd
df = pd.read_csv("data.csv", parse_dates=["timestamp"], index_col="timestamp")

读取.mf4文件(需先安装asammdf库)

from asammdf import MDF
mdf = MDF("data.mf4")
# 提取时间序列及温度信号,转换为DataFrame
df = mdf.to_dataframe()
df.index = df.index.astype("datetime64[ns]")

读取.dat文件(假设为空格分隔格式)

df = pd.read_csv("data.dat", sep="\s+", parse_dates=["timestamp"], index_col="timestamp")

Plotly完整实现代码

import plotly.graph_objects as go
import pandas as pd
import numpy as np

# ---------------------- 模拟数据(可替换为上述读取的真实数据) ----------------------
np.random.seed(42)
time = pd.date_range(start="2024-01-01 00:00:00", periods=100, freq="10s")
temp1 = np.random.normal(25, 2, 100)
temp2 = np.random.normal(35, 3, 100)
temp3 = np.random.normal(15, 1.5, 100)
df = pd.DataFrame({"temp1": temp1, "temp2": temp2, "temp3": temp3}, index=time)

# ---------------------- 创建多Y轴图表 ----------------------
fig = go.Figure()

# 添加第一个温度信号(主Y轴)
fig.add_trace(go.Scatter(
    x=df.index, y=df["temp1"],
    name="温度1",
    yaxis="y",
    line=dict(color="#1f77b4")
))

# 添加第二个温度信号(右侧Y轴1)
fig.add_trace(go.Scatter(
    x=df.index, y=df["temp2"],
    name="温度2",
    yaxis="y2",
    line=dict(color="#ff7f0e")
))

# 添加第三个温度信号(右侧Y轴2)
fig.add_trace(go.Scatter(
    x=df.index, y=df["temp3"],
    name="温度3",
    yaxis="y3",
    line=dict(color="#2ca02c")
))

# 配置多Y轴布局
fig.update_layout(
    xaxis=dict(
        title="时间",
        showgrid=True,
        rangeselector=dict(
            buttons=list([
                dict(count=1, label="1m", step="minute", stepmode="backward"),
                dict(count=5, label="5m", step="minute", stepmode="backward"),
                dict(step="all")
            ])
        ),
        rangeslider=dict(visible=True)
    ),
    yaxis=dict(
        title="温度1 (℃)",
        titlefont=dict(color="#1f77b4"),
        tickfont=dict(color="#1f77b4"),
        showgrid=False
    ),
    yaxis2=dict(
        title="温度2 (℃)",
        titlefont=dict(color="#ff7f0e"),
        tickfont=dict(color="#ff7f0e"),
        anchor="x",
        overlaying="y",
        side="right",
        position=0.92,
        showgrid=False
    ),
    yaxis3=dict(
        title="温度3 (℃)",
        titlefont=dict(color="#2ca02c"),
        tickfont=dict(color="#2ca02c"),
        anchor="x",
        overlaying="y",
        side="right",
        position=0.98,
        showgrid=False
    ),
    legend=dict(x=0, y=1),
    height=600,
    margin=dict(l=50, r=150, t=50, b=50)
)

# ---------------------- 添加双游标与差值计算功能 ----------------------
# 初始化游标位置与差值文本
cursor_a_pos = None
cursor_b_pos = None
diff_annotation = go.layout.Annotation(
    x=0.5, y=1.05,
    xref="paper", yref="paper",
    text="时间差: 0s",
    showarrow=False,
    font=dict(size=14, color="red")
)
fig.add_annotation(diff_annotation)

# 定义点击事件回调(用于设置游标)
def update_cursors(trace, points, selector):
    global cursor_a_pos, cursor_b_pos
    clicked_x = points.xs[0]
    
    # 切换设置游标A/B
    if not cursor_a_pos:
        cursor_a_pos = clicked_x
        # 添加游标A垂直线
        fig.add_shape(type="line",
                      x0=clicked_x, x1=clicked_x, y0=0, y1=1,
                      xref="x", yref="paper",
                      line=dict(color="red", width=2, dash="dash"))
        # 添加游标A数值提示
        fig.add_annotation(
            x=clicked_x, y=1.02,
            xref="x", yref="paper",
            text=f"Cursor A<br>时间: {clicked_x.strftime('%H:%M:%S')}<br>温度1: {df.loc[clicked_x, 'temp1']:.2f}℃<br>温度2: {df.loc[clicked_x, 'temp2']:.2f}℃<br>温度3: {df.loc[clicked_x, 'temp3']:.2f}℃",
            showarrow=True,
            arrowhead=2,
            ax=0, ay=-40
        )
    elif not cursor_b_pos:
        cursor_b_pos = clicked_x
        # 添加游标B垂直线
        fig.add_shape(type="line",
                      x0=clicked_x, x1=clicked_x, y0=0, y1=1,
                      xref="x", yref="paper",
                      line=dict(color="blue", width=2, dash="dash"))
        # 添加游标B数值提示
        fig.add_annotation(
            x=clicked_x, y=0.98,
            xref="x", yref="paper",
            text=f"Cursor B<br>时间: {clicked_x.strftime('%H:%M:%S')}<br>温度1: {df.loc[clicked_x, 'temp1']:.2f}℃<br>温度2: {df.loc[clicked_x, 'temp2']:.2f}℃<br>温度3: {df.loc[clicked_x, 'temp3']:.2f}℃",
            showarrow=True,
            arrowhead=2,
            ax=0, ay=40
        )
        # 计算并更新时间差
        time_diff = abs((cursor_b_pos - cursor_a_pos).total_seconds())
        fig.update_annotations(selector=dict(text="时间差: 0s"),
                               text=f"时间差: {time_diff:.1f}s")
    else:
        # 重置所有游标与提示
        fig.shapes = []
        fig.annotations = [diff_annotation]
        cursor_a_pos = None
        cursor_b_pos = None

# 绑定点击事件到所有轨迹
for trace in fig.data:
    trace.on_click(update_cursors)

# 显示图表
fig.show()

功能说明

  1. 多Y轴布局:每个温度信号对应独立Y轴,避免刻度冲突,便于观察不同量级的温度变化
  2. 双游标交互:点击图表任意位置可依次设置Cursor A、Cursor B,再次点击重置;游标位置显示所有温度信号的数值
  3. 时间差计算:设置双游标后自动计算并显示时间差
  4. 交互操作:X轴自带范围选择器与滑动条,支持缩放、平移;Y轴支持独立缩放
  5. 多格式数据兼容:替换模拟数据部分为对应的数据读取代码即可加载.csv、.mf4、.dat文件

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 13:35:01