はじめに
本記事では、MapLibre と deck.gl を組み合わせて、交通事故データを「時間帯ごと」に 3D/2D 表示で切り替えられる可視化デモを構築します。
deck.gl は WebGL ベースの高性能な可視化ライブラリで、HexagonLayer を用いることで、緯度・経度データを滑らかに集計し、色や高さで密度を表現できます。
今回は以下のような構成で、最小限のコードだけで「動く時系列地図」を実装します:
- 3D 表示:高さのある Hexagon(柱)で密度を表現
- 2D 表示:高さを 0 にした平面 Hexagon(色のみ)で密度を表現
- 時間帯スライダー:24 時間のどの時間帯に事故が集中しているかを即時切り替え
- 表示モード切替:3D/2D をボタンで切り替え
完成イメージは次のようになります。
使用データについて
本記事では、警察庁が公開している「交通事故統計情報(本票データ)」を利用します。
データは以下から入手できます:
本票データには、日本全国で発生した交通事故の発生日時・場所・当事者情報などが含まれています。
ただしそのままでは次の課題があります:
- 都道府県ごとに番号が振られている(東京=30 など)
- 緯度・経度は DMS(度分秒)形式の整数値で提供される
- 日付は複数の列に分かれている
- ファイルサイズが大きい
交通事故統計情報(本票データ)のデータ仕様については過去記事で紹介しています
なお、厳密には「一般道」「高速道路」など道路種別ごとの集計を分けるほうが望ましいですが、本記事ではあくまで “時系列 × 地図 × 3D/2D 可視化の最小構成” にフォーカスするため、道路種別による分類は行いません
本記事で扱う形式(前処理済み JSON)
可視化をシンプルにするため、前処理後のデータは次のような 軽量 JSON に整形しています。
[
{ "lon": 139.7501, "lat": 35.6902, "hour": 14 },
{ "lon": 139.7205, "lat": 35.6801, "hour": 8 }
]
ファイル構成
本記事では、MapLibre × deck.gl を最小構成で動かすために、以下の 4 ファイルだけを使用します。
project-root/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── app.js
└── data/
└── accidents_tokyo_timeline.json
各ファイルの役割
-
index.html
MapLibre と deck.gl の CDN を読み込み、地図と UI(ボタン・スライダー)を配置 -
css/style.css
ボタン・スライダー・地図のレイアウト用の最小スタイル -
js/app.js
地図初期化、deck.gl のレイヤ適用、時間帯スライダーと表示モード(3D/2D)切り替えなど、主要ロジックがすべてここに含まれる -
data/accidents_tokyo_timeline.json
前処理済みの事故データ(緯度・経度・時間帯)
使用する CDN(必要なライブラリ)
index.html では次のライブラリを CDN 経由で読み込みます。
- MapLibre GL JS
- deck.gl(MapboxOverlay 含む)
CDN を利用することで、追加インストールなしですぐに動作確認できます。
このファイル構成をベースに、次章から順に実装内容を解説していきます。
地図と deck.gl
本記事では、MapLibre 上に deck.gl の可視化レイヤ(HexagonLayer)を重ねて表示します。
その際にポイントとなるのが deck.gl の MapboxOverlay を利用する構成です。
MapboxOverlay を使うことで、次のことが実現できます。
- MapLibre と deck.gl が 同じ WebGL コンテキストを共有できる
- 地図のパン/ズームに合わせて deck.gl レイヤが自動で再描画される
-
overlay.setProps({ layers: [...] })で地図上に可視化レイヤを重ねられる
これにより、地図の操作性と描画パフォーマンスを保ちながら、
HexagonLayer の 3D/2D 表示や、時間帯別のデータ切替をスムーズに行えます。
app.js の全体像
このスクリプトでは、次のような役割をまとめて実装します。
- 事故データ(
accidents_tokyo_timeline.json)の読み込み - 表示モード(3D / 2D)と時間帯(0〜23 時)の管理
- MapLibre の初期化と視点(pitch / bearing)の制御
- deck.gl MapboxOverlay の初期化
- HexagonLayer の生成
- 3D 表示:
elevationScale > 0 - 2D 表示:
elevationScale = 0(高さだけ 0 にする)
- 3D 表示:
- スライダーとボタンからのイベントを受け取り、
updateLayer()で再描画
サンプルコード
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>交通事故データ 可視化デモ(MapLibre × deck.gl)</title>
<!-- MapLibre GL JS -->
<link
href="https://unpkg.com/maplibre-gl@3.6.1/dist/maplibre-gl.css"
rel="stylesheet"
/>
<script src="https://unpkg.com/maplibre-gl@3.6.1/dist/maplibre-gl.js"></script>
<!-- deck.gl -->
<script src="https://unpkg.com/deck.gl@8.9.36/dist.min.js"></script>
<!-- Styles -->
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<!-- 地図本体 -->
<div id="map"></div>
<!-- 地図の上に重ねるコントロール -->
<div class="controls">
<div class="mode-buttons">
<button id="btn-3d" class="active">3D</button>
<button id="btn-2d">2D</button>
</div>
<div class="timeline">
<input id="slider" type="range" min="0" max="23" value="0" />
<span id="step-label">00時</span>
</div>
</div>
<!-- スクリプト -->
<script src="js/app.js"></script>
</body>
</html>
css/style.css
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
/* 地図を全画面に表示 */
#map {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* 3D / 2D ボタン:右上 */
.mode-buttons {
position: fixed;
top: 10px;
right: 10px;
display: flex;
gap: 8px;
z-index: 1000;
background: rgba(255, 255, 255, 0.9);
padding: 6px 10px;
border-radius: 6px;
border: 1px solid #ccc;
}
.mode-buttons button {
padding: 6px 14px;
border: 1px solid #444;
background: #fff;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
}
.mode-buttons button.active {
background: #eee;
}
/* タイムライン:下部中央(背景付きのオーバレイ) */
.timeline {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
align-items: center;
z-index: 1000;
background: rgba(255, 255, 255, 0.9);
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #ccc;
font-size: 14px;
}
#slider {
width: 240px;
}
js/app.js
// js/app.js
const DATA_URL = "data/accidents_tokyo_timeline.json";
const CENTER_LON = 139.76;
const CENTER_LAT = 35.68;
const HOUR_MIN = 0;
const HOUR_MAX = 23;
let rawData = [];
let currentMode = "3d";
let currentHour = 0;
const sliderEl = document.getElementById("slider");
const stepLabelEl = document.getElementById("step-label");
const btn3d = document.getElementById("btn-3d");
const btn2d = document.getElementById("btn-2d");
// -------------------------------
// MapLibre 初期化
// -------------------------------
const map = new maplibregl.Map({
container: "map",
style: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
center: [CENTER_LON, CENTER_LAT],
zoom: 10,
pitch: 35,
bearing: -15,
});
map.addControl(new maplibregl.NavigationControl(), "top-left");
// deck.gl overlay
const overlay = new deck.MapboxOverlay({
interleaved: true,
layers: [],
});
map.addControl(overlay);
// -------------------------------
// hour → "00時" の表示形式
// -------------------------------
function formatHourLabel(hour) {
const h = String(hour).padStart(2, "0");
return `${h}時`;
}
// -------------------------------
// データ読み込み
// -------------------------------
async function loadData() {
try {
const res = await fetch(DATA_URL);
const records = await res.json();
if (!Array.isArray(records) || !records.length) return;
rawData = records
.filter(
(r) =>
r.hour != null &&
r.lon != null &&
r.lat != null &&
!Number.isNaN(Number(r.hour))
)
.map((r) => ({
...r,
timeStep: Number(r.hour),
}));
sliderEl.min = String(HOUR_MIN);
sliderEl.max = String(HOUR_MAX);
sliderEl.value = String(HOUR_MIN);
currentHour = HOUR_MIN;
stepLabelEl.textContent = formatHourLabel(currentHour);
updateLayer();
} catch (e) {
console.error("データ読み込みに失敗しました:", e);
}
}
// -------------------------------
// HexagonLayer 生成
// 3D/2D は elevationScale で制御
// -------------------------------
function getLayer(mode, hour) {
if (!rawData.length) return null;
const data = rawData.filter((d) => d.timeStep === hour);
const commonProps = {
data,
getPosition: (d) => [d.lon, d.lat],
getElevationWeight: () => 1,
elevationAggregation: "SUM",
getColorWeight: () => 1,
colorAggregation: "SUM",
radius: 250,
opacity: 0.9,
coverage: 0.8,
extruded: true,
colorRange: [
[255, 255, 204],
[161, 218, 180],
[65, 182, 196],
[44, 127, 184],
[37, 52, 148],
[8, 29, 88],
],
};
if (mode === "3d") {
return new deck.HexagonLayer({
id: "hex-3d",
...commonProps,
elevationScale: 15, // ← 高さあり
});
}
return new deck.HexagonLayer({
id: "hex-2d",
...commonProps,
elevationScale: 0, // ← 高さゼロで平面表示
});
}
// -------------------------------
// レイヤ更新
// -------------------------------
function updateLayer() {
if (!rawData.length) return;
const layer = getLayer(currentMode, currentHour);
overlay.setProps({ layers: [layer] });
stepLabelEl.textContent = formatHourLabel(currentHour);
}
// -------------------------------
// UI イベント
// -------------------------------
sliderEl.addEventListener("input", (e) => {
currentHour = Number(e.target.value);
updateLayer();
});
btn3d.addEventListener("click", () => {
currentMode = "3d";
btn3d.classList.add("active");
btn2d.classList.remove("active");
map.easeTo({
pitch: 35,
bearing: -15,
duration: 500,
});
updateLayer();
});
btn2d.addEventListener("click", () => {
currentMode = "2d";
btn2d.classList.add("active");
btn3d.classList.remove("active");
map.easeTo({
pitch: 0,
bearing: 0,
duration: 500,
});
updateLayer();
});
// -------------------------------
map.on("load", () => {
loadData();
});
可視化パラメータの調整ポイント
HexagonLayer の見た目は、いくつかのパラメータを調整することで大きく変わります。
この記事のサンプルでは最小構成にまとめていますが、目的に応じて以下の項目を調整すると可視化を最適化できます。
radius(六角形の大きさ)
六角形 1 セルの広がりを決めるパラメータです。
細かい局所パターンを見たい場合は小さく、広域的な傾向を示したい場合は大きく設定します。
radius: 250,
elevationScale(高さの強さ)
3D 表示時の高さの強調具合です。
2D 表示では高さを 0 にして平面として描画します。
// 3D
elevationScale: 15,
// 2D
elevationScale: 0,
colorRange(色のスケール)
密度に応じて色を変化させるグラデーションです。
事故データのように偏りが大きいデータでは、寒色系〜中間色の階調が見やすい傾向があります。
colorRange: [
[255, 255, 204],
[161, 218, 180],
[65, 182, 196],
[44, 127, 184],
[37, 52, 148],
[8, 29, 88],
],
pitch / bearing(視点の角度)
MapLibre 側の視点制御です。
3D 表示では傾きを付け、2D 表示では真上からの視点に切り替えています。
// 3D 表示時の視点
map.easeTo({
pitch: 35,
bearing: -15,
duration: 500
});
// 2D 表示時の視点
map.easeTo({
pitch: 0,
bearing: 0,
duration: 500
});
opacity / coverage(透明度と塗りの広がり)
視覚的な重なり具合や広がりを調整するパラメータです。
密度の強弱を強調したい場合に役立ちます。
opacity: 0.9,
coverage: 0.8,
表示目的に合わせて自由に調整可能です。
まとめ
本記事では、MapLibre と deck.gl を組み合わせ、東京都の交通事故データをもとに「時間帯で変化する 3D/2D ヒートマップ」を構築しました。
deck.gl の HexagonLayer を利用することで、密度集計やグリッド分割を自動で行えるため、少ない記述で時系列の変化を表現できます。
3D/2D ボタンによる視点切り替えや、スライダーによる時間帯切り替えを組み合わせることで、事故発生の傾向を直感的に把握できます。
今後は、オープンデータに含まれる属性(道路種別、年代、天候、事故類型など)を組み合わせることで、特定条件下での傾向分析に発展させることができます。
地域や年度を変えて適用することも容易で、さまざまな事故分析に応用できます。
