diff --git a/apps/planner/app/components/ElevationChart.tsx b/apps/planner/app/components/ElevationChart.tsx index b995d66..0f6e13a 100644 --- a/apps/planner/app/components/ElevationChart.tsx +++ b/apps/planner/app/components/ElevationChart.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useRef, useCallback } from "react"; import { useTranslation } from "react-i18next"; +import { IconButton } from "@trails-cool/ui"; import type { DayStage } from "@trails-cool/gpx"; import type { YjsState } from "~/lib/use-yjs"; import { @@ -24,11 +25,68 @@ interface ElevationChartProps { onClickPosition?: (position: [number, number]) => void; onDragSelect?: (bounds: [[number, number], [number, number]]) => void; days?: DayStage[]; + /** Authoritative route stats (same source as the sidebar) for the summary. */ + routeStats?: { distance?: number; elevationGain?: number; elevationLoss?: number }; } -export function ElevationChart({ yjs, onHover, highlightDistance, onClickPosition, onDragSelect, days }: ElevationChartProps) { +const COLLAPSE_KEY = "planner:elevationCollapsed"; + +const ChevronDown = () => ( + + + +); +const ChevronUp = () => ( + + + +); + +/** Tiny inline elevation sparkline for the collapsed summary bar. */ +function Sparkline({ points }: { points: Array<{ distance: number; elevation: number }> }) { + if (points.length < 2) return null; + const W = 72; + const H = 18; + const eles = points.map((p) => p.elevation); + const min = Math.min(...eles); + const range = Math.max(...eles) - min || 1; + const maxD = points[points.length - 1]!.distance || 1; + const d = points + .map((p) => `${((p.distance / maxD) * W).toFixed(1)},${(H - ((p.elevation - min) / range) * H).toFixed(1)}`) + .join(" "); + return ( + + + + ); +} + +export function ElevationChart({ yjs, onHover, highlightDistance, onClickPosition, onDragSelect, days, routeStats }: ElevationChartProps) { const { t } = useTranslation("planner"); const { points, colorMode, surfaces, highways, maxspeeds, smoothnesses, tracktypes, cycleways, bikeroutes } = useElevationData(yjs.routeData); + const [collapsed, setCollapsed] = useState( + () => typeof window !== "undefined" && localStorage.getItem(COLLAPSE_KEY) === "1", + ); + const toggleCollapsed = useCallback(() => { + setCollapsed((c) => { + const next = !c; + try { + localStorage.setItem(COLLAPSE_KEY, next ? "1" : "0"); + } catch { + /* ignore */ + } + return next; + }); + }, []); + + // Collapsed-bar summary. Uses the authoritative routeStats (same source as + // the sidebar) so the figures match; falls back to the chart's own distance + // only when stats aren't available yet. + const distanceMeters = routeStats?.distance ?? (points.length ? points[points.length - 1]!.distance : 0); + const distanceKm = (distanceMeters / 1000).toFixed(1); + const ascent = routeStats?.elevationGain; + const descent = routeStats?.elevationLoss; + const [hoverIdx, setHoverIdx] = useState(null); const canvasRef = useRef(null); const isExternalHover = useRef(false); @@ -382,6 +440,32 @@ export function ElevationChart({ yjs, onHover, highlightDistance, onClickPositio if (points.length < 2) return null; + if (collapsed) { + return ( + + + + {distanceKm} km + {ascent !== undefined && ( + <> + ·↑ {ascent} m + > + )} + {descent !== undefined && ( + <> + ·↓ {descent} m + > + )} + + + + + + + + ); + } + return ( @@ -503,6 +587,14 @@ export function ElevationChart({ yjs, onHover, highlightDistance, onClickPositio {t("colorMode.cycleway")} {t("colorMode.bikeroute")} + + + - + {isZoomedByChart && (