OpenLayersでWBGT地点を選び、Plotlyで実況値と予測値を表示する ― WBGT Risk WebApp r0053
はじめに
ここまで、WBGTデータを扱う部分を少しずつ分けて作ってきました。
- r0051:環境省のWBGT実況値を取得してDuckDBへ保存
- r0052:予測値も保存し、現在時刻を境に実況値と予測値を分けて返すAPIを追加
- r0053:OpenLayersの地点選択とPlotlyの時系列グラフを接続
r0052まででもSwagger UIからデータを確認できましたが、地点番号を入力してJSONを見るだけでは、普段使う画面としては少し扱いにくい状態でした。
そこでr0053では、地図上の地点をクリックすると最新WBGTをポップアップへ表示し、その地点の実況値と予測値をPlotlyで確認できる画面を追加しました。
r0053でできるようになったこと
画面で行う操作は単純です。
OpenLayersで地点をクリック
↓
最新WBGTをポップアップ表示
↓
地点番号を使って時系列APIを呼び出す
↓
Plotlyで実況値と予測値を表示
時系列グラフは、現在時刻を境に左右を分けています。
実況値は青い実線、予測値は橙色の破線です。境界時刻には縦線と「現在」のラベルを入れています。
システム構成
r0052のFastAPIとDuckDBを残し、Nginxで配信するフロントエンドを追加しました。
Docker Composeでは2つのコンテナを起動します。
| サービス | 内容 | ポート |
|---|---|---|
api |
FastAPI、DuckDB、環境省API取得、定期実行 | 8000 |
web |
Nginx、OpenLayers、Plotly | 8080 |
Nginxは/api/をFastAPIへ中継します。そのため、ブラウザ側のJavaScriptはホスト名やポート番号を持たず、次のような相対URLで呼び出せます。
fetch("/api/map/stations");
フォルダー構成
主なファイルは次のとおりです。
WBGT_Risk_WebApp_r0053_fixed5/
├─ docker-compose.yml
├─ .env
├─ .env.example
├─ start_server.bat
├─ stop_server.bat
├─ diagnose_api.bat
├─ fetch_now_live.bat
├─ fetch_forecast_live.bat
│
├─ backend/
│ ├─ Dockerfile
│ ├─ requirements.txt
│ ├─ app/
│ │ ├─ main.py
│ │ ├─ config.py
│ │ ├─ api/
│ │ │ ├─ map_data.py
│ │ │ ├─ timeseries.py
│ │ │ ├─ observations.py
│ │ │ └─ forecasts.py
│ │ ├─ clients/
│ │ │ └─ env_wbgt_client.py
│ │ ├─ repositories/
│ │ │ └─ database.py
│ │ ├─ services/
│ │ │ ├─ ingestion.py
│ │ │ └─ station_master.py
│ │ └─ schemas/
│ ├─ sample_data/
│ │ ├─ station_master_sample.csv
│ │ ├─ survey_response_sample.json
│ │ └─ forecast_response_sample.json
│ └─ tests/
│
├─ frontend/
│ ├─ Dockerfile
│ ├─ nginx.conf
│ ├─ index.html
│ ├─ css/
│ │ └─ style.css
│ └─ js/
│ ├─ api.js
│ ├─ app.js
│ ├─ map.js
│ └─ chart.js
│
├─ data/
│ └─ wbgt.duckdb
└─ docs/
地点マスタを追加する
環境省の実況値・予測値APIには地点番号が含まれていますが、地図表示に必要な地点名や緯度経度は含まれていません。
そのため、DuckDBへwbgt_stationテーブルを追加しました。
CREATE TABLE IF NOT EXISTS wbgt_station (
wbgt_no INTEGER PRIMARY KEY,
station_name VARCHAR NOT NULL,
station_name_kana VARCHAR NOT NULL DEFAULT '',
area_name VARCHAR NOT NULL DEFAULT '',
prefecture_name VARCHAR NOT NULL DEFAULT '',
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
location_note VARCHAR NOT NULL DEFAULT '',
source_name VARCHAR NOT NULL,
source_updated_at TIMESTAMPTZ
);
r0053に同梱している地点マスタは、画面確認用の2地点です。
| 地点番号 | 地点名 |
|---|---|
| 11001 | 宗谷岬 |
| 14163 | 札幌 |
backend/sample_data/station_master_sample.csv
この段階では地図と時系列APIの接続確認を優先しています。全国地点への拡張は次のr0054で行います。
OpenLayers用の地点GeoJSON API
地図表示では、地点マスタと最新実況値をバックエンド側で結合します。
GET /api/map/stations
レスポンスはGeoJSONのFeatureCollectionです。
{
"type": "FeatureCollection",
"count": 2,
"degraded": false,
"features": [
{
"type": "Feature",
"id": "14163",
"geometry": {
"type": "Point",
"coordinates": [141.3283, 43.06]
},
"properties": {
"wbgt_no": 14163,
"station_name": "札幌",
"wbgt_value": 22.0,
"observed_at": "2026-07-31T09:00:00+09:00",
"wbgt_class_label": "実況実測値",
"quality_label": "必要な観測要素あり",
"level_label": "注意",
"color": "#56ccf2",
"has_observation": true
}
}
]
}
GeoJSONの座標は[経度, 緯度]の順です。
WBGT区分の色
地点色は最新WBGTからPython側で決めます。
| WBGT | 区分 | 表示色 |
|---|---|---|
| 21未満 | ほぼ安全 | 青 |
| 21以上25未満 | 注意 | 水色 |
| 25以上28未満 | 警戒 | 黄 |
| 28以上31未満 | 厳重警戒 | 橙 |
| 31以上 | 危険 | 赤 |
JavaScript側で同じ判定を重ねず、APIが返すlevel_labelとcolorをそのまま使います。
地点をクリックしてポップアップを表示する
OpenLayersではsingleclickイベントから地点Featureを取得します。
map.on("singleclick", (event) => {
const feature = map.forEachFeatureAtPixel(
event.pixel,
(candidate) => candidate,
{
hitTolerance: 8,
layerFilter: (layer) => layer === stationLayer,
},
);
if (!feature) {
return;
}
const station = feature.getProperties();
showPopup(station, event.coordinate);
onStationSelected(station);
});
ポップアップには次の情報を表示します。
- 地点名、地点番号
- 最新WBGT
- WBGT区分
- 観測時刻
- 実況実測値または実況推定値
- 品質情報
地点番号は、そのまま時系列APIのパスに使用します。
現在時刻を境にした時系列API
r0052で作成したAPIを、そのままWeb画面から呼び出します。
GET /api/timeseries/{wbgt_no}
例として、札幌の24時間前から72時間後までを取得します。
GET /api/timeseries/14163
?current_time=2026-07-31T09:30:00+09:00
&past_hours=24
&future_hours=72
境界条件は次のとおりです。
実況値:timestamp <= boundary_time
予測値:timestamp > boundary_time
予測版:reference_time <= boundary_time のうち最新
同じ観測時刻に推定値と実測値がある場合は、実測値を優先します。
JavaScript側では、APIが分けた配列を再判定せずPlotlyへ渡します。境界判定をフロントエンドとバックエンドの両方へ書かないことで、表示の食い違いを防いでいます。
Plotlyで実況値と予測値を描画する
実況値と予測値は別々のtraceです。
const observationTrace = {
name: "実況値",
x: payload.observations.map((item) => item.timestamp),
y: payload.observations.map((item) => item.value),
type: "scatter",
mode: "lines+markers",
line: { color: "#1769aa", width: 3 },
};
const forecastTrace = {
name: "予測値",
x: payload.forecasts.map((item) => item.timestamp),
y: payload.forecasts.map((item) => item.value),
type: "scatter",
mode: "lines+markers",
line: { color: "#f2994a", width: 3, dash: "dash" },
};
境界時刻には縦線を追加します。
{
type: "line",
xref: "x",
yref: "paper",
x0: payload.boundary_time,
x1: payload.boundary_time,
y0: 0,
y1: 1,
line: { color: "#27364a", width: 2 },
}
グラフの左側は薄い青、右側は薄い橙の背景にして、実況値と予測値の範囲を見分けやすくしています。
Docker Composeで起動する
Windows 11でDocker Desktopを起動してから、プロジェクトのルートで実行します。
サンプルのZIPファイルをダウンロードして、解凍してください。
start_server.bat
バッチでは、古いAPIイメージを残さないようにキャッシュなしで再構築しています。
docker compose build --no-cache api
docker compose build web
docker compose up -d --force-recreate
起動後のURLです。
Web画面
http://localhost:8080/
Swagger UI
http://localhost:8000/docs
ヘルスチェック
http://localhost:8000/api/health
ブラウザに古いJavaScriptが残っている場合は、Ctrl + F5で再読込みします。
サンプルデータで確認する
最初は外部APIを使わず、同梱JSONで画面連携を確認できます。
fetch_now_sample.bat
Web画面上部の「サンプル取込」ボタンからも実行できます。
サンプル取込後に札幌をクリックすると、ポップアップとグラフが更新されます。
ライブデータで確認する
実況値と予測値をまとめて取得します。
fetch_now_live.bat
予測値だけを再取得する場合はこちらです。
fetch_forecast_live.bat
取得後は診断バッチで状態を確認します。
diagnose_api.bat
テスト
バックエンドでは、実況値・予測値の保存だけでなく、画面用APIも確認しています。
主なテスト項目です。
- 地点マスタCSVの読込みとUPSERT
- GeoJSONの座標順
- 最新実況値との結合
- 実況値がない地点の表示
- NaN、無限大、日時のJSON変換
- 実況実測値と実況推定値の優先順位
- 現在時刻を境にした実況値・予測値の分割
- 最新予測発表版の選択
- 品質コードNULLの取込み
- 特定発表時刻の予測APIクエリ
- Plotlyタイトル削除、trace/layout/CSSによる内部凡例の非表示
JavaScriptはNode.jsの--checkでも構文確認しています。
node --check frontend/js/api.js
node --check frontend/js/app.js
node --check frontend/js/map.js
node --check frontend/js/chart.js
r0053で残していること
r0053の地点マスタは宗谷岬と札幌だけです。
実況API自体は全国のデータを保存できますが、OpenLayersへ表示できるのは地点マスタに登録した地点だけです。また、予測値も地点マスタに登録した2地点だけを取得しています。
次のr0054では、全国の情報提供地点マスタを取り込み、次の点を追加します。
- 全国地点をOpenLayersへ表示
- 低縮尺時のクラスタ表示
- 地点クリック時のWBGT表示
- 予測値の取得回数を抑えるキャッシュ
- 環境省APIへの高頻度アクセスを避ける制御
r0053では、全国化の前に「地点クリックから時系列グラフまでが正しくつながること」を確認できました。
注意事項
このアプリはWBGTデータ表示のプロトタイプです。
- 画面の結果だけで作業続行・中止を決定しないでください。
- 現場では実際のWBGT計、作業内容、服装、体調なども確認してください。
- 環境省データを画面や帳票へ掲載する場合は、環境省の情報を利用していることを明記してください。
- APIは1回の取得件数に上限があります。
- 自動化ツールからの高頻度アクセスは避けてください。
- 外部公開時は管理APIの認証設定を追加してください。
まとめ
r0053では、r0052で作成した時系列APIをOpenLayersとPlotlyへ接続しました。
地点をクリック
↓
最新WBGTをポップアップ表示
↓
DuckDBから時系列を取得
↓
現在時刻を境に実況値と予測値を表示
次は、同じ画面構成を全国地点へ広げます。
参考リンク
- 環境省 熱中症予防情報サイト
- 環境省 暑さ指数の実況値・予測値ダウンロード
- 環境省 熱中症予防情報サイト API仕様書 第1.1版
- 環境省 暑さ指数電子情報提供サービス
- 国土地理院 地理院タイル一覧
- OpenLayers
- Plotly JavaScript
- FastAPI
- DuckDB




