「日本で最近どこで地震があったか、地図で一目で見たい」を ブラウザだけ で実現する。気象庁の防災情報 XML はブラウザ直叩きできない (CORS なし)、地震情報サイトは UI が遅い、というニッチを USGS の FDSN event service (GeoJSON、CORS あり、認証不要) で埋める。マグニチュードで円サイズ、深さで色、5 分おき自動更新。約 300 行。
🌐 デモ: https://sen.ltd/portfolio/earthquake-jp/
📦 GitHub: https://github.com/sen-ltd/earthquake-jp
なぜ気象庁ではなく USGS なのか
国内発生地震を最も網羅的に持っているのは 気象庁 (JMA) — これは事実。ただし、ブラウザから直接叩こうとすると詰まる:
-
CORS が無効 → ブラウザの fetch / XHR から取れない (Same-origin policy で
Access-Control-Allow-Originヘッダ無し) - 配信形式が XML で JSON 化されていない (P2P 地震情報など独立プロジェクトが二次配信しているが、それぞれ別 API)
- bulk download にはアカウント や 防災科研 J-SHIS 経由が必要
つまりサーバを 1 段挟まないと使えない、というのが本記事の前提課題。
これに対して USGS の FDSN event service (earthquake.usgs.gov/fdsnws/event/1/) は:
-
CORS が有効 (
Access-Control-Allow-Origin: *) - auth 不要
- GeoJSON 直返し
- 5-10 分の遅延 で更新される
なので fetch() 1 行で完結する。代償は「USGS の M2.5 以上だけ」になることで、M2 以下の微小地震は気象庁の独壇場のままだが、「日本周辺の最近の地震を視覚的に把握する」 という目的には十分。
URL 1 本にすべて詰める
USGS の FDSN クエリは GET パラメータで時間窓と地理窓を指定できる。日本周辺の bbox は (lat 24-46, lon 122-148) を採用 (列島 + 南西諸島 + 太平洋プレート境界の手前まで):
export const JP_BBOX = {
minLat: 24, maxLat: 46, minLon: 122, maxLon: 148,
};
export function buildFeedUrl(windowHours, minMag, nowMs = Date.now()) {
const end = new Date(nowMs).toISOString();
const start = new Date(nowMs - windowHours * 3600_000).toISOString();
const params = new URLSearchParams({
format: "geojson",
starttime: start, endtime: end,
minlatitude: String(JP_BBOX.minLat),
maxlatitude: String(JP_BBOX.maxLat),
minlongitude: String(JP_BBOX.minLon),
maxlongitude: String(JP_BBOX.maxLon),
minmagnitude: String(minMag),
orderby: "time",
});
return `https://earthquake.usgs.gov/fdsnws/event/1/query?${params.toString()}`;
}
nowMs がパラメータ化されているので、Date.now() を 2_000_000_000_000 (架空の時刻) に固定したテストが書ける。
マグニチュードを「線形の円半径」にマップする理由
リヒタースケールは エネルギーの底-10 対数 で、M7 は M6 の 32 倍のエネルギー、M5 は M3 の 1000 倍。ナイーブに「面積 ∝ エネルギー」「半径 ∝ エネルギー」をやると、M9 の円が画面を埋め尽くす一方で M3 が見えなくなる。
実用的なのは 半径を線形 in magnitude にすること:
export function magnitudeToRadius(mag) {
if (mag == null || mag < 0) return 4;
const r = 3 + (mag - 1) * 4.4;
if (r < 3) return 3;
if (r > 38) return 38;
return r;
}
これで:
| 観測 | 半径 |
|---|---|
| M1 | 3 px |
| M3 | 11.8 px |
| M5 | 20.6 px |
| M7 | 29.4 px |
| M9 | 38 px (clamp) |
「M が 2 上がると、面積はおおよそ 2-3 倍に増える」見え方で、視覚的には自然な「ひと回り大きい」スケールになる。 数学的にはエネルギー比とは違うが、地図上で読みやすい ことの方が大事。
クランプは「M9 級が 1 件入ったときに視野全体を埋めない」ため。実際の 2011 東北 (M9.1) を入れても、隣接県の M5 が同じ画面で見える、を保ちたい。
深さで色を変える理由
地震の被害は マグニチュード × 深さ で決まる。M5 でも深さ 10 km なら直下型で建物が揺れる、M6 でも深さ 500 km なら広域でゆるく揺れるだけ。なので地図上で 深さを色で示す と「赤 (浅) は気をつけたほうがいい、青 (深) は揺れの広がりは大きいが被害は少なめ」というメンタルマップが直感的に立つ。
6 段の anchor を線形補間:
const stops = [
[0, [0xef, 0x44, 0x44]], // red — surface
[30, [0xf9, 0x73, 0x16]], // orange
[70, [0xea, 0xb3, 0x08]], // yellow
[150, [0x10, 0xb9, 0x81]], // teal-green
[300, [0x06, 0x6c, 0xb5]], // blue
[700, [0x1e, 0x40, 0xaf]], // deep blue — slab
];
色相を一周させない (黄を超えたら緑→青に進む、赤に戻らない) ので、色相環の循環を見ているのか上下を見ているのか で迷うことがない。これは地理可視化での経験則。
Leaflet 採用と CARTO ダークタイル
地図ライブラリは Leaflet (1.9.4) を CDN ロード。maplibre-gl は ベクタータイル前提でいい感じだが、無料の vector tile プロバイダ (MapTiler, Stadia) は API キーが要る。今回は キー不要・raster 完結 で済む構成にしたいので Leaflet に倒した。
タイルは CartoDB Voyager Dark (OSM ベースの暗色テーマ、attribution 守れば free):
const TILE_URL = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
const TILE_ATTRIB =
'© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/attributions">CARTO</a>';
L.tileLayer(TILE_URL, { attribution: TILE_ATTRIB, maxZoom: 9, minZoom: 3 }).addTo(map);
これで「ページ全体は暗いダークテーマ、エピセンターの円だけ色がついて目立つ」見せ方になる。maxZoom: 9 は「日本列島全体が見える + 都道府県名が読める」を確保しつつ、深い zoom-in は USGS 詳細ページに譲るための制限。
5 分 polling の理由
USGS は WebSocket / SSE による push を提供していない。なので クライアント側で polling。間隔は 5 分:
- USGS の公開遅延が 5-10 分なので、5 分間隔ならほぼ全更新を拾える
- 5 分 polling で M2.5+ × 7 日窓だと、レスポンスは 10-30 件程度。10 KB に満たない GET。USGS のレートリミットに当たることはない
- ブラウザのタブがバックグラウンドに行くと
setIntervalが throttle されるが、復帰時に即 fetch する設計にしてあるので、戻ったときに最新化される
function scheduleAutoRefresh() {
if (state.refreshTimer) clearInterval(state.refreshTimer);
state.refreshTimer = setInterval(fetchAndRender, REFRESH_INTERVAL_MS);
state.countdownTimer = setInterval(updateCountdown, 1000);
updateCountdown();
}
ステータス行に「次回更新まで X:YZ」のカウントダウンを出している。ユーザに「今表示しているデータがどの時点のものか」を伝えるのは可視化ツールの基本。
純粋ロジックの単独テスト
quakes.js は DOM / Leaflet / fetch を一切持たない。script.js の側にあるのは「マップ作る・fetch する・popup 出す」だけで、計算はすべて quakes.js 経由。
これで node --test が直接消費できる:
$ npm test
✔ buildFeedUrl carries window + bbox + minmagnitude params
✔ buildFeedUrl windows backwards from nowMs
✔ magnitudeToRadius grows linearly in magnitude
✔ magnitudeToRadius clamps the upper end so M9 doesn't blow out
✔ depthToColor returns warm hex for surface events
✔ depthToColor returns cool hex for slab-depth events
✔ depthToColor interpolates between anchor stops
✔ featureToRow flattens USGS GeoJSON to {lat, lon, depth, mag, ...}
✔ filterToJp drops points outside the bbox
✔ filterByWindow keeps only rows newer than (now - hours*3600s)
✔ statsFromRows tracks count, max magnitude, average, most-recent time
✔ formatRelativeJa uses 分前 / 時間前 / M/D HH:mm thresholds
… (17 tests total)
ℹ tests 17 ℹ pass 17 ℹ fail 0
USGS API はテストで叩かない。nowMs を注入できる関数設計にしてあるので、合成 Feature と固定時刻だけで全パスが回る。
触る
https://sen.ltd/portfolio/earthquake-jp/ で 24 時間〜30 日 / M2.5〜M5 を切り替えながら、最近の日本周辺地震をマップで見られる。各エピセンターをクリックすると、マグニチュード・深さ・場所・JST 時刻・USGS 詳細ページへのリンクが popup される。
ソース: https://github.com/sen-ltd/earthquake-jp — MIT、合計 ~300 行 (JS) + 17 ユニットテスト、ビルド不要、npm 依存ゼロ (Leaflet は CDN ロード)。
🛠 本記事は SEN 合同会社 が公開している小さな開発者ツール群の 1 つ。他は portfolio 一覧 から。
