feat: coloration des nœuds par balance nette (orange émetteur / vert récepteur)
ci/woodpecker/push/woodpecker Pipeline was successful

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-24 00:55:08 +01:00
parent 136571ed53
commit 78b4762c88
2 changed files with 41 additions and 4 deletions
+34 -3
View File
@@ -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)}
+6
View File
@@ -247,7 +247,13 @@ export function StatsPanel({ stats, loading, periodDays, source, currentUD, anim
{/* Balance nette */}
{flowStats.netBalance.length > 0 && (
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-xl p-3 space-y-1.5">
<div className="flex items-center justify-between">
<p className="text-[#4b5563] text-xs uppercase tracking-widest">Balance nette</p>
<p className="text-[10px] text-[#4b5563] flex items-center gap-1.5">
<span style={{ color: '#ff6d00' }}></span>émetteur
<span style={{ color: '#00c853' }}></span>récepteur
</p>
</div>
{flowStats.netBalance.map((c) => (
<div key={c.city} className="flex items-center justify-between">
<span className="text-white text-xs truncate">{c.city}</span>