# Bandeau KPI = kpi_row (grille auto-fit) : 3 tuiles CHIFFRES + 1 tuile à DEUX MINI-GRAPHES.
# ⚠️ kpi_tiles_synth (jcn-rpt-kpi) = signature (category, items, footer) — À NE PAS CONFONDRE avec
# kpi_tiles_psect (jcn-kpi-tiles, ex-psect) qui prend title/poids/dyn. Collision de noms résolue
# par le renommage 260822 : ici on veut la version bandeau.
# ⚠️ Un ggplot NE PEUT PAS entrer dans une cellule de grille → gg_img() le convertit en PNG base64.
#
# ÉVOLUTIONS EN SOUS-LIGNE (item$note) : évolution TOTALE, jamais %/an — conv projet, un %/an sur
# 3 ans ne parle à personne alors qu'un « −9 % depuis 2022 » se retient. Deux références (2022 = le
# retournement, 2019 = l'avant-Covid) sur une ligne italique EN RETRAIT, qui dit visuellement
# qu'elle qualifie le chiffre au-dessus. Et la SPARKLINE est rattachée à SON indicateur (item$spark)
# plutôt qu'en pied de tuile, où l'on ne savait pas quelle courbe illustrait quel chiffre.
.tot <- function(tcam, n) if (!is.finite(tcam)) NA_real_ else ((1 + tcam / 100)^n - 1) * 100
# forme COMPACTE « ▼ −3,3 % / 2022 » : le `ref` de kpi_evo pose déjà l'année en gris clair. Écrire
# « depuis 2022 » en toutes lettres faisait passer la note sur 3 lignes dans la colonne label.
# Deux lignes ASSUMÉES (<br>) plutôt qu'un séparateur « · » : la colonne label est étroite, le point
# médian se retrouvait orphelin en fin de ligne au gré du retour automatique.
.note2 <- function(t22, t19) paste0(
kpi_evo(t22, "%", ref = "2022"),
if (is.finite(t19)) paste0(" · ", kpi_evo(t19, "%", ref = "2019")) else "")
# Légende de sparkline : sans elle on ne sait pas quelle courbe illustre quel chiffre.
.sp_cap <- function(txt) sprintf(
"<div style='font-size:.7rem;color:#9a9a9a;margin-bottom:1px'>%s</div>", txt)
.p_ix <- SER_FR$px_ix # médiane des indices départementaux, base 100 = 2015
.p_tr <- SER_FR$trans / 1000 # transactions, milliers/an
# ⚠️ Transactions : les totaux nationaux se lisent sur la SOMME des départements (des stocks, la
# somme est exacte) — surtout PAS sur la médiane des TCAM départementaux, qui n'est pas le TCAM
# national et peut même en inverser le signe.
.trv <- stats::setNames(SER_FR$trans, as.character(SER_FR$annee))
sy_tr_t22 <- (.trv[["2025"]] / .trv[["2022"]] - 1) * 100
sy_tr_t19 <- (.trv[["2025"]] / .trv[["2019"]] - 1) * 100
# Évolutions totales du prix, dérivées des TCAM NATIFS de dbcln (aucune approximation) :
# (1+r1925)^6 = (1+r1922)^3 × (1+r2225)^3 → la fenêtre 19-22, absente de dbcln, s'en déduit exactement.
sy_px_t2225 <- .tot(sy_px_ev, 3)
sy_px_t1922 <- ((1 + sy_px_evl / 100)^6 / (1 + sy_px_ev / 100)^3 - 1) * 100
# ⚠️ PIÈGE ggplot2 : theme_urbn() règle EXPLICITEMENT axis.title.x / axis.title.y. Or l'héritage de
# thème va du parent vers l'ENFANT, et l'enfant l'emporte → `axis.title = element_blank()` NE LES
# EFFACE PAS. Il faut viser les enfants.
# STYLE DÉPOUILLÉ (user 260825) : ni quadrillage, ni fond blanc, ni graduations — dans une tuile de
# 155 px ces éléments ne portent aucune information lisible et ne font qu'ajouter du bruit. Ne
# restent que la forme de la courbe et deux repères d'années. Fond TRANSPARENT pour épouser le bleu
# de la tuile au lieu d'y découper un rectangle blanc.
# Les mini-graphes ggplot de l'ancienne tuile « Le retournement » ont ete RETIRES : ils
# retracaient exactement les sparklines des tuiles 1 et 2. Leur habillage n'est pas perdu pour
# autant — il est promu dans mutils (`kpi_minichart()` / `kpi_minichart_ends()`, jcn-kpi-tiles.R)
# et reste utilisable partout. La tuile 4 accueille desormais la CONSTRUCTION. (260828)
.p_ct <- SER_CONSTR$logaut / 1000 # logements autorises, milliers/an
# delta SANS triangle : le nombre est deja signe, le triangle faisait doublon et debordait.
.dl <- function(t) kpi_evo(t, "%", ref = NULL, arrow = FALSE)
kpi_row(cols = 4,
kpi_tiles_synth(kcat("currency-euro", "Prix médian au m²"),
sub = "2025 · évol / 2022", compact = TRUE, delta_style = "inline",
items = list(
list(value = paste0(fmt_fr(sy_px, 0), " €"), label = "Ensemble",
delta = .dl(.tot(sy_px_ev, 3))),
list(value = paste0(fmt_fr(KPI_LOG$px_apt_25, 0), " €"), label = "Appart.",
delta = .dl(.tot(KPI_LOG$px_apt_ev, 3))),
list(value = paste0(fmt_fr(KPI_LOG$px_mai_25, 0), " €"), label = "Maison",
delta = .dl(.tot(KPI_LOG$px_mai_ev, 3)))),
footer = kpi_sparkline(.p_ix, w = 150, h = 46, annot = TRUE, lab_left = "2015", lab_right = "2025",
annot_fmt = function(v) formatC(v, format = "f", digits = 0))),
kpi_tiles_synth(kcat("arrow-left-right", "Transactions immobilières"),
sub = "Mutations 2025 · évol / 2022", compact = TRUE, delta_style = "inline",
items = list(
list(value = fmt_k(KPI_LOG$trans_25), label = "Ensemble",
delta = .dl(sy_tr_t22)),
list(value = fmt_k(KPI_LOG$trans_apt_25), label = "Appart.",
delta = .dl(KPI_LOG$trans_apt_ev)),
list(value = fmt_k(KPI_LOG$trans_mai_25), label = "Maisons",
delta = .dl(KPI_LOG$trans_mai_ev))),
footer = kpi_sparkline(.p_tr, col1 = "#8a939b", area = TRUE, zero_base = TRUE,
w = 150, h = 46, annot = TRUE, lab_left = "2015", lab_right = "2025", xlab = SER_CONSTR$annee,
annot_fmt = function(v) paste0(formatC(v, format = "f", digits = 0), " k"))),
kpi_tiles_synth(kcat("house-door", "État du parc"),
sub = "Parc · 2023-2026 · évol / 2017", compact = TRUE, delta_style = "inline",
items = list(
list(value = paste0(fmt_fr(sy_vac, 1), " %"), label = "Logements vacants",
delta = .dl(KPI_LOG$vac_evol)),
list(value = paste0(fmt_fr(KPI_LOG$vac2ans, 1), " %"), label = "Vacants > 2 ans"),
list(value = paste0(fmt_fr(sy_dpe, 1), " %"), label = "Passoires F-G"))),
# ⚠️ AUTORISÉS, pas commencés : entre le permis et le chantier il y a un délai, et une part des
# permis n'est jamais mise en œuvre. Et la série SITADEL s'arrête en 2025, pas 2026 comme le DPE.
# DOUBLE ÉVOLUTION (delta2) : / 2022 = la chute du retournement, / 2024 = le rebond en cours —
# une seule référence cachait l'inflexion 2025 (base SITADEL 3 : rebond +15 % vs 2024).
# sub porte DEJA les deux references -> la ligne d'en-tetes de colonnes ferait doublon (retiree,
# user 260901). delta_style="inline" : deltas colles au libelle plutot qu'en rangee dediee.
kpi_tiles_synth(kcat("building", "Logements autorisés"),
sub = "2025 · évol / 2022 / 2024", compact = TRUE, delta_style = "inline",
items = list(
list(value = fmt_k(KPI_LOG$constr_25), label = "Ensemble",
delta = .dl(KPI_LOG$constr_ev), delta2 = .dl(KPI_LOG$constr_ev24)),
list(value = fmt_k(KPI_LOG$constr_ind_25), label = "Individuel",
delta = .dl(KPI_LOG$constr_ind_ev), delta2 = .dl(KPI_LOG$constr_ind_ev24)),
list(value = fmt_k(KPI_LOG$constr_m2_25), label = "m² autorisés",
delta = .dl(KPI_LOG$constr_m2_ev), delta2 = .dl(KPI_LOG$constr_m2_ev24))),
footer = kpi_sparkline(.p_ct, col1 = "#8a6d3b", area = TRUE, zero_base = TRUE,
w = 150, h = 46, annot = TRUE, lab_left = "2015", lab_right = "2025", xlab = SER_CONSTR$annee,
annot_fmt = function(v) paste0(formatC(v, format = "f", digits = 0), " k"))))