Carte tendance : deuxieme courbe du nombre de magasins vendeurs

Une quantite qui monte parce que le produit est diffuse dans plus de
magasins ne raconte pas la meme histoire qu'une quantite qui monte a
diffusion constante. La courbe des quantites seule ne permettait pas de
distinguer un succes produit d'un simple elargissement.

- computeStoresSeries() : serie « magasins vendeurs » sur la MEME fenetre
  12 mois que la tendance, avec la meme regle du tout ou rien — les 12
  cles doivent etre presentes, sinon null. Un mois non extrait ne doit
  pas se lire comme « zero magasin », ce qui simulerait un arret de
  diffusion.
- nbMagReseauByMonth remonte du cache (metricsByMonth, deja stocke par la
  sync) jusqu'a la ligne de grille.
- NetworkLineChart accepte une serie `stores` optionnelle.

DEUX ECHELLES INDEPENDANTES : les quantites se comptent en milliers, les
magasins plafonnent a ~270. Sur une echelle commune la courbe des
magasins serait ecrasee sur l'axe et ne montrerait rien. Chaque serie est
normalisee sur son propre maximum, rappele dans la legende — c'est la
FORME des deux courbes qu'on compare, pas leurs hauteurs. La courbe des
magasins est pointillee et indigo, pour ne jamais entrer en collision
avec le vert/rouge/gris de la tendance.

Branchee sur la carte de la Grille et sur celle de la fiche produit. La
sparkline de la colonne reste a une seule courbe : 46x18 px, deux traces
y seraient illisibles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
Claude committed 2026-08-01 09:07:39 +00:00
1 parent b6225f1920
commit abc704a363
6 files changed
+160 -8

No files matched your search

+20
View File
@@ -484,6 +484,25 @@ async function reconcileSelectedStoreFromMensuelApi(
}
}
/**
* Extrait la série « nombre de magasins vendeurs » du détail mensuel complet.
*
* `metricsByMonth` porte les cinq mesures Qlik du mois ; seule `nbMag` sert à la
* deuxième courbe de la carte tendance. Renvoie `null` plutôt qu'un objet vide
* pour que l'UI distingue « pas de donnée » de « zéro magasin ».
*/
function nbMagParMois(
metricsByMonth: Record<string, { nbMag?: number }> | null | undefined,
): Record<string, number> | null {
if (!metricsByMonth) return null;
const parMois: Record<string, number> = {};
for (const [mois, mesures] of Object.entries(metricsByMonth)) {
const nb = Number(mesures?.nbMag);
if (Number.isFinite(nb)) parMois[mois] = nb;
}
return Object.keys(parMois).length > 0 ? parMois : null;
}
/**
* Enrichit les produits avec les metriques reseau Qlik (cache qlik_network_metrics),
* jointes par code centrale. Degradation propre si la sync Qlik n'a jamais tourne
@@ -514,6 +533,7 @@ async function enrichWithNetworkMetrics(productMap: Map<string, ProductRow>): Pr
product.margePctReseau = m.margePctReseau;
product.tauxPresenceReseau = m.nbMagasinsReseau / NB_MAGASINS_RESEAU;
product.qteReseauByMonth = m.qteByMonth ?? null;
product.nbMagReseauByMonth = nbMagParMois(m.metricsByMonth);
product.networkFetchedAt = m.fetchedAt ?? undefined;
}
}