Blog
気象データ可視化アプリ aiseed-weather を題材に
気象データ可視化アプリ
aiseed-weatherを題材に
「Python だけで」「宣言的に」「クロスプラットフォームの GUI を書く」 ― Flet はこの三つを本気で実現していて、業務ソフトの開発用 UI として、 Visual Basic 6 が当時持っていた生産性を、現代のスタックで取り戻している。
この記事では抽象論はやめて、実際の中規模アプリ aiseed-weather
(気象庁・ECMWF・Open-Meteo を統合した天気図スタジオ、Flet 0.85 +
Python 3.13 製、コンポーネント 6,800 行、図形描画 3,200 行)を例に、
業務 GUI として Flet が何を解決しているかを見ていく。
※コードは、以後の開発で変更されている場合があります。
src/aiseed_weather/
├── components/ # Flet コンポーネント (UI)
│ ├── app.py ← ルート + ナビゲーション
│ ├── point_forecast_view.py ← 地点予報タブ (2,000 行)
│ ├── map_view.py ← 天気図タブ (3,300 行)
│ ├── radar_view.py ← レーダータブ
│ └── amedas_view.py ← AMeDAS 観測タブ
├── figures/ # matplotlib + flet.canvas でのチャート描画
├── services/ # ECMWF / ERA5 / JMA / Open-Meteo へのアクセス
└── models/ # データクラス / 設定
UI ・データ取得・データ処理がすべて Python で書かれている。 Web/REST API のレイヤがなく、polars の DataFrame をそのまま Flet コンポーネントに渡している。

