我正在尝试使用 Plotly 构建一个带有两个子图的同步可视化:下面的网络图和上面的线图。网络图中的每个节点代表固定 2D 空间中的一个实体,其颜色强度表示其激活级别,该级别随时间变化。线图显示每个节点随时间变化的激活水平。我想使用滑块同时控制两个子图。
每个子图的要求是:
网络图(底部子图):
线图(顶部子图):
这是我的代码和到目前为止的绘图图像,它设置了两个子图并尝试用滑块控制它们。然而,我遇到了一个问题,即network graph节点(下面的子图)不会停留在固定位置;相反,x 轴也会在这里更新。
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
# Example activation levels
times = np.arange(0, 10, 0.5)
nodes = ['A', 'B', 'C']
activation_data = {
'A': np.sin(times),
'B': np.cos(times),
'C': np.sin(times + np.pi / 4)
}
# Define fixed colors for each node
node_colors = {
'A': 'red',
'B': 'blue',
'C': 'green'
}
# Create subplot layout with 2 rows: line plot (row 1) and network graph (row 2)
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.5, 0.5], vertical_spacing=0.15)
# Add line plot traces for each node with constant color, one scatter per time step
line_traces = {node: [] for node in nodes}
for node in nodes:
for t in range(len(times)):
trace = go.Scatter(
x=times[:t+1], # Show data only up to the current time
y=activation_data[node][:t+1],
mode='lines+markers',
name=f'Node {node} Activation',
line=dict(color=node_colors[node]), # Apply constant color for each node
visible=(t == 0) # Initially only the first point is visible
)
fig.add_trace(trace, row=1, col=1)
line_traces[node].append(trace)
# Add network graph traces, one for each time step
for t in range(len(times)):
node_activation_colors = [activation_data[node][t] for node in nodes]
fig.add_trace(
go.Scatter(
x=[1, 2, 3], # Replace with actual node positions
y=[1, 2, 3],
mode='markers',
marker=dict(size=20, color=node_activation_colors, colorscale='Viridis', showscale=False), # Remove color scale
name=f"Network at t={times[t]:.1f}",
visible=(t == 0) # Only first frame visible initially
),
row=2, col=1
)
# Define the slider steps
steps = []
for t in range(len(times)):
step = dict(
method="update",
args=[{"visible": [False] * len(fig.data)}],
label=f"t = {times[t]:.1f}",
)
# Make all traces up to the current time `t` visible for the line plot (row 1)
for node_idx, node in enumerate(nodes):
for i in range(t + 1):
step["args"][0]["visible"][node_idx * len(times) + i] = True
# Enable only the current network graph trace in row 2
step["args"][0]["visible"][len(nodes) * len(times) + t] = True
steps.append(step)
# Add slider to control both plots
sliders = [dict(
active=0,
currentvalue={"prefix": "Time: "},
pad={"t": 50},
steps=steps
)]
# Update layout to remove legend and add slider
fig.update_layout(
sliders=sliders,
height=600,
title="Synchronized Network Graph and Line Plot with Time Slider",
showlegend=False # Disable legend
)
fig.show()
此实现的问题是:
我找到了两个问题的解决方案,我想分享它们,以防其他人遇到类似问题。
问题1:
网络图中的固定位置问题:网络图中的节点没有停留在固定位置,因为 Plotly 根据可见数据自动调整轴范围。当数据更新时,轴会自行重新缩放。
解决方案: 使用 update_xaxes 和 update_yaxes(fixedrange=True)设置网络图子图的固定轴范围并指定范围参数。
问题2:
线图中的累积数据 问题: 我最初为每个时间步长为每个节点创建多个轨迹,这使得控制可见性变得复杂,并且不允许线图累积数据直至选定的时间。
解决方案:在线图中为每个节点创建一条轨迹,并使用 Plotly 的帧和动画功能随时间更新数据。在每一帧中,更新线图迹线的 x 和 y 数据以包含截至选定时间的数据。
更新代码
这是实现这些解决方案的更新代码:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
# Example activation levels
times = np.arange(0, 10, 0.5)
nodes = ['A', 'B', 'C']
activation_data = {
'A': np.sin(times),
'B': np.cos(times),
'C': np.sin(times + np.pi / 4)
}
# Define fixed colors for each node
node_colors = {
'A': 'red',
'B': 'blue',
'C': 'green'
}
# Node positions
node_positions = {
'A': (1, 1),
'B': (2, 2),
'C': (3, 1.5)
}
x_positions = [node_positions[node][0] for node in nodes]
y_positions = [node_positions[node][1] for node in nodes]
# Create subplot layout
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=False,
row_heights=[0.5, 0.5],
vertical_spacing=0.15
)
# Add line plot traces for each node
for node in nodes:
fig.add_trace(
go.Scatter(
x=[], # Empty initial data
y=[],
mode='lines+markers',
name=f'Node {node} Activation',
line=dict(color=node_colors[node]),
),
row=1, col=1
)
# Add network graph trace
initial_colors = [activation_data[node][0] for node in nodes]
fig.add_trace(
go.Scatter(
x=x_positions,
y=y_positions,
mode='markers',
marker=dict(
size=20,
color=initial_colors,
colorscale='Viridis',
showscale=False
),
name="Network Graph",
),
row=2, col=1
)
# Fix axes ranges for network graph
fig.update_xaxes(
range=[0, 4],
fixedrange=True,
row=2, col=1
)
fig.update_yaxes(
range=[0, 3],
fixedrange=True,
row=2, col=1
)
# Create frames for animation
frames = []
for t, time in enumerate(times):
frame_data = []
# Update line plot traces
for i, node in enumerate(nodes):
x_data = times[:t+1]
y_data = activation_data[node][:t+1]
frame_data.append(go.Scatter(
x=x_data,
y=y_data,
mode='lines+markers',
line=dict(color=node_colors[node])
))
# Update network graph colors
node_activation_colors = [activation_data[node][t] for node in nodes]
frame_data.append(go.Scatter(
x=x_positions,
y=y_positions,
mode='markers',
marker=dict(
size=20,
color=node_activation_colors,
colorscale='Viridis',
showscale=False
)
))
frames.append(go.Frame(data=frame_data, name=str(t)))
# Define slider steps
sliders = [dict(
steps=[
dict(
method='animate',
args=(
[str(k)],
dict(
mode='immediate',
frame=dict(duration=0, redraw=True),
transition=dict(duration=0)
)
),
label=f"t={times[k]:.1f}"
) for k in range(len(times))
],
transition=dict(duration=0),
x=0, y=0,
currentvalue=dict(
font=dict(size=12),
prefix='Time: ',
visible=True,
xanchor='center'
),
len=1.0
)]
# Update layout with frames and slider
fig.update(frames=frames)
fig.update_layout(
sliders=sliders,
height=600,
title="Synchronized Network Graph and Line Plot with Time Slider",
)
# Fix axes ranges for line plot
fig.update_xaxes(
range=[times[0], times[-1]],
row=1, col=1
)
fig.show()