Files
wtr/components/charts/day-forecast-charts.tsx
zv 91acdb39b8
Some checks failed
CI / Lint, test, typecheck and build (push) Has been cancelled
feat: add weather unit preferences
2026-07-04 20:16:11 +02:00

225 lines
8.5 KiB
TypeScript

"use client";
import { Bar, CartesianGrid, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { Card } from "@/components/ui/card";
import { ChartTooltip } from "@/components/charts/chart-tooltip";
import { CHART_COLORS } from "@/lib/chart-theme";
import { useI18n } from "@/lib/i18n";
import { formatForecastTemperature } from "@/lib/forecast-utils";
import { useWeatherStore } from "@/lib/store";
import {
convertPrecipitation,
convertTemperature,
formatTemperatureValue,
getPrecipitationUnitLabel,
getTemperatureUnitLabel,
} from "@/lib/weather-utils";
import type { HourlyForecast } from "@/types/forecast";
const INITIAL_CHART_DIMENSION = { width: 1, height: 1 };
interface ChartLegendItem {
color: string;
label: string;
marker: "bar" | "line" | "dashed-line";
}
function formatHour(value: string) {
return value.slice(11, 16);
}
function formatChartPrecipitation(value: number | null, language: "pl" | "en", unit: string) {
if (value === null) return "—";
const formattedValue = new Intl.NumberFormat(language === "pl" ? "pl-PL" : "en-GB", {
minimumFractionDigits: unit === "in" ? 2 : 0,
maximumFractionDigits: unit === "mm" ? (value < 1 ? 2 : 1) : 2,
}).format(value);
return `${formattedValue} ${unit}`;
}
function ChartLegend({ items }: { items: ChartLegendItem[] }) {
return (
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-[0.72rem] font-medium text-muted">
{items.map((item) => (
<span className="inline-flex items-center gap-1.5" key={item.label}>
{item.marker === "bar" ? (
<span className="h-2.5 w-3 rounded-t-[5px]" style={{ backgroundColor: item.color }} aria-hidden="true" />
) : (
<span
className={`h-0 w-5 border-t-2 ${item.marker === "dashed-line" ? "border-dashed" : "border-solid"}`}
style={{ borderColor: item.color }}
aria-hidden="true"
/>
)}
{item.label}
</span>
))}
</div>
);
}
export function DayForecastCharts({ hours }: { hours: HourlyForecast[] }) {
const { language, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const rows = hours.map((hour) => ({
time: formatHour(hour.time),
temperature: hour.temperature === null ? null : convertTemperature(hour.temperature, temperatureUnit),
feelsLike: hour.feelsLike === null ? null : convertTemperature(hour.feelsLike, temperatureUnit),
precipitation: hour.precipitation === null ? null : convertPrecipitation(hour.precipitation, precipitationUnit),
precipitationProbability: hour.precipitationProbability,
}));
const temperatureUnitLabel = getTemperatureUnitLabel(temperatureUnit);
const precipitationUnitLabel = getPrecipitationUnitLabel(precipitationUnit);
const temperatureLegendItems: ChartLegendItem[] = [
{ color: CHART_COLORS.temperature, label: t("forecast.temperature"), marker: "line" },
{ color: CHART_COLORS.feelsLike, label: t("forecast.apparentTemperature"), marker: "dashed-line" },
];
const rainfallLegendItems: ChartLegendItem[] = [
{ color: CHART_COLORS.rainfall, label: t("forecast.precipitation"), marker: "bar" },
{ color: CHART_COLORS.probability, label: t("forecast.precipitationProbability"), marker: "line" },
];
return (
<div className="grid gap-3 lg:grid-cols-2">
<Card className="p-4 sm:p-5">
<h3 className="text-sm font-semibold">{t("forecast.temperatureChart")}</h3>
<p className="mt-1 text-xs leading-5 text-muted">{t("forecast.temperatureChartDescription")}</p>
<div className="mt-4 h-56 min-w-0">
<ResponsiveContainer
width="100%"
height="100%"
minWidth={0}
minHeight={0}
initialDimension={INITIAL_CHART_DIMENSION}
>
<ComposedChart data={rows} margin={{ left: -20, right: 8, top: 8 }}>
<CartesianGrid stroke={CHART_COLORS.grid} strokeDasharray="4 4" vertical={false} />
<XAxis
dataKey="time"
axisLine={false}
tickLine={false}
interval={3}
tick={{ fill: "currentColor", fontSize: 11 }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: "currentColor", fontSize: 11 }}
unit={temperatureUnitLabel}
/>
<Tooltip
cursor={{ stroke: CHART_COLORS.grid, strokeDasharray: "4 4" }}
content={
<ChartTooltip
valueFormatter={(entry) =>
typeof entry.value === "number"
? formatTemperatureValue(entry.value, language, temperatureUnit)
: formatForecastTemperature(null, language, temperatureUnit)
}
/>
}
/>
<Line
type="monotone"
dataKey="temperature"
name={t("forecast.temperature")}
stroke={CHART_COLORS.temperature}
strokeWidth={3}
dot={false}
connectNulls
/>
<Line
type="monotone"
dataKey="feelsLike"
name={t("forecast.apparentTemperature")}
stroke={CHART_COLORS.feelsLike}
strokeWidth={2}
strokeDasharray="5 4"
dot={false}
connectNulls
/>
</ComposedChart>
</ResponsiveContainer>
</div>
<ChartLegend items={temperatureLegendItems} />
</Card>
<Card className="p-4 sm:p-5">
<h3 className="text-sm font-semibold">{t("forecast.rainfallChart")}</h3>
<p className="mt-1 text-xs leading-5 text-muted">{t("forecast.rainfallChartDescription")}</p>
<div className="mt-4 h-56 min-w-0">
<ResponsiveContainer
width="100%"
height="100%"
minWidth={0}
minHeight={0}
initialDimension={INITIAL_CHART_DIMENSION}
>
<ComposedChart data={rows} margin={{ left: -20, right: -10, top: 8 }}>
<CartesianGrid stroke={CHART_COLORS.grid} strokeDasharray="4 4" vertical={false} />
<XAxis
dataKey="time"
axisLine={false}
tickLine={false}
interval={3}
tick={{ fill: "currentColor", fontSize: 11 }}
/>
<YAxis
yAxisId="rainfall"
axisLine={false}
tickLine={false}
tick={{ fill: "currentColor", fontSize: 11 }}
unit={` ${precipitationUnitLabel}`}
/>
<YAxis
yAxisId="probability"
orientation="right"
domain={[0, 100]}
axisLine={false}
tickLine={false}
tick={{ fill: "currentColor", fontSize: 11 }}
unit="%"
/>
<Tooltip
cursor={{ fill: "hsl(var(--border) / 0.18)" }}
content={
<ChartTooltip
valueFormatter={(entry) =>
entry.dataKey === "precipitation"
? formatChartPrecipitation(
typeof entry.value === "number" ? entry.value : null,
language,
precipitationUnitLabel,
)
: `${typeof entry.value === "number" ? entry.value : "—"}%`
}
/>
}
/>
<Bar
yAxisId="rainfall"
dataKey="precipitation"
name={t("forecast.precipitation")}
fill={CHART_COLORS.rainfall}
radius={[5, 5, 0, 0]}
/>
<Line
yAxisId="probability"
type="monotone"
dataKey="precipitationProbability"
name={t("forecast.precipitationProbability")}
stroke={CHART_COLORS.probability}
strokeWidth={2}
dot={false}
connectNulls
/>
</ComposedChart>
</ResponsiveContainer>
</div>
<ChartLegend items={rainfallLegendItems} />
</Card>
</div>
);
}