この記事で学べること#
- Time Marker(進捗線)の実装方法
- go.Scatter traceを使用する理由
- layout.shapesではない理由(frames配列に含められない)
- 各フレームでのマーカー位置更新方法
- yref=‘paper’による縦線の描画テクニック
対象読者#
- D-4「Plotly.js Frames APIアニメーション基礎」を読んだ方
- アニメーションに時刻表示(進捗線)を追加したい方
- Plotly.jsの実装パターンを深く理解したい方
前回の記事では、Frames APIの基本的な使い方を学びました。本記事では、アニメーション中に**現在の時刻を示す縦線(Time Marker)**を表示する実装方法を解説します。
Time Markerとは?#
アニメーションの進行状況を示す縦線#
Time Marker(タイムマーカー)は、アニメーション再生中に現在の時刻を示す赤い縦線です。2Dグラフ(時系列グラフ)上で、どの時点のデータを表示しているかを視覚的に示します。
使用例#
- 高度・速度・荷重倍数の時系列グラフ
- バンク角・横滑り角のグラフ
- ボディレート(p, q, r)のグラフ
これらのグラフ上で、縦線が左から右へ移動することで、時間の進行が一目で分かります。
なぜlayout.shapesではなくgo.Scatterを使うのか?#
layout.shapesの制約#
Plotly.jsでは、layout.shapesを使って線や図形を描画できます。
const layout = {
shapes: [
{
type: 'line',
x0: 10, // 開始X座標
x1: 10, // 終了X座標
y0: 0, // 開始Y座標
y1: 1, // 終了Y座標
yref: 'paper', // Y軸は相対座標(0-1)
line: {
color: 'red',
width: 2
}
}
]
};しかし、shapesはframesに含めることができません。つまり、アニメーション中に動的に更新できないのです。
go.Scatter traceなら可能#
go.Scatter traceは、framesで動的に更新できます。縦線を2点(上端と下端)を持つ線として定義することで、Time Markerを実現します。
go.Scatterによる縦線の描画#
基本的な縦線の定義#
// Time Marker traceの定義(縦線)
const timeMarkerTrace = {
x: [10, 10], // X座標(同じ値で縦線を作る)
y: [0, 1], // Y座標(下端と上端)
yaxis: 'y', // Y軸の参照
type: 'scatter',
mode: 'lines', // 線のみ
name: 'Time Marker',
line: {
color: 'red',
width: 2
},
showlegend: false, // 凡例に表示しない
hoverinfo: 'skip' // ホバー情報を表示しない
};ポイント:
x: [10, 10]- 同じX座標を2回指定することで、縦線を作りますy: [0, 1]- Y軸の範囲(0から1)mode: 'lines'- 線のみ表示
yref=‘paper’の代替: データ範囲を使用#
layout.shapesのyref='paper'(0-1の相対座標)は、go.Scatterでは使えません。代わりに、データのY軸範囲を使用します。
// データのY軸範囲を取得
const yMin = Math.min(...altitudeData);
const yMax = Math.max(...altitudeData);
// Time Marker traceの定義
const timeMarkerTrace = {
x: [10, 10],
y: [yMin, yMax], // データの最小値・最大値
type: 'scatter',
mode: 'lines',
line: {
color: 'red',
width: 2
},
showlegend: false,
hoverinfo: 'skip'
};複数グラフでの実装パターン#
グラフごとにTime Markerを追加#
複数の2Dグラフ(高度、速度、荷重倍数等)がある場合、それぞれにTime Marker traceを追加します。
// 例: 3つのグラフ
// Trace 0: 高度データ
// Trace 1: 高度のTime Marker
// Trace 2: 速度データ
// Trace 3: 速度のTime Marker
// Trace 4: 荷重倍数データ
// Trace 5: 荷重倍数のTime Marker
const traces = [
// 高度グラフ
{
x: timeData,
y: altitudeData,
name: 'Altitude',
yaxis: 'y' // 1番目のY軸
},
// 高度のTime Marker
{
x: [timeData[0], timeData[0]],
y: [Math.min(...altitudeData), Math.max(...altitudeData)],
mode: 'lines',
line: { color: 'red', width: 2 },
yaxis: 'y',
showlegend: false,
hoverinfo: 'skip'
},
// 速度グラフ
{
x: timeData,
y: speedData,
name: 'Speed',
yaxis: 'y2' // 2番目のY軸
},
// 速度のTime Marker
{
x: [timeData[0], timeData[0]],
y: [Math.min(...speedData), Math.max(...speedData)],
mode: 'lines',
line: { color: 'red', width: 2 },
yaxis: 'y2',
showlegend: false,
hoverinfo: 'skip'
},
// ...
];重要: 各Time Markerは、対応するグラフのyaxisを指定します(y, y2, y3…)。
framesでの更新パターン#
Time Markerのみを更新#
静的なデータトレースは変化しないため、framesではTime Markerのみを更新します。
const frames = [];
for (let i = 0; i < totalPoints; i += skipInterval) {
const currentTime = timeData[i];
frames.push({
name: `frame${i}`,
data: [
// Trace 0: 高度データ(静的)→ 省略
// Trace 1: 高度のTime Marker(動的)
{
x: [currentTime, currentTime],
y: [Math.min(...altitudeData), Math.max(...altitudeData)]
},
// Trace 2: 速度データ(静的)→ 省略
// Trace 3: 速度のTime Marker(動的)
{
x: [currentTime, currentTime],
y: [Math.min(...speedData), Math.max(...speedData)]
}
// ...
],
traces: [1, 3] // Time MarkerのtraceインデックスをTime Markerのみを指定
});
}framesのtracesプロパティ:
traces: [1, 3]- 更新するtraceのインデックスを指定- Time Markerのインデックス(1, 3, 5…)のみを指定することで、静的トレースは変化しません
実装例: 高度グラフのTime Marker#
完全な実装コード#
// データ準備
const timeData = [0.0, 0.1, 0.2, 0.3, 0.4];
const altitudeData = [100, 105, 110, 108, 102];
// Trace 0: 高度データ(静的)
const traceAltitude = {
x: timeData,
y: altitudeData,
type: 'scatter',
mode: 'lines',
name: 'Altitude (m)'
};
// Trace 1: Time Marker(動的)
const yMin = Math.min(...altitudeData);
const yMax = Math.max(...altitudeData);
const traceTimeMarker = {
x: [timeData[0], timeData[0]], // 初期位置
y: [yMin, yMax],
type: 'scatter',
mode: 'lines',
line: {
color: 'red',
width: 2
},
showlegend: false,
hoverinfo: 'skip'
};
// layout
const layout = {
title: '高度の時系列変化',
xaxis: { title: 'Time (s)' },
yaxis: { title: 'Altitude (m)' },
height: 400
};
// frames作成
const targetFrames = 5;
const skipInterval = Math.max(1, Math.floor(timeData.length / targetFrames));
const frames = [];
for (let i = 0; i < timeData.length; i += skipInterval) {
frames.push({
name: `frame${i}`,
data: [
// Trace 0(静的)は省略
// Trace 1: Time Markerのみ更新
{
x: [timeData[i], timeData[i]],
y: [yMin, yMax]
}
],
traces: [1] // Trace 1のみ更新
});
}
// グラフ描画
Plotly.newPlot('plotAltitude', [traceAltitude, traceTimeMarker], layout, { frames: frames });
// アニメーション再生
Plotly.animate('plotAltitude', null, {
frame: { duration: 50 },
transition: { duration: 0 },
mode: 'afterall'
});メモリ最適化の効果#
問題: 全データを毎回更新すると重い#
// ❌ 悪い例: 静的トレースも毎回含める
frames.push({
data: [
{ x: timeData, y: altitudeData }, // 全データ(不要)
{ x: [currentTime, currentTime], y: [yMin, yMax] } // Time Marker
]
});問題:
- 1フレーム = 7KB(全データ)
- 300フレーム = 2.1MB
- メモリ消費とファイルサイズが膨大に
対策: Time Markerのみを更新#
// ✅ 良い例: Time Markerのみ更新
frames.push({
data: [
{ x: [currentTime, currentTime], y: [yMin, yMax] } // Time Markerのみ
],
traces: [1] // Trace 1のみ更新
});効果:
- 1フレーム = 100B(Time Markerのみ)
- 300フレーム = 30KB
- 99.3%のメモリ削減
よくある間違いと対処法#
間違い1: layout.shapesを使ってしまう#
// ❌ 悪い例: shapesはframesで更新できない
const layout = {
shapes: [
{ type: 'line', x0: 10, x1: 10, y0: 0, y1: 1 }
]
};問題: shapesはframesに含められないため、動的に更新できません。
対策: go.Scatter traceを使用します。
間違い2: traceのインデックス指定ミス#
// ❌ 悪い例: tracesを指定しない
frames.push({
data: [
{ x: [currentTime, currentTime], y: [yMin, yMax] }
]
// traces指定なし → 全traceが更新されてしまう
});問題: tracesを指定しないと、全traceが更新されてしまい、静的トレースも毎回再描画されます。
対策: traces: [1]のように、Time MarkerのtraceインデックスのみTime Markerのみを指定します。
まとめ#
本記事では、Plotly.js Frames APIでTime Marker(進捗線)を実装する方法を解説しました。
重要なポイント:
- layout.shapesはframesに含められないため、go.Scatter traceを使う
- 縦線は2点(同じX座標、異なるY座標)で定義
- framesではTime Markerのみを更新(静的トレースは省略)
traces: [1]でTime Markerのtraceインデックスを指定- メモリ最適化により99.3%のサイズ削減が可能
次のステップとして、updatemenusとslidersを使ったアニメーション再生コントロール(再生/一時停止ボタン、シークバー)の実装に挑戦してみましょう。
参照資料#
本記事の執筆にあたり、以下の資料を参照しました [@plotly_js_docs_animations_2025; @plotly_js_docs_shapes_2025]。