如何实现同一DataFrame不同轴范围多子图的缩放联动?
实现Plotly子图基于关联数据的缩放联动
要实现两个子图的缩放联动(不共享轴),可以通过监听第一个图的布局变化事件,筛选对应的数据后更新第二个图的轴范围。以下提供两种可行方案:
方案1:纯Plotly + 自定义JavaScript回调(适合静态HTML/Jupyter)
这种方案无需额外框架,直接在Plotly图表中嵌入JS逻辑,适合导出静态HTML或在Jupyter Notebook中使用:
import plotly.subplots as subplots import pandas as pd import plotly.graph_objects as go df = pd.DataFrame({ 'a': [1, 2, 3, 4, 5], 'b': [10, 15, 13, 17, 20], 'c': [2, 3, 4, 5, 6], 'd': [8, 12, 10, 14, 16], 'time': [1, 2, 3, 4, 5] }) # 创建子图并添加轨迹 fig = subplots.make_subplots(rows=2, cols=1, subplot_titles=("A vs B", "C vs D")) fig.add_trace(go.Scatter(x=df['a'], y=df['b'], mode='markers+lines'), row=1, col=1) fig.add_trace(go.Scatter(x=df['c'], y=df['d'], mode='markers+lines'), row=2, col=1) # 给轴设置唯一ID,方便JS回调定位 fig.update_xaxes(id='x1', row=1, col=1) fig.update_xaxes(id='x2', row=2, col=1) fig.update_yaxes(id='y2', row=2, col=1) # 添加自定义JS联动逻辑 fig.update_layout( js_on_change=""" function(gd) { // 获取第一个图的x轴当前范围 const xRange = gd.layout.xaxis.range; if (!xRange) return; // 加载原始数据 const data = """ + df.to_json(orient='records') + """; // 筛选出第一个图可见范围内的数据点 const filteredPoints = data.filter(row => row.a >= xRange[0] && row.a <= xRange[1]); if (filteredPoints.length === 0) return; // 计算第二个图需要展示的c、d轴范围(加5%边距避免点贴边) const cVals = filteredPoints.map(p => p.c); const dVals = filteredPoints.map(p => p.d); const cMargin = (Math.max(...cVals) - Math.min(...cVals)) * 0.05 || 0.1; const dMargin = (Math.max(...dVals) - Math.min(...dVals)) * 0.05 || 0.1; // 更新第二个图的轴范围 Plotly.relayout(gd, { 'xaxis2.range': [Math.min(...cVals) - cMargin, Math.max(...cVals) + cMargin], 'yaxis2.range': [Math.min(...dVals) - dMargin, Math.max(...dVals) + dMargin] }); } """ ) fig.show()
逻辑说明
- 监听第一个图的布局变化(缩放、平移都会触发)
- 根据第一个图的x轴范围筛选对应的数据点
- 计算筛选后数据中
c和d的最值,加入小边距后更新第二个图的轴范围 - 保留两个子图各自的刻度体系,避免共享轴导致的可读性问题
方案2:Dash框架实现Python回调(适合Web应用)
如果需要构建交互式Web应用,用Dash的Python回调更灵活,无需写JS:
import dash from dash import dcc, html, Input, Output, State import pandas as pd import plotly.graph_objects as go df = pd.DataFrame({ 'a': [1, 2, 3, 4, 5], 'b': [10, 15, 13, 17, 20], 'c': [2, 3, 4, 5, 6], 'd': [8, 12, 10, 14, 16], 'time': [1, 2, 3, 4, 5] }) app = dash.Dash(__name__) # 页面布局 app.layout = html.Div([ dcc.Graph(id='plot-a-b', figure=go.Figure( data=[go.Scatter(x=df['a'], y=df['b'], mode='markers+lines')], layout=go.Layout(title="A vs B") )), dcc.Graph(id='plot-c-d', figure=go.Figure( data=[go.Scatter(x=df['c'], y=df['d'], mode='markers+lines')], layout=go.Layout(title="C vs D") )) ]) # 联动回调 @app.callback( Output('plot-c-d', 'figure'), Input('plot-a-b', 'relayoutData'), State('plot-c-d', 'figure') ) def update_cd_plot(relayout_data, cd_fig): # 初始状态或无缩放操作时返回原图表 if not relayout_data or 'xaxis.range[0]' not in relayout_data: return cd_fig # 获取第一个图的x轴范围 x_min = relayout_data['xaxis.range[0]'] x_max = relayout_data['xaxis.range[1]'] # 筛选对应数据 filtered_df = df[(df['a'] >= x_min) & (df['a'] <= x_max)] if filtered_df.empty: return cd_fig # 计算轴范围并添加边距 c_min, c_max = filtered_df['c'].min(), filtered_df['c'].max() d_min, d_max = filtered_df['d'].min(), filtered_df['d'].max() c_margin = (c_max - c_min) * 0.05 if c_max != c_min else 0.1 d_margin = (d_max - d_min) * 0.05 if d_max != d_min else 0.1 # 更新第二个图的轴范围 cd_fig['layout']['xaxis']['range'] = [c_min - c_margin, c_max + c_margin] cd_fig['layout']['yaxis']['range'] = [d_min - d_margin, d_max + d_margin] return cd_fig if __name__ == '__main__': app.run_server(debug=True)
适用场景
- 适合需要后端数据处理、用户认证等复杂逻辑的Web应用
- 全程用Python编写,无需掌握JavaScript
内容的提问来源于stack exchange,提问作者K L
相关产品推荐
相关产品推荐

