From 78b4762c8889f052c50cf03b2e58aa2a09ccd572 Mon Sep 17 00:00:00 2001 From: syoul Date: Tue, 24 Mar 2026 00:55:08 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20coloration=20des=20n=C5=93uds=20par=20b?= =?UTF-8?q?alance=20nette=20(orange=20=C3=A9metteur=20/=20vert=20r=C3=A9ce?= =?UTF-8?q?pteur)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/components/FlowMap.tsx | 37 ++++++++++++++++++++++++++++++++--- src/components/StatsPanel.tsx | 8 +++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/components/FlowMap.tsx b/src/components/FlowMap.tsx index fb2bc21..d857193 100644 --- a/src/components/FlowMap.tsx +++ b/src/components/FlowMap.tsx @@ -1,5 +1,25 @@ import { useEffect, useRef, useState, useMemo } from 'react'; import L from 'leaflet'; + +/** Interpolation RGB linéaire entre deux couleurs hex, t ∈ [0, 1]. */ +function lerpColor(hex1: string, hex2: string, t: number): string { + const parse = (h: string) => [ + parseInt(h.slice(1, 3), 16), + parseInt(h.slice(3, 5), 16), + parseInt(h.slice(5, 7), 16), + ]; + const [r1, g1, b1] = parse(hex1); + const [r2, g2, b2] = parse(hex2); + const r = Math.round(r1 + (r2 - r1) * t); + const g = Math.round(g1 + (g2 - g1) * t); + const b = Math.round(b1 + (b2 - b1) * t); + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; +} + +const COLOR_NEUTRAL = '#d4a843'; // or Ğ1 +const COLOR_NEG = '#ff6d00'; // orange vif +const COLOR_POS = '#00c853'; // vert vif +const NEUTRAL_THRESHOLD = 0.05; // ±5 % → couleur neutre import type { TransactionArc } from '../data/arcData'; import { buildCorridors } from '../data/arcData'; @@ -83,6 +103,7 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { const maxVol = Math.max(...corridors.map(c => c.totalVolume), 1); const maxNodeVol = Math.max(...[...cityNodes.values()].map(c => c.emitted + c.received), 1); + const maxAbsNet = Math.max(...[...cityNodes.values()].map(d => Math.abs(d.received - d.emitted)), 1); // ---- Arcs ---- const arcElems = corridors.map((c, idx) => { @@ -139,7 +160,17 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { const p = proj(data.lat, data.lng); const vol = data.emitted + data.received; const r = Math.max(3, Math.min(14, 3 + 9 * (vol / maxNodeVol))); - return { name, p, r, isSelected: focusCity === name }; + + // Couleur selon balance nette normalisée + const net = data.received - data.emitted; + const t = net / maxAbsNet; // ∈ [-1, 1] + const fillColor = Math.abs(t) < NEUTRAL_THRESHOLD + ? COLOR_NEUTRAL + : t < 0 + ? lerpColor(COLOR_NEUTRAL, COLOR_NEG, -t) + : lerpColor(COLOR_NEUTRAL, COLOR_POS, t); + + return { name, p, r, fillColor, isSelected: focusCity === name }; }); return { arcElems, nodeElems }; @@ -194,8 +225,8 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) { cx={node.p.x} cy={node.p.y} r={node.r} - fill={node.isSelected ? '#ffffff' : '#d4a843'} - stroke={node.isSelected ? '#d4a843' : '#0a0b0f'} + fill={node.isSelected ? '#ffffff' : node.fillColor} + stroke={node.isSelected ? node.fillColor : '#0a0b0f'} strokeWidth={1.5} style={{ cursor: 'pointer' }} onClick={() => onCityClick(focusCity === node.name ? null : node.name)} diff --git a/src/components/StatsPanel.tsx b/src/components/StatsPanel.tsx index 5157c17..38e9148 100644 --- a/src/components/StatsPanel.tsx +++ b/src/components/StatsPanel.tsx @@ -247,7 +247,13 @@ export function StatsPanel({ stats, loading, periodDays, source, currentUD, anim {/* Balance nette */} {flowStats.netBalance.length > 0 && (
-

Balance nette

+
+

Balance nette

+

+ émetteur + récepteur +

+
{flowStats.netBalance.map((c) => (
{c.city}