この記事で学べること#
make_subplots()による複雑なレイアウト構築specs配列によるサブプロットタイプの指定rowspanを使った複数行にまたがるプロット- 3D + 2Dグラフの統合表示
secondary_yによる2軸グラフの作成- レイアウト調整(間隔、サイズ、比率)
対象読者#
- D-3「Plotly.js 3D軌道プロット」を読んだ方
- D-6「Time Marker実装」を読んだ方
- 複数のグラフを統合したダッシュボードを作りたい上級者
本記事では、Plotly.jsのmake_subplots()を使って、3D軌道プロットと複数の時系列グラフを統合したダッシュボードを作成する方法を解説します。
サブプロットレイアウトの基礎#
基本的な使い方#
from plotly.subplots import make_subplots
import plotly.graph_objects as go
# 2行2列のサブプロット作成
fig = make_subplots(rows=2, cols=2)
# 各サブプロットにトレースを追加
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[4, 5, 6]), row=1, col=1)
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[7, 8, 9]), row=1, col=2)
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[10, 11, 12]), row=2, col=1)
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[13, 14, 15]), row=2, col=2)
fig.show()レイアウトの構造#
┌────────┬────────┐
│ (1,1) │ (1,2) │ row=1
├────────┼────────┤
│ (2,1) │ (2,2) │ row=2
└────────┴────────┘
col=1 col=2specs配列: サブプロットタイプの指定#
specsの基本構造#
specs配列は、各サブプロットのタイプ(2D/3D)や結合を指定します。
fig = make_subplots(
rows=2,
cols=2,
specs=[
[{"type": "xy"}, {"type": "xy"}], # row 1: 2D × 2
[{"type": "scene"}, {"type": "xy"}] # row 2: 3D, 2D
]
)主要なタイプ#
| タイプ | 説明 | 用途 |
|---|---|---|
"xy" |
2Dグラフ(デフォルト) | 時系列、散布図、棒グラフ |
"scene" |
3Dグラフ | 3D軌道、3D散布図 |
"polar" |
極座標グラフ | レーダーチャート |
rowspan/colspan: セルの結合#
rowspanの使い方#
複数行にまたがるサブプロットを作成できます。
fig = make_subplots(
rows=3,
cols=2,
specs=[
[{"type": "scene", "rowspan": 3}, {"type": "xy"}], # 左列は3行結合
[None, {"type": "xy"}], # Noneは結合により使用済み
[None, {"type": "xy"}] # Noneは結合により使用済み
]
)レイアウト図:
┌─────────┬─────────┐
│ │ (1,2) │
│ (1,1) ├─────────┤
│ 3D │ (2,2) │
│ (rowspan│ │
│ =3) ├─────────┤
│ │ (3,2) │
└─────────┴─────────┘colspanの使い方#
fig = make_subplots(
rows=2,
cols=3,
specs=[
[{"type": "xy", "colspan": 2}, None, {"type": "xy"}], # 左は2列結合
[{"type": "xy"}, {"type": "xy"}, {"type": "xy"}]
]
)レイアウト図:
┌───────────────┬─────┐
│ (1,1) │(1,3)│
│ colspan=2 │ │
├─────┬─────┬───┴─────┤
│(2,1)│(2,2)│ (2,3) │
└─────┴─────┴─────────┘実用例: 統合ダッシュボード#
レイアウト設計#
目標: 左列に3D軌道、右列に3つの時系列グラフ
┌──────────────┬──────────────┐
│ │ 高度・速度 │
│ │ │
│ 3D軌道 ├──────────────┤
│ (rowspan │ 姿勢角 │
│ =3) │ │
│ ├──────────────┤
│ │ 制御入力 │
└──────────────┴──────────────┘実装コード#
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import pandas as pd
import numpy as np
# CSVデータ読み込み
df = pd.read_csv('flight_data.csv')
# サブプロット作成
fig = make_subplots(
rows=3,
cols=2,
specs=[
[{"type": "scene", "rowspan": 3}, {"type": "xy", "secondary_y": True}], # 3D + 高度/速度(2軸)
[None, {"type": "xy"}], # 姿勢角
[None, {"type": "xy"}] # 制御入力
],
column_widths=[0.5, 0.5], # 左50%, 右50%
row_heights=[0.33, 0.33, 0.34], # ほぼ均等
horizontal_spacing=0.08, # 列間隔 8%
vertical_spacing=0.08 # 行間隔 8%
)
# トレース追加については次のセクションで解説トレースの追加#
3D軌道プロット(左列)#
# 3D軌道データ(ENU座標系: East-North-Up)
x_enu = df['x_enu_m'].values
y_enu = df['y_enu_m'].values
z_enu = df['altitude_m'].values
fig.add_trace(
go.Scatter3d(
x=x_enu,
y=y_enu,
z=z_enu,
mode='lines',
line=dict(color='blue', width=4),
name='Trajectory',
showlegend=False
),
row=1, col=1 # 左列(rowspan=3により全体に表示)
)高度・速度(右列上段、2軸グラフ)#
time = df['time'].values
# 高度(主軸、左Y軸)
fig.add_trace(
go.Scatter(
x=time,
y=df['altitude_m'],
mode='lines',
line=dict(color='green', width=3),
name='Altitude (m)'
),
row=1, col=2,
secondary_y=False # 主軸(左Y軸)
)
# 速度(副軸、右Y軸)
fig.add_trace(
go.Scatter(
x=time,
y=df['airspeed_m_s'],
mode='lines',
line=dict(color='orange', width=3),
name='Speed (m/s)'
),
row=1, col=2,
secondary_y=True # 副軸(右Y軸)
)
# Y軸ラベル設定
fig.update_yaxes(title_text="Altitude [m]", row=1, col=2, secondary_y=False)
fig.update_yaxes(title_text="Speed [m/s]", row=1, col=2, secondary_y=True)姿勢角(右列中段)#
# ロール・ピッチ・ヨー角
fig.add_trace(
go.Scatter(
x=time,
y=df['phi_deg'],
mode='lines',
line=dict(color='red', width=2),
name='Roll (φ)'
),
row=2, col=2
)
fig.add_trace(
go.Scatter(
x=time,
y=df['theta_deg'],
mode='lines',
line=dict(color='blue', width=2),
name='Pitch (θ)'
),
row=2, col=2
)
fig.add_trace(
go.Scatter(
x=time,
y=df['psi_deg'],
mode='lines',
line=dict(color='darkgreen', width=2),
name='Yaw (ψ)'
),
row=2, col=2
)
# Y軸ラベル
fig.update_yaxes(title_text="Angle [deg]", row=2, col=2)制御入力(右列下段)#
# エルロン・エレベータ・ラダー
fig.add_trace(
go.Scatter(
x=time,
y=df['aileron'],
mode='lines',
line=dict(color='purple', width=2),
name='Aileron'
),
row=3, col=2
)
fig.add_trace(
go.Scatter(
x=time,
y=df['elevator'],
mode='lines',
line=dict(color='brown', width=2),
name='Elevator'
),
row=3, col=2
)
fig.add_trace(
go.Scatter(
x=time,
y=df['rudder'],
mode='lines',
line=dict(color='cyan', width=2),
name='Rudder'
),
row=3, col=2
)
# 軸ラベル
fig.update_xaxes(title_text="Time [s]", row=3, col=2)
fig.update_yaxes(title_text="Control [-]", row=3, col=2)レイアウト調整#
サイズと余白#
fig.update_layout(
height=1000, # 全体の高さ(ピクセル)
width=1400, # 全体の幅
margin=dict(l=50, r=50, t=50, b=50), # 外側余白
showlegend=True,
legend=dict(
x=1.05, # 凡例位置(右側)
y=1.0,
xanchor='left',
yanchor='top'
)
)3Dシーンの設定#
# 3D軌道の軸設定
fig.update_scenes(
xaxis=dict(title="East [m]", backgroundcolor="rgb(230, 230,230)"),
yaxis=dict(title="North [m]", backgroundcolor="rgb(230, 230,230)"),
zaxis=dict(title="Up [m]", backgroundcolor="rgb(230, 230,230)"),
aspectmode='data' # データの比率を保持
)グリッド設定#
# 全ての2Dグラフにグリッドを追加
fig.update_xaxes(showgrid=True, gridcolor='lightgray', gridwidth=0.5)
fig.update_yaxes(showgrid=True, gridcolor='lightgray', gridwidth=0.5)column_widths / row_heights の詳細#
比率による指定#
fig = make_subplots(
rows=2,
cols=3,
column_widths=[0.2, 0.5, 0.3], # 20%, 50%, 30%
row_heights=[0.7, 0.3] # 70%, 30%
)解釈:
- 列1: 全幅の20%
- 列2: 全幅の50%
- 列3: 全幅の30%
- 行1: 全高の70%
- 行2: 全高の30%
不均等レイアウト例#
# 左列を広く、右列を狭く
fig = make_subplots(
rows=1,
cols=2,
column_widths=[0.65, 0.35], # 65% : 35%
horizontal_spacing=0.10
)spacing: 間隔調整#
horizontal_spacing(列間隔)#
fig = make_subplots(
rows=1,
cols=3,
horizontal_spacing=0.05 # 5%の間隔
)間隔の計算:
horizontal_spacing=0.05: 全幅の5%が列間の余白- 例: 幅1000pxの場合、50pxの間隔
vertical_spacing(行間隔)#
fig = make_subplots(
rows=3,
cols=1,
vertical_spacing=0.08 # 8%の間隔
)デフォルト値:
horizontal_spacing: 0.2 / cols(列が多いほど狭い)vertical_spacing: 0.3 / rows(行が多いほど狭い)
適切な間隔の目安#
| レイアウト | horizontal_spacing | vertical_spacing |
|---|---|---|
| 2列以下 | 0.05 ~ 0.10 | 0.08 ~ 0.12 |
| 3列以上 | 0.03 ~ 0.08 | 0.05 ~ 0.10 |
| タイトルなし | 0.05 ~ 0.08 | 0.05 ~ 0.08 |
| タイトルあり | 0.08 ~ 0.12 | 0.10 ~ 0.15 |
secondary_y: 2軸グラフ#
基本的な使い方#
from plotly.subplots import make_subplots
# secondary_y=Trueを指定
fig = make_subplots(
rows=1,
cols=1,
specs=[[{"secondary_y": True}]] # 2軸を有効化
)
# 主軸(左Y軸)
fig.add_trace(
go.Scatter(x=[1, 2, 3], y=[10, 20, 30], name="Series 1"),
secondary_y=False
)
# 副軸(右Y軸)
fig.add_trace(
go.Scatter(x=[1, 2, 3], y=[100, 200, 300], name="Series 2"),
secondary_y=True
)
# 軸ラベル
fig.update_yaxes(title_text="Primary Y", secondary_y=False)
fig.update_yaxes(title_text="Secondary Y", secondary_y=True)2軸が必要な場面#
単位が異なる: 高度[m]と速度[m/s] スケールが大きく異なる: 温度[°C]と気圧[hPa] 異なる物理量: 位置[m]と角度[deg]
完全な実装例#
"""
統合ダッシュボード: 3D軌道 + 時系列グラフ
"""
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import pandas as pd
# データ読み込み
df = pd.read_csv('phugoid_mode.csv')
time = df['time'].values
# サブプロット作成
fig = make_subplots(
rows=3,
cols=2,
specs=[
[{"type": "scene", "rowspan": 3}, {"type": "xy", "secondary_y": True}],
[None, {"type": "xy"}],
[None, {"type": "xy"}]
],
column_widths=[0.5, 0.5],
row_heights=[0.33, 0.33, 0.34],
horizontal_spacing=0.08,
vertical_spacing=0.08
)
# === 3D軌道 (左列) ===
fig.add_trace(
go.Scatter3d(
x=df['x_enu_m'],
y=df['y_enu_m'],
z=df['altitude_m'],
mode='lines',
line=dict(color='blue', width=4),
name='Trajectory',
showlegend=False
),
row=1, col=1
)
# === 高度・速度 (右列上段、2軸) ===
fig.add_trace(
go.Scatter(x=time, y=df['altitude_m'], mode='lines',
line=dict(color='green', width=3), name='Altitude'),
row=1, col=2, secondary_y=False
)
fig.add_trace(
go.Scatter(x=time, y=df['airspeed_m_s'], mode='lines',
line=dict(color='orange', width=3), name='Speed'),
row=1, col=2, secondary_y=True
)
fig.update_yaxes(title_text="Altitude [m]", row=1, col=2, secondary_y=False)
fig.update_yaxes(title_text="Speed [m/s]", row=1, col=2, secondary_y=True)
# === 姿勢角 (右列中段) ===
fig.add_trace(go.Scatter(x=time, y=df['phi_deg'], name='Roll',
line=dict(color='red', width=2)), row=2, col=2)
fig.add_trace(go.Scatter(x=time, y=df['theta_deg'], name='Pitch',
line=dict(color='blue', width=2)), row=2, col=2)
fig.add_trace(go.Scatter(x=time, y=df['psi_deg'], name='Yaw',
line=dict(color='darkgreen', width=2)), row=2, col=2)
fig.update_yaxes(title_text="Angle [deg]", row=2, col=2)
# === 制御入力 (右列下段) ===
fig.add_trace(go.Scatter(x=time, y=df['aileron'], name='Aileron',
line=dict(color='purple', width=2)), row=3, col=2)
fig.add_trace(go.Scatter(x=time, y=df['elevator'], name='Elevator',
line=dict(color='brown', width=2)), row=3, col=2)
fig.add_trace(go.Scatter(x=time, y=df['rudder'], name='Rudder',
line=dict(color='cyan', width=2)), row=3, col=2)
fig.update_xaxes(title_text="Time [s]", row=3, col=2)
fig.update_yaxes(title_text="Control [-]", row=3, col=2)
# === レイアウト調整 ===
fig.update_layout(
height=1000,
width=1400,
margin=dict(l=50, r=50, t=50, b=50),
showlegend=True
)
# 3Dシーン設定
fig.update_scenes(
xaxis=dict(title="East [m]"),
yaxis=dict(title="North [m]"),
zaxis=dict(title="Up [m]"),
aspectmode='data'
)
# グリッド追加
fig.update_xaxes(showgrid=True, gridcolor='lightgray')
fig.update_yaxes(showgrid=True, gridcolor='lightgray')
# 保存・表示
fig.write_html('dashboard.html')
print("[OK] Dashboard saved: dashboard.html")よくある問題と対処法#
問題1: トレースが表示されない#
# ❌ 間違い: row, colを指定していない
fig.add_trace(go.Scatter(x=[1, 2], y=[3, 4]))
# ✅ 正しい: row, colを明示的に指定
fig.add_trace(go.Scatter(x=[1, 2], y=[3, 4]), row=1, col=1)問題2: specsとトレース追加の不一致#
# ❌ 間違い: specs配列でNoneの位置にトレース追加
specs = [
[{"type": "scene", "rowspan": 2}, {"type": "xy"}],
[None, {"type": "xy"}] # (2,1)はNone
]
fig.add_trace(go.Scatter(...), row=2, col=1) # エラー!
# ✅ 正しい: Noneでない位置に追加
fig.add_trace(go.Scatter(...), row=2, col=2)問題3: 2軸が機能しない#
# ❌ 間違い: specsでsecondary_yを指定していない
fig = make_subplots(rows=1, cols=1, specs=[[{"type": "xy"}]])
fig.add_trace(go.Scatter(...), secondary_y=True) # 効果なし
# ✅ 正しい: specsでsecondary_y=Trueを指定
fig = make_subplots(rows=1, cols=1, specs=[[{"secondary_y": True}]])
fig.add_trace(go.Scatter(...), secondary_y=True) # 動作するまとめ#
本記事では、Plotly.jsのmake_subplots()を使った複雑なダッシュボードレイアウトの実装方法を解説しました。
重要なポイント:
make_subplots(): rows, cols, specsでレイアウト定義specs配列: タイプ(xy/scene)、rowspan/colspan指定add_trace(): row, col指定でトレース追加column_widths,row_heights: 比率指定(合計1.0)horizontal_spacing,vertical_spacing: 間隔調整(全体の比率)secondary_y=True: 2軸グラフの作成- 3D + 2Dの統合表示で実用的なダッシュボード実現
次のステップとして、アニメーション機能の統合(D-4, D-5の応用)や、複数CSVファイルの比較表示に挑戦してみましょう。
参照資料#
本記事の執筆にあたり、以下の資料を参照しました [@plotly_python_docs_subplots_2025; @plotly_python_docs_multiple_axes_2025; @plotly_python_docs_3d_scatter_2025; @phase7d_integration_plan_2025]。