fix: improve chart tooltips

This commit is contained in:
zv
2026-06-14 21:00:05 +02:00
parent bdba3c709c
commit 169d5e5e59
3 changed files with 79 additions and 24 deletions

View File

@@ -0,0 +1,49 @@
import type { ReactNode } from "react";
interface ChartTooltipPayload {
color?: string;
dataKey?: string | number;
name?: ReactNode;
payload?: Record<string, unknown>;
value?: unknown;
}
interface ChartTooltipProps {
active?: boolean;
label?: ReactNode;
labelFormatter?: (label: ReactNode) => ReactNode;
payload?: ChartTooltipPayload[];
valueFormatter?: (entry: ChartTooltipPayload) => ReactNode;
}
export function ChartTooltip({ active, label, labelFormatter, payload, valueFormatter }: ChartTooltipProps) {
if (!active || !payload?.length) return null;
return (
<div className="min-w-36 rounded-card border border-border/80 bg-surface-raised/95 px-3.5 py-3 text-xs text-foreground shadow-card backdrop-blur-xl">
{label !== undefined && (
<p className="mb-2 text-[0.68rem] font-semibold uppercase tracking-[0.14em] text-muted">
{labelFormatter ? labelFormatter(label) : label}
</p>
)}
<div className="space-y-1.5">
{payload.map((entry, index) => (
<div
className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4"
key={`${String(entry.dataKey ?? entry.name ?? "series")}-${index}`}
>
<span className="flex min-w-0 items-center gap-2 text-muted">
{entry.color && (
<span className="size-2 rounded-full" style={{ backgroundColor: entry.color }} aria-hidden="true" />
)}
<span className="truncate">{entry.name}</span>
</span>
<span className="font-semibold tabular-nums text-foreground">
{valueFormatter ? valueFormatter(entry) : String(entry.value ?? "—")}
</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -2,6 +2,7 @@
import { Bar, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { Bar, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { ChartTooltip } from "@/components/charts/chart-tooltip";
import { CHART_COLORS } from "@/lib/chart-theme"; import { CHART_COLORS } from "@/lib/chart-theme";
import { useI18n } from "@/lib/i18n"; import { useI18n } from "@/lib/i18n";
import { formatForecastRainfall, formatForecastTemperature } from "@/lib/forecast-utils"; import { formatForecastRainfall, formatForecastTemperature } from "@/lib/forecast-utils";
@@ -56,17 +57,16 @@ export function DayForecastCharts({ hours }: { hours: HourlyForecast[] }) {
unit={temperatureUnitLabel} unit={temperatureUnitLabel}
/> />
<Tooltip <Tooltip
contentStyle={{ cursor={{ stroke: CHART_COLORS.grid, strokeDasharray: "4 4" }}
borderRadius: 14, content={
border: `1px solid ${CHART_COLORS.tooltipBorder}`, <ChartTooltip
background: CHART_COLORS.tooltipBackground, valueFormatter={(entry) =>
color: CHART_COLORS.tooltipText, typeof entry.value === "number"
}} ? formatTemperatureValue(entry.value, language, temperatureUnit)
formatter={(value) => [ : formatForecastTemperature(null, language, temperatureUnit)
typeof value === "number" }
? formatTemperatureValue(value, language, temperatureUnit) />
: formatForecastTemperature(null, language, temperatureUnit), }
]}
/> />
<Legend wrapperStyle={{ fontSize: "0.72rem" }} /> <Legend wrapperStyle={{ fontSize: "0.72rem" }} />
<Line <Line
@@ -130,18 +130,16 @@ export function DayForecastCharts({ hours }: { hours: HourlyForecast[] }) {
unit="%" unit="%"
/> />
<Tooltip <Tooltip
contentStyle={{ cursor={{ fill: "hsl(var(--border) / 0.18)" }}
borderRadius: 14, content={
border: `1px solid ${CHART_COLORS.tooltipBorder}`, <ChartTooltip
background: CHART_COLORS.tooltipBackground, valueFormatter={(entry) =>
color: CHART_COLORS.tooltipText, entry.dataKey === "precipitation"
}} ? formatForecastRainfall(typeof entry.value === "number" ? entry.value : null, language)
formatter={(value, name) => [ : `${typeof entry.value === "number" ? entry.value : "—"}%`
name === t("forecast.precipitation") }
? formatForecastRainfall(typeof value === "number" ? value : null, language) />
: `${typeof value === "number" ? value : "—"}%`, }
name,
]}
/> />
<Legend wrapperStyle={{ fontSize: "0.72rem" }} /> <Legend wrapperStyle={{ fontSize: "0.72rem" }} />
<Bar <Bar

View File

@@ -2,6 +2,7 @@
import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { Bar, BarChart, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import type { SynopStation } from "@/types/imgw"; import type { SynopStation } from "@/types/imgw";
import { ChartTooltip } from "@/components/charts/chart-tooltip";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { useI18n } from "@/lib/i18n"; import { useI18n } from "@/lib/i18n";
import { useWeatherStore } from "@/lib/store"; import { useWeatherStore } from "@/lib/store";
@@ -56,7 +57,14 @@ export function SnapshotChart({ station }: { station: SynopStation }) {
/> />
<Tooltip <Tooltip
cursor={{ fill: "hsl(var(--border) / 0.22)" }} cursor={{ fill: "hsl(var(--border) / 0.22)" }}
formatter={(_, __, item) => [`${item.payload.value} ${item.payload.unit}`, item.payload.name]} content={
<ChartTooltip
valueFormatter={(entry) => {
const row = entry.payload;
return `${row?.value ?? "—"} ${row?.unit ?? ""}`.trim();
}}
/>
}
/> />
<Bar dataKey="normalized" radius={[0, 8, 8, 0]} barSize={14}> <Bar dataKey="normalized" radius={[0, 8, 8, 0]} barSize={14}>
{rows.map((row) => ( {rows.map((row) => (