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

+18 -2
View File
@@ -9,7 +9,7 @@ import {
} from "lucide-react";
import { HeatmapCell } from "@/features/grid/components/heatmap-cell";
import { NetworkLineChart, DualLineChart, TrendSparkline } from "@/features/grid/components/network-charts";
import { computeNetworkTrend, trendLabel, TREND_COLOR, NB_MAGASINS_RESEAU } from "@/features/grid/lib/network-trend";
import { computeNetworkTrend, computeStoresSeries, trendLabel, TREND_COLOR, NB_MAGASINS_RESEAU } from "@/features/grid/lib/network-trend";
import { formatMonthLabel, formatDate, SITE_LABELS, ffMonthToQlik } from "@/features/grid/lib/months";
import { useQlikSyncJob } from "@/features/qlik-sync/use-qlik-sync-job";
import { computeComparatif, indiceVerdict, normalizeMargePct } from "@/features/produits/lib/compare-reseau";
@@ -149,6 +149,17 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba
const totauxAffiches = magasin === "TOTAL" ? fiche.totaux : (fiche.totauxParSite[magasin] ?? { qte: 0, ca: 0, marge: 0, tauxMarge: 0 });
const trend = useMemo(() => computeNetworkTrend(reseau?.qteByMonth ?? null), [reseau]);
/** Nombre de magasins vendeurs par mois — deuxième courbe de la carte tendance. */
const magasinsParMois = useMemo(() => {
const detailMensuel = reseau?.metricsByMonth;
if (!detailMensuel) return null;
const parMois: Record<string, number> = {};
for (const [mois, mesures] of Object.entries(detailMensuel)) {
const nb = Number(mesures?.nbMag);
if (Number.isFinite(nb)) parMois[mois] = nb;
}
return computeStoresSeries(parMois);
}, [reseau]);
const comparatif = useMemo(() => computeComparatif(fiche), [fiche]);
const verdict = indiceVerdict(comparatif.indiceQte);
@@ -280,7 +291,12 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba
<div className="text-[10px] font-semibold uppercase tracking-wide mb-1" style={{ color: "var(--text-muted)" }}>
Quantités vendues réseau, mois par mois
</div>
<NetworkLineChart labels={trend.labels} values={trend.values} color={TREND_COLOR[trend.direction]} />
<NetworkLineChart
labels={trend.labels}
values={trend.values}
color={TREND_COLOR[trend.direction]}
stores={magasinsParMois?.values}
/>
</div>
)}