import React, { useMemo, useRef } from "react"; import { RegionEvolutionPoint } from "../../../models/RegionEvolution"; import { WIDTH, HEIGHT, MARGIN, extent, padDomain, scaleLinear, buildPath, colorForRegion, } from "./timeseries.utils"; import "./TimeSeries.css"; export type HoverPoint = { x: number; y: number; year: number; regionName: string; rate: number; }; type Props = { series: Map; raw: RegionEvolutionPoint[]; hover: HoverPoint | null; selectedRegion: string | null; onHoverChange: (h: HoverPoint | null) => void; onSelectRegion: (region: string) => void; }; export const TimeSeriesChart: React.FC = ({ series, raw, hover, selectedRegion, onHoverChange, onSelectRegion, }) => { const svgRef = useRef(null); const years = useMemo(() => raw.map((d) => d.year), [raw]); const rates = useMemo(() => raw.map((d) => d.rate), [raw]); const innerW = WIDTH - MARGIN.left - MARGIN.right; const innerH = HEIGHT - MARGIN.top - MARGIN.bottom; const xDomain = useMemo(() => extent(years), [years]); const yDomain = useMemo(() => padDomain(extent(rates)), [rates]); const xScale = useMemo( () => scaleLinear(xDomain, [MARGIN.left, MARGIN.left + innerW]), [xDomain, innerW] ); const yScale = useMemo( () => scaleLinear(yDomain, [MARGIN.top + innerH, MARGIN.top]), [yDomain, innerH] ); const xTicks = useMemo(() => { const [minY, maxY] = xDomain; const ticks: number[] = []; for (let y = Math.round(minY); y <= Math.round(maxY); y++) ticks.push(y); return ticks; }, [xDomain]); const yTicks = useMemo(() => { const [min, max] = yDomain; const count = 5; const step = (max - min) / (count - 1); return Array.from({ length: count }, (_, i) => min + i * step); }, [yDomain]); function handleMouseMove(e: React.MouseEvent) { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; let best: HoverPoint | null = null; let bestDist = Infinity; for (const [regionName, pts] of Array.from(series.entries())) { for (const p of pts) { const px = xScale(p.year); const py = yScale(p.rate); const d = (px - mouseX) ** 2 + (py - mouseY) ** 2; if (d < bestDist) { bestDist = d; best = { x: px, y: py, year: p.year, regionName, rate: p.rate }; } } } if (best && bestDist < 500) onHoverChange(best); else onHoverChange(null); } return (
onHoverChange(null)} /> {xTicks.map((t) => { const x = xScale(t); const y = MARGIN.top + innerH; return ( {t} ); })} {yTicks.map((t, i) => { const y = yScale(t); return ( {t.toFixed(1)} ); })} {Array.from(series.entries()).map(([regionName, pts]) => { const path = buildPath(pts.map((p) => ({ x: xScale(p.year), y: yScale(p.rate) }))); const isSelected = selectedRegion === regionName; const isHovered = hover?.regionName === regionName; return ( onSelectRegion(regionName)} > ); })} {hover && ( {hover.regionName} {hover.year} — taux : {hover.rate.toFixed(2)} )}
); };