From d11e2a708de99fedeb55fc3543c4867a9fa4fa9b Mon Sep 17 00:00:00 2001 From: Akira Date: Sat, 28 Feb 2026 13:46:08 +0900 Subject: [PATCH] =?UTF-8?q?=E6=B0=97=E8=B1=A1=E3=83=87=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E3=81=AB=E3=82=B0=E3=83=A9=E3=83=95=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=EF=BC=88Recharts=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 月別気温折れ線グラフ(最高・平均・最低) - 月別降水量棒グラフ + 日照時間折れ線グラフ(右軸) - recharts ^3.7.0 を依存に追加 Co-Authored-By: Claude Sonnet 4.6 --- frontend/package.json | 1 + frontend/src/app/weather/page.tsx | 425 +++++++++++++++++++----------- 2 files changed, 271 insertions(+), 155 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 599ae7c..4750cd0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "next": "14.1.0", "react": "^18", "react-dom": "^18", + "recharts": "^3.7.0", "tailwind-merge": "^3.4.0" }, "devDependencies": { diff --git a/frontend/src/app/weather/page.tsx b/frontend/src/app/weather/page.tsx index 6fb8a87..dcca6be 100644 --- a/frontend/src/app/weather/page.tsx +++ b/frontend/src/app/weather/page.tsx @@ -1,9 +1,13 @@ 'use client'; import { useState, useEffect } from 'react'; +import { + LineChart, Line, BarChart, Bar, ComposedChart, + XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, +} from 'recharts'; import { api } from '@/lib/api'; import Navbar from '@/components/Navbar'; -import { Cloud, Loader2, Sun, Droplets, Wind, Thermometer } from 'lucide-react'; +import { Cloud, Loader2, Sun, Droplets, Thermometer, Wind } from 'lucide-react'; interface MonthlyRecord { month: number; @@ -43,16 +47,34 @@ interface DailyRecord { pressure_min: number | null; } -const MONTHS = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; +const MONTH_LABELS = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; function fmt(val: number | null, digits = 1): string { return val == null ? '—' : val.toFixed(digits); } +// null を含むグラフデータを安全に変換 +function toChartTemp(monthly: MonthlyRecord[]) { + return monthly.map((m) => ({ + month: MONTH_LABELS[m.month - 1], + 平均: m.temp_mean_avg, + 最高: m.temp_max_avg, + 最低: m.temp_min_avg, + })); +} + +function toChartPrecipSunshine(monthly: MonthlyRecord[]) { + return monthly.map((m) => ({ + month: MONTH_LABELS[m.month - 1], + 降水量: m.precip_total != null ? Math.round(m.precip_total) : null, + 日照時間: m.sunshine_total != null ? Math.round(m.sunshine_total) : null, + })); +} + export default function WeatherPage() { const currentYear = new Date().getFullYear(); const [year, setYear] = useState(currentYear); - const [tab, setTab] = useState<'summary' | 'recent'>('summary'); + const [tab, setTab] = useState<'chart' | 'summary' | 'recent'>('chart'); const [summary, setSummary] = useState(null); const [summaryLoading, setSummaryLoading] = useState(true); @@ -74,12 +96,9 @@ export default function WeatherPage() { end.setDate(end.getDate() - 1); const start = new Date(end); start.setDate(start.getDate() - 13); - const fmt2 = (d: Date) => d.toISOString().slice(0, 10); - api.get(`/weather/records/?start=${fmt2(start)}&end=${fmt2(end)}`) - .then((res) => { - const data: DailyRecord[] = res.data; - setRecentRecords([...data].reverse()); - }) + const fmtDate = (d: Date) => d.toISOString().slice(0, 10); + api.get(`/weather/records/?start=${fmtDate(start)}&end=${fmtDate(end)}`) + .then((res) => setRecentRecords([...res.data].reverse())) .catch((err) => console.error(err)) .finally(() => setRecentLoading(false)); }, []); @@ -87,6 +106,9 @@ export default function WeatherPage() { const years: number[] = []; for (let y = currentYear; y >= 2016; y--) years.push(y); + const tempData = summary ? toChartTemp(summary.monthly) : []; + const precipData = summary ? toChartPrecipSunshine(summary.monthly) : []; + return (
@@ -98,43 +120,183 @@ export default function WeatherPage() {

気象データ

-
- -
+ + {/* 年間サマリーカード(常に表示) */} + {!summaryLoading && summary && ( +
+
+
+ +

年間平均気温

+
+

+ {fmt(summary.annual.temp_mean_avg)} +

+
+
+
+ +

年間降水量

+
+

+ {fmt(summary.annual.precip_total, 0)}mm +

+
+
+
+ +

年間日照時間

+
+

+ {fmt(summary.annual.sunshine_total, 0)}h +

+
+
+
+ +

猛暑日 / 冬日

+
+

+ {summary.annual.hot_days}猛暑 + / + {summary.annual.cold_days} +

+
+
+ )} + {/* タブ */}
- - + {(['chart', 'summary', 'recent'] as const).map((t) => ( + + ))}
+ {/* ===== グラフタブ ===== */} + {tab === 'chart' && ( + summaryLoading ? ( +
+ +
+ ) : summary ? ( +
+ + {/* 気温グラフ */} +
+

月別気温(℃)

+ + + + + `${v}℃`} + domain={['auto', 'auto']} + /> + value == null ? '—' : `${value.toFixed(1)}℃`} + /> + + + + + + +
+ + {/* 降水量・日照時間グラフ */} +
+

月別降水量(mm)・日照時間(h)

+ + + + + `${v}`} + label={{ value: 'mm', position: 'insideTopLeft', offset: 5, fontSize: 11, fill: '#6b7280' }} + /> + `${v}h`} + /> + { + if (value == null) return '—'; + return name === '降水量' ? `${value} mm` : `${value} h`; + }} + /> + + + + + +
+ +
+ ) : ( +
データの取得に失敗しました
+ ) + )} + {/* ===== 月別サマリー ===== */} {tab === 'summary' && ( summaryLoading ? ( @@ -142,96 +304,51 @@ export default function WeatherPage() { ) : summary ? ( -
- {/* 年間サマリーカード */} -
-
-
- -

年間平均気温

-
-

- {fmt(summary.annual.temp_mean_avg)} -

-
-
-
- -

年間降水量

-
-

- {fmt(summary.annual.precip_total, 0)}mm -

-
-
-
- -

年間日照時間

-
-

- {fmt(summary.annual.sunshine_total, 0)}h -

-
-
-
- -

猛暑日 / 冬日

-
-

- {summary.annual.hot_days}猛暑 - / - {summary.annual.cold_days} -

-
-
- - {/* 月別テーブル */} -
-
- - - - - - - - - - - - +
+
+
平均気温最高(avg)最低(avg)日照h降水mm猛暑日冬日雨天日
+ + + + + + + + + + + + + + + {summary.monthly.map((m) => ( + + + + + + + + + + - - - {summary.monthly.map((m) => ( - - - - - - - - - - - - ))} - -
平均気温最高(avg)最低(avg)日照h降水mm猛暑日冬日雨天日
{MONTH_LABELS[m.month - 1]} + {m.temp_mean_avg == null ? '—' : ( + = 25 ? 'text-orange-500 font-medium' : m.temp_mean_avg < 5 ? 'text-blue-500 font-medium' : ''}> + {fmt(m.temp_mean_avg)}℃ + + )} + {fmt(m.temp_max_avg)}℃{fmt(m.temp_min_avg)}℃{fmt(m.sunshine_total, 0)}{fmt(m.precip_total, 0)} + {m.hot_days > 0 ? {m.hot_days} : 0} + + {m.cold_days > 0 ? {m.cold_days} : 0} + {m.rainy_days}
{MONTHS[m.month - 1]} - {m.temp_mean_avg == null ? '—' : ( - = 25 ? 'text-orange-500 font-medium' : m.temp_mean_avg < 5 ? 'text-blue-500 font-medium' : ''}> - {fmt(m.temp_mean_avg)}℃ - - )} - {fmt(m.temp_max_avg)}℃{fmt(m.temp_min_avg)}℃{fmt(m.sunshine_total, 0)}{fmt(m.precip_total, 0)} - {m.hot_days > 0 ? {m.hot_days} : 0} - - {m.cold_days > 0 ? {m.cold_days} : 0} - {m.rainy_days}
-
-
- 猛暑日: 最高気温 ≥ 35℃ 冬日: 最低気温 < 0℃ 雨天日: 降水量 ≥ 1mm -
+ ))} + + +
+
+ 猛暑日: 最高気温 ≥ 35℃ 冬日: 最低気温 < 0℃ 雨天日: 降水量 ≥ 1mm
) : ( @@ -266,32 +383,30 @@ export default function WeatherPage() { データがありません - ) : ( - recentRecords.map((r) => ( - - {r.date} - {fmt(r.temp_mean)}℃ - - = 35 ? 'text-red-500 font-medium' : 'text-gray-500'}> - {fmt(r.temp_max)}℃ - - - - - {fmt(r.temp_min)}℃ - - - {fmt(r.sunshine_h)} - - = 10 ? 'text-blue-600 font-medium' : 'text-gray-700'}> - {fmt(r.precip_mm)} - - - {fmt(r.wind_max)} - {fmt(r.pressure_min, 0)} - - )) - )} + ) : recentRecords.map((r) => ( + + {r.date} + {fmt(r.temp_mean)}℃ + + = 35 ? 'text-red-500 font-medium' : 'text-gray-500'}> + {fmt(r.temp_max)}℃ + + + + + {fmt(r.temp_min)}℃ + + + {fmt(r.sunshine_h)} + + = 10 ? 'text-blue-600 font-medium' : 'text-gray-700'}> + {fmt(r.precip_mm)} + + + {fmt(r.wind_max)} + {fmt(r.pressure_min, 0)} + + ))}