0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

環境省のWBGT予測値をDuckDBへ保存し、OpenLayersの地点選択とPlotlyの時系列グラフを連動する(r0053) release 0.0.5α Step 3

0
Posted at

OpenLayersでWBGT地点を選び、Plotlyで実況値と予測値を表示する ― WBGT Risk WebApp r0053

はじめに

ここまで、WBGTデータを扱う部分を少しずつ分けて作ってきました。

  • r0051:環境省のWBGT実況値を取得してDuckDBへ保存
  • r0052:予測値も保存し、現在時刻を境に実況値と予測値を分けて返すAPIを追加
  • r0053:OpenLayersの地点選択とPlotlyの時系列グラフを接続

r0052まででもSwagger UIからデータを確認できましたが、地点番号を入力してJSONを見るだけでは、普段使う画面としては少し扱いにくい状態でした。

そこでr0053では、地図上の地点をクリックすると最新WBGTをポップアップへ表示し、その地点の実況値と予測値をPlotlyで確認できる画面を追加しました。

Screenshot 2026-08-01 092757.png


r0053でできるようになったこと

画面で行う操作は単純です。

OpenLayersで地点をクリック
        ↓
最新WBGTをポップアップ表示
        ↓
地点番号を使って時系列APIを呼び出す
        ↓
Plotlyで実況値と予測値を表示

時系列グラフは、現在時刻を境に左右を分けています。

実況値は青い実線、予測値は橙色の破線です。境界時刻には縦線と「現在」のラベルを入れています。

実況値と予測値の境界


システム構成

r0052のFastAPIとDuckDBを残し、Nginxで配信するフロントエンドを追加しました。

WBGT時系列API構成

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_labelcolorをそのまま使います。


地点をクリックしてポップアップを表示する

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/

Screenshot 2026-08-01 092233.png

Swagger UI
http://localhost:8000/docs

Swagger UI
Screenshot 2026-08-01 092355.png

ヘルスチェック
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から時系列を取得
    ↓
現在時刻を境に実況値と予測値を表示

次は、同じ画面構成を全国地点へ広げます。


参考リンク


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?