如何用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()
功能说明
- 多Y轴布局:每个温度信号对应独立Y轴,避免刻度冲突,便于观察不同量级的温度变化
- 双游标交互:点击图表任意位置可依次设置Cursor A、Cursor B,再次点击重置;游标位置显示所有温度信号的数值
- 时间差计算:设置双游标后自动计算并显示时间差
- 交互操作:X轴自带范围选择器与滑动条,支持缩放、平移;Y轴支持独立缩放
- 多格式数据兼容:替换模拟数据部分为对应的数据读取代码即可加载.csv、.mf4、.dat文件
内容的提问来源于stack exchange,提问作者RanjanN
相关产品推荐
相关产品推荐

