// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/NeighborhoodStats.tsx : « Quartier » — KPI compacts du recensement // (aire de diffusion), accessibilité StatCan en lignes compactes (4 par // défaut + « Voir tous les indicateurs »), îlot de chaleur, criminalité // (résumé + détail par catégorie en accordéon), sources. // ----------------------------------------------------------------------------- import { useState } from "react"; import { Quartier } from "../api"; import { IcoUsers } from "../components/Icons"; import { Accordion, MoreButton, SectionCard, SourceLine, StatTile, StatusBadge, NBSP } from "./ui"; const fmtMoney = (v: number | null | undefined) => (v == null ? null : `${Math.round(v / 1000)}${NBSP}k$`); const fmtPct = (v: number | null | undefined) => (v == null ? null : `${Math.round(v)}${NBSP}%`); const PROX: [string, string][] = [ ["prox_epicerie", "Épiceries"], ["prox_transport", "Transport"], ["prox_pharmacie", "Pharmacies"], ["prox_parc", "Parcs"], ["prox_ecole_prim", "Écoles primaires"], ["prox_ecole_sec", "Écoles secondaires"], ["prox_sante", "Soins de santé"], ["prox_garderie", "Garderies"], ["prox_bibliotheque", "Bibliothèques"], ["prox_emploi", "Emplois"], ]; const niveau = (v: number) => (v >= 90 ? ["Excellent", "good"] : v >= 70 ? ["Très bon", "good"] : v >= 45 ? ["Moyen", "warn"] : ["Faible", "bad"]) as [string, string]; export default function NeighborhoodStats({ q }: { q: Quartier }) { const [all, setAll] = useState(false); const d = q.demographie; const kpis: [string, string | null][] = d ? [ ["Revenu médian", fmtMoney(d.revenu_median)], ["Locataires", fmtPct(d.pct_locataires)], ["Loyer moyen 2021", d.loyer_moyen != null ? `${Math.round(d.loyer_moyen).toLocaleString("fr-CA")}${NBSP}$` : null], ["Âge médian", d.age_median != null ? `${Math.round(d.age_median)} ans` : null], ["Français à la maison", fmtPct(d.pct_francais)], ["Universitaires", fmtPct(d.pct_univ)], ] : []; const kpisOk = kpis.filter(([, v]) => v != null) as [string, string][]; const prox = q.proximite ?? {}; const proxOk = PROX.filter(([k]) => typeof prox[k] === "number") .map(([k, l]) => [l, Math.round(Math.max(0, Math.min(1, prox[k])) * 100)] as [string, number]); const shown = all ? proxOk : proxOk.slice(0, 4); const crime = q.crime; if (kpisOk.length === 0 && proxOk.length === 0 && !q.chaleur && !crime) return null; return ( } sub="Secteur immédiat de l'immeuble (aire de diffusion du recensement, ±500 habitants)"> {kpisOk.length > 0 && (
{kpisOk.map(([l, v]) => )}
)} {proxOk.length > 0 && ( <>

Accessibilité du quartier · indice 0–100 StatCan

{shown.map(([l, v]) => { const [lbl, tone] = niveau(v); return (
{l} {v} {lbl}
); })}
{proxOk.length > 4 && setAll(!all)} expanded={all}>{all ? "Réduire" : "Voir tous les indicateurs"}} )} {(q.chaleur || crime) && (
{q.chaleur && ( q.chaleur.classe <= 3 ? Îlot de fraîcheur : q.chaleur.classe >= 7 ? Îlot de chaleur{q.chaleur.ecart != null ? ` (+${q.chaleur.ecart.toFixed(1)}${NBSP}°C)` : ""} : Température de quartier moyenne )} {crime?.type === "points" && ( {crime.douze_mois} acte{crime.douze_mois > 1 ? "s" : ""} criminel{crime.douze_mois > 1 ? "s" : ""} à moins de {crime.rayon_m} m (12 mois) {crime.douze_mois_precedents > 0 && (crime.douze_mois <= crime.douze_mois_precedents ? " · en baisse" : " · en hausse")} )} {crime?.type === "igc" && (() => { const c = crime; if (c.indice_canada != null && c.indice_canada > 0) { const delta = Math.round(100 * (c.indice - c.indice_canada) / c.indice_canada); return Criminalité {Math.abs(delta)}{NBSP}% {delta <= 0 ? "sous" : "au-dessus de"} la moyenne canadienne; } return Indice de gravité de la criminalité : {c.indice}; })()}
)} {crime?.type === "points" && (crime.categories?.length ?? 0) > 0 && (
{crime.categories!.filter((c) => c.n + c.n_prec > 0).map((c) => { const max = Math.max(...crime.categories!.map((x) => x.n), 1); const delta = c.n - c.n_prec; return (
{c.nom} {c.n} {delta === 0 ? "=" : delta > 0 ? `▲${delta}` : `▼${-delta}`}
); })}

Actes criminels enregistrés par le SPVM (données ouvertes, position approximée à l'intersection) — 12 derniers mois vs les 12 précédents.

)} {crime?.type === "igc" && (

Indice de gravité de la criminalité (Statistique Canada) — {crime.ville}, {crime.annee} : {crime.indice}{crime.indice_canada != null ? ` · Canada : ${crime.indice_canada}` : ""}.

)}
); }