// ----------------------------------------------------------------------------- // Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // Charts.tsx : graphiques SVG maison — histogramme, barres horizontales, donut. // Marques fines, bouts arrondis, étiquettes directes sélectives, infobulle // au survol, vue tableau repliable (accessibilité). Palette catégorielle // validée (contraste + daltonisme) : #d94f1e #1f6fb5 #1c7a4d #b58500 // #7d4fc9 #a05a2c. // ----------------------------------------------------------------------------- import { ReactNode, useState } from "react"; export const CAT_COLORS = ["#d94f1e", "#1f6fb5", "#1c7a4d", "#b58500", "#7d4fc9", "#a05a2c"]; export interface Datum { label: string; n: number; extra?: string; } const fmt = (n: number) => Math.round(n).toLocaleString("fr-CA"); function DataTable({ rows, valueLabel }: { rows: Datum[]; valueLabel: string }) { return (
Voir les données {rows.map((r) => ( ))}
Catégorie{valueLabel}
{r.label} {fmt(r.n)}{r.extra ? ` · ${r.extra}` : ""}
); } function Tip({ x, y, children }: { x: number; y: number; children: ReactNode }) { return (
{children}
); } /** Histogramme vertical — magnitude d'une distribution, une seule teinte. */ export function VBars({ data, color = "#d94f1e", valueLabel = "véhicules", height = 190 }: { data: Datum[]; color?: string; valueLabel?: string; height?: number }) { const [tip, setTip] = useState<{ x: number; y: number; d: Datum } | null>(null); if (!data.length) return null; const max = Math.max(...data.map((d) => d.n), 1); const W = 720, PAD = 6, LBL = 28; const bw = (W - PAD * (data.length - 1)) / data.length; const plotH = height - LBL - 18; return (
setTip(null)}> {data.map((d, i) => { const h = Math.max(3, (d.n / max) * plotH); const x = i * (bw + PAD); const y = 18 + (plotH - h); const show = d.n === max || i === 0 || i === data.length - 1; return ( { const r = (e.currentTarget.closest(".chart-wrap") as HTMLElement).getBoundingClientRect(); setTip({ x: e.clientX - r.left, y: e.clientY - r.top - 10, d }); }} /> {show && ( {fmt(d.n)} )} {d.label} ); })} {tip && {tip.d.label} — {fmt(tip.d.n)} {valueLabel}{tip.d.extra ? ` · ${tip.d.extra}` : ""}}
); } /** Barres horizontales — classement d'une même mesure, une seule teinte. */ export function HBars({ data, color = "#d94f1e", valueLabel = "véhicules" }: { data: Datum[]; color?: string; valueLabel?: string }) { const [tip, setTip] = useState<{ x: number; y: number; d: Datum } | null>(null); if (!data.length) return null; const max = Math.max(...data.map((d) => d.n), 1); const W = 720, ROW = 26, LBL = 150; const H = data.length * ROW; return (
setTip(null)}> {data.map((d, i) => { const w = Math.max(4, (d.n / max) * (W - LBL - 120)); const y = i * ROW + 4; return ( {d.label.length > 22 ? d.label.slice(0, 21) + "…" : d.label} { const r = (e.currentTarget.closest(".chart-wrap") as HTMLElement).getBoundingClientRect(); setTip({ x: e.clientX - r.left, y: e.clientY - r.top - 10, d }); }} /> {fmt(d.n)}{d.extra ? ` · ${d.extra}` : ""} ); })} {tip && {tip.d.label} — {fmt(tip.d.n)} {valueLabel}{tip.d.extra ? ` · ${tip.d.extra}` : ""}}
); } /** Donut — parts d'un tout (≤ 6 catégories + « Autres »), palette validée. */ export function Donut({ data, valueLabel = "véhicules" }: { data: Datum[]; valueLabel?: string }) { const [tip, setTip] = useState<{ x: number; y: number; d: Datum; pct: number } | null>(null); if (!data.length) return null; const top = data.slice(0, 5); const rest = data.slice(5).reduce((s, d) => s + d.n, 0); const parts: Datum[] = rest > 0 ? [...top, { label: "Autres", n: rest }] : top; const total = parts.reduce((s, d) => s + d.n, 0) || 1; const R = 80, r = 46, CX = 110, CY = 100; let angle = -Math.PI / 2; const arcs = parts.map((d, i) => { const span = (d.n / total) * Math.PI * 2; const a0 = angle, a1 = angle + span; angle = a1; const large = span > Math.PI ? 1 : 0; const p = (a: number, rad: number) => `${CX + rad * Math.cos(a)},${CY + rad * Math.sin(a)}`; return { d, pct: (d.n / total) * 100, color: CAT_COLORS[i % CAT_COLORS.length], path: `M ${p(a0, R)} A ${R} ${R} 0 ${large} 1 ${p(a1, R)} L ${p(a1, r)} A ${r} ${r} 0 ${large} 0 ${p(a0, r)} Z`, }; }); return (
setTip(null)}> {arcs.map((a) => ( { const rr = (e.currentTarget.closest(".chart-wrap") as HTMLElement).getBoundingClientRect(); setTip({ x: e.clientX - rr.left, y: e.clientY - rr.top - 10, d: a.d, pct: a.pct }); }} /> ))} {fmt(total)} {valueLabel}
{arcs.map((a) => (
{a.d.label} {a.pct.toFixed(1)} %
))}
{tip && {tip.d.label} — {fmt(tip.d.n)} {valueLabel} ({tip.pct.toFixed(1)} %)}
); }