SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%

Quartier : criminalité SPVM détaillée par catégorie (12 mois, tendance)

_crime_mtl ventile désormais les actes par catégorie (vol dans/sur véhicule,
introduction, vol de véhicule, méfait, vol qualifié, infractions mortelles) ;
le bloc quartier affiche des barres par catégorie avec la variation vs les
12 mois précédents (flèches chiffrées, jamais la couleur seule).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 24, 2026) parent 093aac9

4 changed files +64 −11

modified frontend/src/api.ts +2 −1
@@ -66,7 +66,8 @@ export interface Quartier {
66 66 defavorisation?: { quintile_materiel: number | null; quintile_social: number | null };
67 67 chaleur?: { classe: number; ecart: number | null }; // 1 fraîcheur … 9 chaleur
68 68 crime?:
69 − | { type: "points"; rayon_m: number; douze_mois: number; douze_mois_precedents: number }
69 + | { type: "points"; rayon_m: number; douze_mois: number; douze_mois_precedents: number;
70 + categories?: { nom: string; n: number; n_prec: number }[] }
70 71 | { type: "igc"; ville: string; annee: number; indice: number; indice_canada: number | null };
71 72 }
72 73
modified frontend/src/components/QuartierBlock.tsx +36 −8
@@ -94,15 +94,43 @@ export default function QuartierBlock({ q }: { q: Quartier }) {
94 94 return <span className={b.cls}>{b.ico} {b.txt}</span>;
95 95 })()}
96 96 {q.crime?.type === "points" && (
97 − <span className="q-badge neutral">
98 − 🛡 {q.crime.douze_mois} acte{q.crime.douze_mois > 1 ? "s" : ""} criminel{q.crime.douze_mois > 1 ? "s" : ""} à
99 − moins de 500 m (12 mois)
100 − {q.crime.douze_mois_precedents > 0 && (
101 − q.crime.douze_mois <= q.crime.douze_mois_precedents
102 − ? ` · en baisse (${q.crime.douze_mois_precedents} l'année d'avant)`
103 − : ` · en hausse (${q.crime.douze_mois_precedents} l'année d'avant)`
97 + <div className="q-crime">
98 + <span className="q-badge neutral">
99 + 🛡 {q.crime.douze_mois} acte{q.crime.douze_mois > 1 ? "s" : ""} criminel{q.crime.douze_mois > 1 ? "s" : ""} à
100 + moins de 500 m (12 mois)
101 + {q.crime.douze_mois_precedents > 0 && (
102 + q.crime.douze_mois <= q.crime.douze_mois_precedents
103 + ? ` · en baisse (${q.crime.douze_mois_precedents} l'année d'avant)`
104 + : ` · en hausse (${q.crime.douze_mois_precedents} l'année d'avant)`
105 + )}
106 + </span>
107 + {(q.crime.categories?.length ?? 0) > 0 && (
108 + <ul className="q-crime-cats">
109 + {q.crime.categories!.filter((c) => c.n + c.n_prec > 0).map((c) => {
110 + const max = Math.max(...q.crime!.type === "points"
111 + ? q.crime!.categories!.map((x) => x.n) : [1], 1);
112 + const delta = c.n - c.n_prec;
113 + return (
114 + <li key={c.nom}>
115 + <span className="q-crime-nom">{c.nom}</span>
116 + <span className="q-crime-barre" aria-hidden="true">
117 + <i style={{ width: `${Math.max(3, (c.n / max) * 100)}%` }} />
118 + </span>
119 + <span className="q-crime-n">{c.n}
120 + <small>{delta === 0 ? " =" : delta > 0
121 + ? ` ▲${delta}` : ` ▼${-delta}`}</small>
122 + </span>
123 + </li>
124 + );
125 + })}
126 + </ul>
104 127 )}
105 − </span>
128 + <p className="fine q-crime-src">
129 + Actes criminels enregistrés par le SPVM (données ouvertes,
130 + position approximée à l'intersection) — 12 derniers mois,
131 + variation vs les 12 précédents.
132 + </p>
133 + </div>
106 134 )}
107 135 {q.crime?.type === "igc" && (() => {
108 136 const c = q.crime;
modified frontend/src/styles.css +17 −0
@@ -1915,3 +1915,20 @@ html { scroll-padding-top: 76px; } /* header sticky au-dessus des ancres */
1915 1915 .rdl-bars-axe { stroke: var(--line, #e6e4df); stroke-width: 1; }
1916 1916 .rdl-ecart { margin: 4px 0 8px; }
1917 1917 .rdl-cap { caption-side: top; text-align: left; font-size: 13px; font-weight: 700; color: var(--navy); padding: 6px 0; }
1918 +
1919 +
1920 +/* ---- Criminalité détaillée SPVM (bloc quartier) ---- */
1921 +.q-crime { width: 100%; }
1922 +.q-crime-cats { list-style: none; margin: 8px 0 0; padding: 0; }
1923 +.q-crime-cats li { display: flex; align-items: center; gap: 8px;
1924 + padding: 3px 0; font-size: 12.5px; }
1925 +.q-crime-nom { flex: 0 0 46%; color: var(--ink-2); overflow: hidden;
1926 + text-overflow: ellipsis; white-space: nowrap; }
1927 +.q-crime-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);
1928 + border-radius: 5px; overflow: hidden; }
1929 +.q-crime-barre i { display: block; height: 100%; border-radius: 5px;
1930 + background: var(--accent, #ff6a00); opacity: 0.6; }
1931 +.q-crime-n { flex: 0 0 52px; text-align: right; font-weight: 600;
1932 + color: var(--ink); white-space: nowrap; }
1933 +.q-crime-n small { font-weight: 400; color: var(--ink-3); }
1934 +.q-crime-src { margin-top: 8px; }
modified louka/quartier.py +9 −2
@@ -96,24 +96,31 @@ def _crime_mtl(qcon: sqlite3.Connection, lat: float, lng: float) -> dict | None:
96 96 dlng = 500 / (111000.0 * max(0.2, math.cos(math.radians(lat))))
97 97 now = time.time()
98 98 rows = qcon.execute(
99 − "SELECT lat, lng, ts FROM crime_mtl WHERE lat BETWEEN ? AND ?"
99 + "SELECT lat, lng, ts, categorie FROM crime_mtl WHERE lat BETWEEN ? AND ?"
100 100 " AND lng BETWEEN ? AND ? AND ts >= ?",
101 101 (lat - dlat, lat + dlat, lng - dlng, lng + dlng, now - 730 * 86400)).fetchall()
102 102 recent = avant = 0
103 + cats: dict[str, list[int]] = {} # categorie -> [12 mois, 12 prec.]
103 104 for r in rows:
104 105 # distance exacte (le bbox est un carré)
105 106 d = math.hypot((r["lat"] - lat) * 111000.0,
106 107 (r["lng"] - lng) * 111000.0 * math.cos(math.radians(lat)))
107 108 if d > 500:
108 109 continue
110 + c = cats.setdefault(r["categorie"] or "Autre", [0, 0])
109 111 if r["ts"] >= now - 365 * 86400:
110 112 recent += 1
113 + c[0] += 1
111 114 else:
112 115 avant += 1
116 + c[1] += 1
113 117 if recent == 0 and avant == 0:
114 118 return None
119 + categories = [{"nom": k, "n": v[0], "n_prec": v[1]}
120 + for k, v in sorted(cats.items(),
121 + key=lambda kv: -(kv[1][0] + kv[1][1]))]
115 122 return {"type": "points", "rayon_m": 500, "douze_mois": recent,
116 − "douze_mois_precedents": avant}
123 + "douze_mois_precedents": avant, "categories": categories}
117 124
118 125
119 126 def _crime_igc(qcon: sqlite3.Connection, city: str) -> dict | None:
120 127