この記事で学べること#

  • 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.shapesyref='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]。