225 lines
8.5 KiB
TypeScript
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>
|
|
);
|
|
}
|