mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
Carte tendance : garantir que la courbe magasins se distingue des quantites
La courbe des quantites prend la couleur de la TENDANCE : vert, rouge, ou gris-bleu (#94a3b8) quand elle est stable. L'indigo des magasins se detache du vert et du rouge, mais il est trop proche du gris-bleu — or « stable » est le cas le plus frequent. storesColorFor() bascule sur l'ambre (#f59e0b) dans ce cas precis : gris froid contre orange chaud, impossible a confondre. Les deux series se distinguent desormais par TROIS canaux, pas seulement la couleur — qui ne suffit ni en impression noir et blanc, ni pour un daltonien : - teinte garantie differente, - trait pointille contre trait plein, - points evides contre points pleins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
1 file changed
+37
-11
@@ -43,14 +43,25 @@ export function TrendSparkline({ trend }: { trend: NetworkTrend }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Couleur par défaut de la courbe « magasins vendeurs » : indigo franc. */
|
||||||
* Couleur de la courbe « magasins vendeurs ».
|
|
||||||
*
|
|
||||||
* Indigo choisi pour ne jamais entrer en collision avec les couleurs de
|
|
||||||
* tendance (vert / rouge / gris) portées par la courbe des quantités, et pour
|
|
||||||
* rester lisible sur fond clair comme sur fond sombre.
|
|
||||||
*/
|
|
||||||
export const STORES_COLOR = "#6366f1";
|
export const STORES_COLOR = "#6366f1";
|
||||||
|
/** Repli chaud, quand la courbe des quantités est elle-même dans les tons froids. */
|
||||||
|
export const STORES_COLOR_ALT = "#f59e0b";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Couleur de la courbe « magasins » garantie DISTINCTE de celle des quantités.
|
||||||
|
*
|
||||||
|
* La courbe des quantités prend la couleur de la tendance : vert (#22c55e),
|
||||||
|
* rouge (#ef4444) ou gris-bleu (#94a3b8) quand elle est stable. L'indigo se
|
||||||
|
* détache nettement du vert et du rouge, mais il est trop proche du gris-bleu —
|
||||||
|
* or « stable » est le cas le plus fréquent. Dans ce cas précis on bascule sur
|
||||||
|
* l'ambre : gris froid contre orange chaud, impossible à confondre.
|
||||||
|
*/
|
||||||
|
export function storesColorFor(couleurQuantites: string): string {
|
||||||
|
return couleurQuantites.toLowerCase() === TREND_COLOR.flat.toLowerCase()
|
||||||
|
? STORES_COLOR_ALT
|
||||||
|
: STORES_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ventes réseau mensuelles, avec en option la courbe du nombre de magasins.
|
* Ventes réseau mensuelles, avec en option la courbe du nombre de magasins.
|
||||||
@@ -61,6 +72,11 @@ export const STORES_COLOR = "#6366f1";
|
|||||||
* normalisée sur son propre maximum, rappelé en haut du graphique — c'est la
|
* normalisée sur son propre maximum, rappelé en haut du graphique — c'est la
|
||||||
* FORME des deux courbes qu'on compare, pas leurs hauteurs.
|
* FORME des deux courbes qu'on compare, pas leurs hauteurs.
|
||||||
*
|
*
|
||||||
|
* Les deux séries se distinguent par TROIS canaux, et pas seulement la couleur
|
||||||
|
* (qui ne suffit ni en impression noir et blanc, ni pour un daltonien) : teinte
|
||||||
|
* garantie différente (`storesColorFor`), trait pointillé contre trait plein, et
|
||||||
|
* points évidés contre points pleins.
|
||||||
|
*
|
||||||
* Lecture : quantités qui montent avec les magasins = élargissement de la
|
* Lecture : quantités qui montent avec les magasins = élargissement de la
|
||||||
* diffusion ; quantités qui montent à magasins constants = vraie accélération
|
* diffusion ; quantités qui montent à magasins constants = vraie accélération
|
||||||
* des ventes ; quantités qui baissent alors que les magasins tiennent = essoufflement.
|
* des ventes ; quantités qui baissent alors que les magasins tiennent = essoufflement.
|
||||||
@@ -89,6 +105,7 @@ export function NetworkLineChart({
|
|||||||
const yS = (v: number) => padT + plotH - (v / maxS) * plotH;
|
const yS = (v: number) => padT + plotH - (v / maxS) * plotH;
|
||||||
const linePts = values.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
const linePts = values.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
|
||||||
const areaPts = `${padL},${padT + plotH} ${linePts} ${(padL + plotW).toFixed(1)},${(padT + plotH).toFixed(1)}`;
|
const areaPts = `${padL},${padT + plotH} ${linePts} ${(padL + plotW).toFixed(1)},${(padT + plotH).toFixed(1)}`;
|
||||||
|
const storesColor = storesColorFor(color);
|
||||||
const storePts = avecMagasins
|
const storePts = avecMagasins
|
||||||
? stores!.map((v, i) => `${x(i).toFixed(1)},${yS(v).toFixed(1)}`).join(" ")
|
? stores!.map((v, i) => `${x(i).toFixed(1)},${yS(v).toFixed(1)}`).join(" ")
|
||||||
: "";
|
: "";
|
||||||
@@ -101,7 +118,7 @@ export function NetworkLineChart({
|
|||||||
Quantités vendues <span className="tabular-nums" style={{ color: "var(--text-muted)" }}>(max {maxV.toLocaleString("fr-FR")})</span>
|
Quantités vendues <span className="tabular-nums" style={{ color: "var(--text-muted)" }}>(max {maxV.toLocaleString("fr-FR")})</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1.5 text-[11px]" style={{ color: "var(--text-secondary)" }}>
|
<span className="flex items-center gap-1.5 text-[11px]" style={{ color: "var(--text-secondary)" }}>
|
||||||
<span className="inline-block w-3 h-[3px] rounded-full" style={{ background: STORES_COLOR }} />
|
<span className="inline-block w-3 h-[3px] rounded-full" style={{ background: storesColor }} />
|
||||||
Magasins vendeurs <span className="tabular-nums" style={{ color: "var(--text-muted)" }}>(max {maxS.toLocaleString("fr-FR")})</span>
|
Magasins vendeurs <span className="tabular-nums" style={{ color: "var(--text-muted)" }}>(max {maxS.toLocaleString("fr-FR")})</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,15 +139,24 @@ export function NetworkLineChart({
|
|||||||
<polyline
|
<polyline
|
||||||
points={storePts}
|
points={storePts}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={STORES_COLOR}
|
stroke={storesColor}
|
||||||
strokeWidth={1.75}
|
strokeWidth={1.75}
|
||||||
strokeDasharray="4 3"
|
strokeDasharray="4 3"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/* points évidés : distincts des points pleins des quantités, même sans couleur */}
|
||||||
{avecMagasins && stores!.map((v, i) => (
|
{avecMagasins && stores!.map((v, i) => (
|
||||||
<circle key={`mag-${labels[i]}`} cx={x(i)} cy={yS(v)} r={2.2} fill={STORES_COLOR} />
|
<circle
|
||||||
|
key={`mag-${labels[i]}`}
|
||||||
|
cx={x(i)}
|
||||||
|
cy={yS(v)}
|
||||||
|
r={2.4}
|
||||||
|
fill="var(--bg-surface)"
|
||||||
|
stroke={storesColor}
|
||||||
|
strokeWidth={1.4}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
{/* courbe */}
|
{/* courbe */}
|
||||||
{n > 1 && <polyline points={linePts} fill="none" stroke={color} strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" />}
|
{n > 1 && <polyline points={linePts} fill="none" stroke={color} strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" />}
|
||||||
@@ -147,7 +173,7 @@ export function NetworkLineChart({
|
|||||||
</text>
|
</text>
|
||||||
{/* nb de magasins sous l'axe, juste au-dessus du mois */}
|
{/* nb de magasins sous l'axe, juste au-dessus du mois */}
|
||||||
{avecMagasins && (
|
{avecMagasins && (
|
||||||
<text x={x(i)} y={H - 20} textAnchor="middle" fontSize={8} fontWeight={600} fill={STORES_COLOR}>
|
<text x={x(i)} y={H - 20} textAnchor="middle" fontSize={8} fontWeight={600} fill={storesColor}>
|
||||||
{Math.round(stores![i]).toLocaleString("fr-FR")}
|
{Math.round(stores![i]).toLocaleString("fr-FR")}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user