Flet 0.70+ の宣言的モードでは、@ft.component でデコレートした関数が
コンポーネントになり、ft.use_state / ft.use_effect / ft.use_ref
といったフックを React 同様に使う。
aiseed-weather の地点予報ビュー (実コードから抜粋):
@ft.component
def PointForecastView(settings: UserSettings):
data_dir = resolved_data_dir(settings)
locations, set_locations = ft.use_state(load_locations(data_dir))
selected_name, set_selected_name = ft.use_state(settings.default_location)
forecast_state, set_forecast_state = ft.use_state("idle")
forecast_data, set_forecast_data = ft.use_state(None)
error_msg, set_error_msg = ft.use_state("")
variable, set_variable = ft.use_state("temperature_2m")
visible_days, set_visible_days = ft.use_state(7)
pan_offset_h, set_pan_offset_h = ft.use_state(0)
# … (実コードでは 30+ の use_state スロット)
地点リスト、選択中の地点、ロード状態、予報データ、エラーメッセージ、
表示変数、表示期間、パンオフセット… 業務 UI のすべての状態を、
ローカルな use_state だけで扱えている。Redux も Context Provider も
Zustand も登場しない。
async def fetch_forecast(_):
set_forecast_state("loading")
try:
df = await fetch_open_meteo(lat, lon, ...)
set_forecast_data(df)
set_forecast_state("ready")
except Exception as e:
set_error_msg(str(e))
set_forecast_state("error")
return ft.Column([
ft.Dropdown(
value=selected_name,
options=[ft.DropdownOption(loc.name) for loc in locations],
on_change=lambda e: set_selected_name(e.value),
),
ft.FilledButton("更新", on_click=fetch_forecast),
_StatusBanner(forecast_state, error_msg),
_HourlyStrip(forecast_data) if forecast_data else None,
_Chart(forecast_data, variable, visible_days, pan_offset_h),
])
これが コンポーネントの全貌。VB の Button1_Click がそのまま
on_click= に置き換わっていると思えばよい。async def のハンドラが
そのまま受理されるところが、現代的な改善点。
@ft.observableタブをまたいで生きる「長時間ダウンロード処理」のような状態には、
@ft.observable 付きのデータクラスを使う。これは aiseed-weather の
天気図タブが、ECMWF GRIB2 ファイル(1 ファイル数十 MB)を S3 から
並列ダウンロードする間、進捗を画面上のあらゆる場所に伝播させるための
仕組み:
@ft.observable
@dataclass
class FetchSession:
"""ECMWF Open Data ダウンロードのライフサイクル状態。
タブ移動しても生き続け、すべてのコンポーネントが購読する。"""
running: bool = False
items: list = field(default_factory=list)
progress: dict = field(default_factory=lambda: {"done": 0, "total": 0})
status_text: str = ""
session.running = True と書くだけで、これを use_state で読んでいる
全コンポーネントが自動再描画される。React の Recoil / Jotai 風だが、
追加のライブラリ無しで Flet 標準で出来る。
業務アプリで頻発する「裏で長いジョブが走っているときに、画面の ステータスバーとプログレスバーとボタンの enable/disable が連動する」を、 配線コードゼロで実現できるのが効く。
flet.canvas業務アプリで頻出する「自前グラフ」 ― aiseed-weather では当初
matplotlib を ft.Image に貼り付けていたが、ホバーやクリックを後付け
したい都合で、flet.canvas で描き直した。これが 650 行で 5 変数 ×
HRES + MSM + 気候値 + アンサンブル帯の重ね描きができる:
import flet.canvas as cv
shapes: list[cv.Shape] = []
# 気候値の範囲帯 (p25 〜 p75)
shapes.append(cv.Path(
elements=band_path_elements,
paint=ft.Paint(color="#d8e4f5", style=ft.PaintingStyle.FILL),
))
# HRES 折れ線
shapes.append(cv.Path(
elements=hres_line_elements,
paint=ft.Paint(color="#234b86", stroke_width=2.0,
style=ft.PaintingStyle.STROKE),
))
# 軸ラベル
for tick in time_ticks:
shapes.append(cv.Line(x, pad_t, x, pad_t + plot_h,
paint=ft.Paint(color="#cccccc")))
shapes.append(cv.Text(x, pad_t + plot_h + 14, label,
style=ft.TextStyle(size=10)))
return cv.Canvas(shapes=shapes, width=W, height=H)
Web フロントエンドだったら d3.js / Recharts / Plotly + データ整形
コードが必要なところを、すべて Python で完結している。データソースの
polars DataFrame から直接 Canvas の Path 要素を組み立てている。
「データ処理と描画の言語が同じ」というのが、業務開発でじわじわ効いて
くる。
画面表示は flet.canvas だが、「PNG ダウンロード」ボタンを押したときの
出力は matplotlib で生成している:
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(times, temperatures, label="HRES")
ax.fill_between(times, p25, p75, alpha=0.3, label="気候値範囲")
fig.savefig(path, dpi=150, metadata={"Source": "ECMWF Open Data / ..."})
インタラクティブ用とエクスポート用で別エンジン、しかし両方 Python なので「変数追加」「色の統一」が一箇所で済む。Web アプリで「画面は React + 出力は別バックエンドで Pillow」みたいな分裂が起きないのが 大きい。
asyncio がそのまま動くECMWF の S3 からの GRIB2 ダウンロードはネットワーク律速で並列化したい。
asyncio.TaskGroup + httpx.AsyncClient を、Flet のイベントハンドラ
から直接呼ぶ:
async def on_fetch(_):
session.running = True # 全コンポーネントが自動更新
try:
async with asyncio.TaskGroup() as tg:
for param in params:
tg.create_task(download_one(param, session))
except* asyncio.CancelledError:
session.status_text = "キャンセル"
finally:
session.running = False
VB 6 時代の DoEvents 地獄も、Electron の IPC 配線地獄もない。
ネイティブ Python の async が、UI の更新サイクルと自然に噛み合って
いる。
業務アプリは「Python から C ライブラリを呼ぶ」が頻発する。
aiseed-weather は:
これらは pip では設定が辛いが、miniforge を使えば environment.yml
一発で揃う:
dependencies:
- python=3.13
- flet>=0.85
- polars
- xarray
- cfgrib
- cartopy
- matplotlib
mamba env create --prefix ./.venv -f environment.yml
mamba activate ./.venv
flet run src/aiseed_weather/main.py
Flet と科学計算スタックが衝突しない。これは Electron 系では地獄に 近い領域 (各 OS のネイティブビルド) で、Python + conda の組み合わせの 強さがそのまま生きる。
flet pack src/aiseed_weather/main.py # 単一バイナリ
flet build apk # Android
flet build web # PWA
同じコードベースから、デスクトップアプリ、Web アプリ、モバイルアプリを 生成できる。業務 SI の世界では「同じツールを社内 Web と現場タブレット で」が頻出するので、これは事実上の killer feature。
Flet が業務 GUI で強いのはデスクトップ / Web だけではない。 Flutter ベースのレンダリングが GPU を素直に使うため、ARM ボード級の ハードでもヌルヌル動く。これは業務向け専用端末を作る現場で重要な性質。
| 要件 | Flet の対応 |
|---|---|
| Raspberry Pi 4/5 等で動く | Flutter Linux desktop が動けば OK。実績多数 |
| タッチ前提 UI | Flutter / Material はタッチファースト設計 |
| キオスク化 (フルスクリーン、Window chrome なし) | page.window.full_screen = True 1 行 |
| ハードと話す (GPIO / シリアル / CAN / Modbus / OPC-UA) | Python 業務ライブラリがそのまま使える (pyserial, pymodbus, python-can, asyncua …) |
| 画像処理・推論を同居 | OpenCV, PyTorch, ONNX Runtime, TFLite すべて Python から |
| 現場ごとのカスタマイズ | コード 1 行差し替えて再配置、ビルドチェーン不要 |
| 監視を Web からも見たい | 同じコードで flet run --web ― 端末側と遠隔監視ダッシュボードを 1 ソースで |
aiseed-weather の例から類推できることこのアプリは「PC で動く解析ツール」として書かれているが、全く同じ コードを Raspberry Pi 5 + 7" タッチディスプレイで動かしてキオスクモード にすれば、それだけで気象表示端末になる。
def main(page: ft.Page):
page.window.full_screen = True
page.window.frameless = True
page.theme_mode = ft.ThemeMode.DARK # 夜間視認性
page.padding = 0
page.add(App())
ft.run(main)
3 行追加するだけで「業務専用端末」のガワが整う。matplotlib のグラフは Pi 5 でも 60 fps スムーズに描かれる。これが Flet の地味だが強烈な利点。
特筆すべきは「端末用ファームと、それを遠隔監視する Web ダッシュボード が同一ソース」になる点。業務 IoT で監視 Web をわざわざ別チームで作って きた現場には、開発体制ごと変える破壊力がある。
「Raspberry Pi 級以上 + タッチパネル + 業務ロジック」のゾーンは、 Flet の独擅場になるかも。
業務アプリで避けて通れない「帳票出力」 ― これは Flet の弱点ではなく、 むしろ Python の伝統的な得意領域。日本の現場で de facto となっている Excel 帳票を、テンプレートに値を流し込む方式で生成できる:
from openpyxl import load_workbook
wb = load_workbook("template/月次報告.xlsx")
ws = wb["集計"]
ws["B3"] = report_date
ws["D5"] = total_count
for i, row in enumerate(df.iter_rows(named=True), start=10):
ws.cell(i, 1, row["地点"])
ws.cell(i, 2, row["最高気温"])
ws.cell(i, 3, row["最低気温"])
wb.save(output_path)
書式・罫線・印刷範囲・ヘッダ/フッタ・社判の画像はテンプレート側で 作り込んでおけば、Python は値を埋めるだけ。現場の経理 / 事務担当が Excel で直接テンプレートを編集できるので、運用に乗せやすい。
選べる出力経路:
| 出力 | ライブラリ | 用途 |
|---|---|---|
| Excel (.xlsx) テンプレート差し込み | openpyxl |
月次報告、明細、見積書 |
| Excel + チャート / 条件付き書式 | xlsxwriter |
グラフ付き集計表 |
| Word (.docx) テンプレート | python-docx, docxtpl |
契約書、報告書 |
| PDF (低レベル) | reportlab |
レイアウト固定の証憑 |
| HTML → PDF | weasyprint |
CSS で組んだレポート |
| プリンタ直送 | win32print (Win), cups (Linux) |
ラベル発行、レシート |
Flet コンポーネント側はボタン 1 つで済む:
ft.FilledButton(
"帳票出力",
icon=ft.Icons.PRINT,
on_click=lambda _: export_excel(data, settings.template_path),
)
aiseed-weather の「PNG ダウンロード」と同じパターン ― 画面表示と
出力ファイルで別エンジンを使い、両方とも Python なので 1 つのコード
ベースに収まる。
ここまで紹介した特徴は、AI による自動コーディングと組み合わせた
ときに本領を発揮する。aiseed-weather の UI コード 6,800 行は、
ほぼすべて Claude Code との対話で書かれた。人間側がやったのは
「何を作るか」「データの意味は何か」を伝えることと、出てきたコードの
レビューだけ。
Flet が AI フレンドリーな理由は構造的:
ft.Column / ft.Row /
ft.Container / ft.Text …)結果として、「データクラスを 1 つ書いて、それを表示するコンポーネ ントを作って」と頼むだけで、ほぼ修正なしで動く Flet コードが返って くる。Web フロントだと "TypeScript の型を書き直して" "Tailwind が 効かない" "useMemo が必要" など複数の往復が要るところが、Flet なら 1 ターンで決まる。
さらに、プロジェクト固有のコーディング規約は AI に常時参照させる
仕組み (Claude Code の Skill 機能など) でリポジトリに同梱できる。
「うちは Flet 0.85+ 宣言的モードのみ、page.update() は禁止」と
一度書いておけば、Web 検索で混入する古い書き方を AI が選ぶことは
無い。Flet の宣言的モードが「規約として書き下ろしやすい」ことと、
LLM のコード生成性能が良いこととが、ここで相乗効果を出す。
(この「規約を AI に読ませる」運用そのものは、業務 SI の体制論 として独立した話題なので、別稿で扱う。Python 側のホットスポットを Rust (PyO3) で潰す手法も同様、別稿予定。)
| VB 6 | Flet + Python | |
|---|---|---|
| 学習コスト | 低い | 同じく低い (Python が読めれば 1 日で動かせる) |
| GUI 配置 | フォームデザイナ | コード (Git, レビュー, AI 生成と相性◎) |
| イベント駆動 | Button1_Click |
on_click=fn (async OK) |
| 状態管理 | グローバル変数 + フォームスコープ | use_state + @ft.observable |
| 数値計算 | 自前 / Excel 連携 | numpy / polars / xarray が標準 |
| 可視化 | MSChart, ActiveX | matplotlib / flet.canvas / Plotly |
| ネットワーク | WinINet, Winsock | httpx, asyncio, gRPC, etc. |
| 配布 | EXE | EXE / Web / iOS / Android / 組み込み端末 |
| 動作環境 | Windows only | Win / Mac / Linux / Web / Mobile / Raspberry Pi |
| エコシステム | 業務 ActiveX | Python パッケージ全部 |
VB 6 で生産性を支えていたのは「言語の単純さ × フォームの即時性 × 配布の単純さ」だった。Flet はこの三拍子を、Python という強力な裏方 を引き連れて現代に再演している。
aiseed-weather を書きながら強く感じた、Flet の本領が出る場面:
執筆時点で、公式 (Flet Team) は 宣言的モード = 実アプリで使える段階 だと明言している:
@ft.component を本気で本番に使うために最後まで
欠けていた ルーティング (ft.Router) と 宣言的ダイアログ
(ft.use_dialog()) が追加された「新しい API だから様子見」というフェーズはもう終わっている。今から 新規業務アプリを書き始めるなら、宣言的モード一択。
注意点としては「Web 上の古い記事は命令的 API (page.add() / 直接ミュ
テート) で書かれているものが多い」 ― ただし AI コーディング前提なら、
規約ファイルに「宣言的モードのみ」と書いておけば AI 側は最新 API で
生成するので、実害は無い。
DataTable は 1 万行スクロールには向かない。仮想化は自前それでも、業務 GUI を Python で書くという軸がもたらす利得が、これら を上回る場面は広い。
aiseed-weather は、Flet が「中規模の本格業務アプリ」を支えられるか
どうかの個人的な検証だった。結論は、はっきり YES。
flet.canvas + matplotlib)これらを、Python 単一言語で、Web のフロント / バックエンド分離 なしで、React も TypeScript もバンドラも CSS フレームワークも使わず に、ふつうに開発できている。Raspberry Pi にも載る。Web にも出せる。
VB 6 が「業務 GUI のミニマム生産性スタック」を発明したとすれば、
Flet はそれを Python の文脈で再発明した上で、配布先の天井を取り払った。
VB の生産性を懐かしむすべての開発者、Streamlit で物足りなさを感じている
すべてのデータサイエンティスト、Electron で挫折したすべての Python 使い、
現場端末を Qt で苦労して作ってきたすべての SI エンジニアに、flet run
してみてほしい。
公式: https://flet.dev/ / ソース: aiseed-weather