import { useMemo } from "react"; import { FitnessChartPoint, FitnessChartSeries, FitnessChartsResponse } from "../api/client"; import "./FitnessCharts.css"; const CHART_KEYS = ["weight_kg", "calories", "protein_g", "water_l", "steps", "body_fat_pct"] as const; interface MetricChartProps { series: FitnessChartSeries; showTrend: boolean; granularity: "week" | "day"; } function formatTick(point: FitnessChartPoint, granularity: "week" | "day") { if (granularity === "day") { const d = new Date(`${point.date}T12:00:00`); return d.toLocaleDateString("ru-RU", { day: "numeric", month: "short" }); } const d = new Date(`${point.week_start}T12:00:00`); return d.toLocaleDateString("ru-RU", { day: "numeric", month: "short" }); } function MetricChart({ series, showTrend, granularity }: MetricChartProps) { const layout = useMemo(() => { const width = 640; const height = 168; const pad = { top: 12, right: 12, bottom: 28, left: 44 }; const plotW = width - pad.left - pad.right; const plotH = height - pad.top - pad.bottom; const active = series.points.filter((p) => p.has_data && p.value != null); if (active.length === 0) { return null; } const xMax = Math.max(1, series.points.length - 1); const values = active.map((p) => p.value as number); let yMin = Math.min(...values); let yMax = Math.max(...values); if (yMin === yMax) { yMin -= 1; yMax += 1; } else { const padY = (yMax - yMin) * 0.12; yMin -= padY; yMax += padY; } const xScale = (index: number) => pad.left + (index / xMax) * plotW; const yScale = (value: number) => pad.top + plotH - ((value - yMin) / (yMax - yMin)) * plotH; const dots = active.map((p) => ({ x: xScale(p.index), y: yScale(p.value as number), label: formatTick(p, granularity), value: p.value as number, })); let trendPath = ""; if (showTrend && series.trend?.line) { const trendPoints = series.trend.line .map((p) => `${xScale(p.index)},${yScale(p.value)}`) .join(" "); trendPath = trendPoints; } const yTicks = [yMin, (yMin + yMax) / 2, yMax]; const labelIndexes = active.length <= 4 ? active.map((p) => p.index) : [active[0].index, active[active.length - 1].index]; return { width, height, pad, plotW, plotH, yMin, yMax, yScale, xScale, dots, trendPath, yTicks, labelIndexes, }; }, [granularity, series, showTrend]); if (!layout) { return null; } const slopeLabel = granularity === "day" && series.trend && "slope_per_day" in series.trend ? `${(series.trend as { slope_per_day: number }).slope_per_day > 0 ? "+" : ""}${(series.trend as { slope_per_day: number }).slope_per_day}/день` : series.trend && "slope_per_week" in series.trend ? `${(series.trend as { slope_per_week: number }).slope_per_week > 0 ? "+" : ""}${(series.trend as { slope_per_week: number }).slope_per_week}/нед` : null; return (

{series.label} ({series.unit})

{series.data_points} {granularity === "day" ? "дн." : "нед."} {showTrend && slopeLabel ? ` · тренд ${slopeLabel}` : ""}
{layout.yTicks.map((tick) => ( {tick.toFixed(tick >= 100 ? 0 : 1)} ))} {showTrend && layout.trendPath ? ( ) : null} {layout.dots.map((dot) => ( {dot.label}: {dot.value.toFixed(1)} {series.unit} ))} {layout.labelIndexes.map((idx) => { const point = series.points[idx]; if (!point?.has_data) return null; return ( {formatTick(point, granularity)} ); })}
); } interface FitnessChartsProps { data: FitnessChartsResponse | null; showTrend: boolean; loading?: boolean; } export default function FitnessCharts({ data, showTrend, loading }: FitnessChartsProps) { if (loading) { return

Загрузка графиков…

; } if (!data) { return null; } const granularity = data.granularity; const source = granularity === "day" && data.daily_series ? data.daily_series : data.series; const charts = CHART_KEYS.map((key) => source[key]).filter( (series) => series && series.data_points > 0, ); return (

Динамика за год

{granularity === "day" ? `Мало данных (${data.days_with_data} дн.) — показаны дневные точки` : `Недельные точки · заполнено ${data.weeks_with_data} из ${data.weeks} нед.`}

{charts.length === 0 ? (

Пока нет данных для графиков. Логируй еду, воду, шаги или вес — точки появятся автоматически.

) : (
{charts.map((series) => ( ))}
)}
); }