deck.gl で数万件の移動データを WebGL アニメ描画する
deck.gl の TripsLayer で「時刻つき経路」を動く軌跡として再生するパターン。Canvas/SVG が数千要素で頭打ちになる領域を WebGL で突破する仕組み、React(@deck.gl/react)での層構成、毎フレーム currentTime だけを更新して GPU にデータを載せたままにする設計、trailLength の演出、MapLibre 併用まで、架空路線網を再生する触れる demo 付きで整理する実装メモ。
検証日: 2026-07-26
使用バージョン:
@deck.gl/core@9.3.7/@deck.gl/layers/@deck.gl/geo-layers/@deck.gl/react/react@19.2対象: 点や線が数万件を超えて Canvas/SVG の描画が破綻し始めた人。移動・フロー(交通、物流、アクセスログ)を「動き」として見せたい人
deck.gl(Uber 発・現 OpenJS Foundation 配下の WebGL 可視化フレームワーク)の TripsLayer で、「時刻つき経路」の集合を動く軌跡として再生する。SVG の DOM 数千要素・Canvas 2D の数万描画で頭打ちになる領域を、GPU 描画で数十万点まで引き上げるのが deck.gl の役割。動く demo 付き(データは架空の放射状路線網)。
触って試す
数百本の「列車」が同時に走っている。静的な路線ラインは 1 本も描いておらず、軌跡の尾(trail)の重なりだけで路線網が浮かび上がる。「尾の長さ」を最短にすると粒の流れに、最長にすると残像の網になる。ドラッグで回転(pitch 45°)、スクロールでズーム。
1. なぜ deck.gl か — 描画手段の使い分け
| 手段 | 実用上限の目安 | 使いどころ |
|---|---|---|
| SVG(D3 等) | 〜数千要素 | 軸・ラベル・少数の図形。DOM なのでイベントが楽 |
| Canvas 2D | 〜数万描画/frame | ヒートマップ、静的な大量点 |
| deck.gl(WebGL) | 数十万〜 | 大量点・線・3D・時間アニメーション |
deck.gl は「レイヤーの宣言」だけを書く。ScatterplotLayer(点)、ArcLayer(2 点間の弧)、PathLayer(線)、TripsLayer(時刻つき線)などをデータと accessor(データ 1 件から座標や色を取り出す関数)で構成すると、GPU バッファへの転送・シェーダ・再描画はフレームワークが持つ。
2. 最小サンプル — ScatterplotLayer を React で出す
pnpm add @deck.gl/core @deck.gl/layers @deck.gl/geo-layers @deck.gl/react
次のコードは 1 万点を WebGL で描く最小構成。背景地図なしでも動く(deck.gl 単体で 1 つの WebGL キャンバス):
import DeckGL from "@deck.gl/react";
import { ScatterplotLayer } from "@deck.gl/layers";
const layer = new ScatterplotLayer({
id: "points",
data: points, // [{position: [lng, lat]}, ...] × 10,000
getPosition: (d) => d.position, // accessor: データ → [経度, 緯度]
getFillColor: [80, 210, 255, 180],
radiusMinPixels: 2,
});
export function Map() {
return (
<div style={{ position: "relative", height: 400 }}>
<DeckGL
initialViewState={{ longitude: 139.7, latitude: 35.68, zoom: 10 }}
controller // ドラッグ/ズーム操作を有効化
layers={[layer]}
/>
</div>
);
}
ポイント:
DeckGLは absolute 配置なので、position: relative+ 高さ指定の親で包む- レイヤーは毎レンダーで new してよい。deck.gl が前回インスタンスと props を diff し、変わっていなければ GPU 側は何もしない(React の仮想 DOM と同じ発想)
- 色は
[r, g, b, a]の 0-255 配列
3. TripsLayer — 「時刻つき経路」を再生する
TripsLayer のデータは 経路(座標列)+ 各点の通過時刻 のペア。currentTime を進めると、各経路上の「いま」の位置までが描かれ、trailLength 分の過去が尾として残る:
import { TripsLayer } from "@deck.gl/geo-layers";
new TripsLayer({
id: "trips",
data: trips, // [{path: [[lng,lat],...], timestamps: [0, 4, 8, ...]}, ...]
getPath: (d) => d.path,
getTimestamps: (d) => d.timestamps, // path と同じ長さの時刻配列
getColor: (d) => d.color,
currentTime: time, // ← これだけを毎フレーム更新する
trailLength: 18, // 何時間ぶんの尾を残すか(timestamps と同じ単位)
widthMinPixels: 2.5,
fadeTrail: true, // 尾を透明に減衰
});
アニメーションは requestAnimationFrame で time を進めてループさせるだけ:
useEffect(() => {
let last = performance.now();
let raf = 0;
const tick = (now: number) => {
setTime((t) => (t + (now - last) / 1000 * 60) % LOOP); // 実 1 秒 = 仮想 60 分
last = now;
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
性能の鍵は「座標データを毎フレーム触らない」こと。data が同一参照なら GPU バッファは載せたまま、currentTime(ただの uniform 値)だけが更新される。逆に毎フレーム data を作り直すと、全経路の再転送が走ってフレームレートが崩壊する。
4. 「描かずに見せる」— trailLength の演出
demo で静的な路線ラインを描いていないのは意図的で、TripsLayer だけで密度が表現できるため:
- 尾が重なる場所 = 本数が多い区間が自然に明るくなる(加算的な視覚効果)
- 運行が少ない区間は流れては消え、運行終了後は画面が静かになる — 「動と静」がデータそのものを語る
trailLength短 → 粒の流れ(個々の便が見える)/ 長 → 残像の網(路線網の形が見える)
この「軌跡の重なりで構造を浮かび上がらせる」手法は、交通に限らずアクセスログ・物流・人流でも同じ形で使える。
5. 背景地図と重ねる(MapLibre 併用)
demo は抽象背景(単色)だが、実データでは背景地図が欲しくなる。deck.gl は MapLibre GL JS と公式に統合できる(pnpm add @deck.gl/mapbox を追加。名前は mapbox だが MapLibre 対応):
import { MapboxOverlay } from "@deck.gl/mapbox";
import maplibregl from "maplibre-gl";
const map = new maplibregl.Map({ /* 通常の MapLibre 初期化 */ });
const overlay = new MapboxOverlay({ interleaved: true, layers: [tripsLayer] });
map.addControl(overlay); // MapLibre のカメラと deck.gl が同期する
interleaved: true にすると deck.gl のレイヤーが MapLibre の描画パイプラインに割り込み、地図ラベルの下に軌跡を描くような重ね順制御ができる。背景タイルの配信をサーバーレスにしたい場合は MapLibre + PMTiles の記事と組み合わせる。
つまずいたポイント
- 画面が真っ黒のままエラーも出ない: 親要素の高さ 0 が定番。
DeckGLは absolute 配置なので、親に明示的なheightとposition: relativeが必須 - アニメーションがカクつく(数千 trip 程度なのに): 毎フレーム
data配列を再生成していた。座標はuseMemoで固定し、フレームで動かすのはcurrentTimeだけにする(§3) trailLengthを変えても何も変わらないように見える:timestampsの単位と混同していた。trailLengthは timestamps と同じ単位(demo は「分」)。秒とミリ秒が混ざると 1000 倍ずれて見た目が変わらない/全部消える- React 19 の StrictMode で WebGL context が 2 個作られる: dev では mount が 2 回走るため。unmount 側の cleanup は deck.gl が処理するので実害は dev のみだが、
console.warnが出るのは仕様 - ズームすると点が消える(ScatterplotLayer):
getRadiusがメートル単位のため、引きの表示で 1px 未満になっていた。radiusMinPixelsで下限を張る
向くケース / 向かないケース
| 判定 | |
|---|---|
| 移動・フローの時系列再生(交通、物流、人流) | ◎ TripsLayer の本領 |
| 数万〜数十万点の散布・密度表示 | ◎ ScatterplotLayer / HexagonLayer |
| 軸・凡例つきの業務チャート | ✖ Recharts / D3 の領分(deck.gl に軸はない) |
| 描画対象が数百件以下 | △ WebGL の初期化コストの方が高い。SVG で十分 |
関連書籍
Python と JavaScriptではじめるデータビジュアライゼーション | tech-book.net
関連テーマ・学習ロードマップから次の一冊へ。
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス | tech-book.net
関連テーマ・学習ロードマップから次の一冊へ。