import { useState } from "react"; import { useTranslation } from "react-i18next"; import { formatDistanceKm } from "~/lib/stats"; export interface WeeklyDistanceBucket { weekStart: string; distance: number; } // SVG layout (user units; rendered responsive via viewBox). const W = 480; const H = 132; const PAD = { top: 10, right: 6, bottom: 18, left: 36 }; const PLOT_W = W - PAD.left - PAD.right; const PLOT_H = H - PAD.top - PAD.bottom; const BASE_Y = PAD.top + PLOT_H; function axisLabel(km: number): string { if (km <= 0) return "0"; return km < 10 ? `${km.toFixed(1)} km` : `${Math.round(km)} km`; } /** * Weekly-distance bar chart for the profile (last N weeks, oldest → newest). * Gridlines + a y-scale topped at the busiest week, faint per-week tracks so * the 12-week axis is always visible (empty weeks read as gaps, not nothing), * and a hover readout naming the week + its distance. Hidden when there is no * distance in the window. */ export function WeeklyDistanceChart({ weeks, className, }: { weeks: WeeklyDistanceBucket[]; className?: string; }) { const { t, i18n } = useTranslation("journal"); const [hover, setHover] = useState(null); const maxM = weeks.reduce((m, w) => Math.max(m, w.distance), 0); if (maxM <= 0) return null; const maxKm = maxM / 1000; const n = weeks.length; const colW = PLOT_W / n; const barW = colW * 0.6; const x = (i: number) => PAD.left + i * colW + (colW - barW) / 2; const yFor = (m: number) => BASE_Y - (m / maxM) * PLOT_H; const fmtWeek = (iso: string) => new Date(`${iso}T00:00:00`).toLocaleDateString(i18n.language, { month: "short", day: "numeric" }); const gridFracs = [0, 0.5, 1]; const active = hover != null ? weeks[hover] : null; const first = weeks[0]; const last = weeks[n - 1]; const firstLabel = first ? fmtWeek(first.weekStart) : ""; const lastLabel = last ? fmtWeek(last.weekStart) : ""; return (
{t("profileStats.weeklyDistance")} {active && ( {t("profileStats.weekOf", { date: fmtWeek(active.weekStart) })} ·{" "} {formatDistanceKm(active.distance)} )}
setHover(null)} > {/* gridlines + y-axis labels (0 · half · peak) */} {gridFracs.map((f) => { const yy = BASE_Y - f * PLOT_H; return ( {axisLabel(maxKm * f)} ); })} {weeks.map((w, i) => { const isActive = hover === i; return ( {/* faint week-slot track so empty weeks stay visible */} {/* distance bar */} {w.distance > 0 && ( )} {/* full-height hover hit area */} setHover(i)} > {`${fmtWeek(w.weekStart)} · ${formatDistanceKm(w.distance)}`} ); })}
{firstLabel} {lastLabel}
); }