From 6d0ba26e071e2534fdeed3a5445f01bec0d6e7e6 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Tue, 8 Sep 2026 11:00:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(=C3=A9nergie):=20le=20graphe=20de=20flux?= =?UTF-8?q?=20EMPILE=20au=20lieu=20de=20superposer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validé par Patrick. Sa proposition — production en jaune, autoconso en orange, et des traits pleins — partait d'un bon constat, mais la couleur ne pouvait pas régler le problème : superposées, autoconsommation et production sont égales au watt près dès que rien n'est exporté, et deux traits pleins superposés n'en laissent voir qu'un, quelles que soient leurs teintes. Le défaut est géométrique, pas chromatique — et le pointillé posé avant n'était qu'un contournement. Empilées, le recouvrement devient impossible : jaune PV -> réseau (exporté) ┐ orange PV -> maison (autoconsommé) ┘ sommet = production totale La production cesse d'être une courbe : elle EST le haut de la pile. Quand rien n'est exporté la bande jaune a une hauteur nulle — vrai, et lisible d'un coup d'œil. L'export devient une SURFACE au lieu d'un écart à mesurer à l'œil entre deux traits. Vérifié à l'écran : l'orange porte le fond de la journée, le jaune ne sort que sur l'épisode d'export de fin d'après-midi. Deux détails qui vont avec : - `_aire` ne courbe pas (`isCurved: false`) — une spline peut passer sous zéro entre deux points, et une bande d'énergie sous l'axe affiche une grandeur qui n'existe pas ; - les lignes n'ont plus de remplissage par défaut : un voile à 7 % recouvrait les bandes et faussait leurs teintes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw --- lib/screens/energy_screen.dart | 83 +++++++++++++++++++++++----------- lib/theme/etm_tokens.dart | 11 +++++ 2 files changed, 67 insertions(+), 27 deletions(-) diff --git a/lib/screens/energy_screen.dart b/lib/screens/energy_screen.dart index 9166aa2..43b4477 100644 --- a/lib/screens/energy_screen.dart +++ b/lib/screens/energy_screen.dart @@ -288,12 +288,12 @@ class _EnergyScreenState extends State { ? 'Puissances · SOC %' : 'Puissances', subtitle: _aUneBatterie - ? 'kW (gauche) · % batterie (droite)' - : 'kW', + ? 'kW — le sommet de la pile est la production · % batterie (droite)' + : 'kW — le sommet de la pile est la production', legend: [ - _LegendDot(EtmTokens.amber, 'Production'), + _LegendDot(EtmTokens.selfUse, 'Autoconsommé'), + _LegendDot(EtmTokens.amber, 'Exporté'), _LegendDot(EtmTokens.blue, 'Consommation'), - _LegendDot(EtmTokens.green, 'Autoconso', dashed: true), if (_aUneBatterie) _LegendDot(EtmTokens.ev, 'SOC %', dashed: true), ], @@ -372,11 +372,15 @@ class _EnergyScreenState extends State { // n'est pas vrai du réseau, où le signe DIT le sens — d'où `gridImportW` / // `gridExportW` plutôt qu'un `.abs()` là-bas. final prodW = d.productionW.abs(); - prodSpots .add(FlSpot(x, prodW / 1000)); - consoSpots.add(FlSpot(x, d.consumptionW.abs() / 1000)); // INTERIM (§2.2) — bande autoconso via la formule canonique du seam. - autoSpots .add(FlSpot( - x, EnergyRatiosInterim.selfConsumptionPower(prodW, d.acquisitionW) / 1000)); + final selfW = EnergyRatiosInterim.selfConsumptionPower(prodW, d.acquisitionW); + // `prodSpots` porte désormais le SOMMET DE LA PILE, c'est-à-dire la production : + // autoconsommé + exporté. L'égalité est exacte par construction — l'exporté est + // défini comme `production − autoconsommé` — donc la pile ne peut pas mentir sur + // son total. + prodSpots .add(FlSpot(x, prodW / 1000)); + autoSpots .add(FlSpot(x, selfW / 1000)); + consoSpots.add(FlSpot(x, d.consumptionW.abs() / 1000)); } // Plage Y gauche en kW @@ -478,27 +482,35 @@ class _EnergyScreenState extends State { ), ), ), - // ⚠️ **L'autoconsommation est en POINTILLÉ, et ce n'est pas décoratif.** + // ⚠️ **On EMPILE, on ne superpose pas.** Validé par Patrick le 2026-09-08. // - // Tant que rien n'est exporté, autoconsommation == production **exactement** : - // tracée pleine et par-dessus, elle recouvrait la production au pixel près. Vu à - // l'écran le 2026-09-08, juste après avoir corrigé le signe — la courbe ambre - // n'apparaissait qu'aux rares moments d'export, c'est-à-dire là où les deux - // diffèrent. Le signe était réparé et la production toujours invisible, pour une - // deuxième raison qui n'avait rien à voir avec la première. + // Superposées, autoconsommation et production sont identiques au watt près dès + // que rien n'est exporté — et deux traits pleins superposés n'en laissent voir + // qu'un, quelles que soient leurs couleurs. Le problème est géométrique, pas + // chromatique : changer de teinte ne l'aurait pas réglé, et le pointillé essayé + // avant n'était qu'un contournement. // - // En pointillé elle se lit COMME une superposition : sur la bande ambre, des - // tirets verts disent « tout ce qui est produit est consommé sur place », et leur - // décrochage dit l'export. Deux courbes qui coïncident doivent pouvoir se voir - // coïncider — sinon l'une des deux ment par absence. + // Empilées, il n'y a plus de recouvrement possible : + // + // jaune PV → réseau (exporté) ┐ + // orange PV → maison (autoconsommé) ┘ sommet = production totale + // + // La production cesse d'être une courbe : elle EST le haut de la pile. Et quand + // rien n'est exporté, la bande jaune a une hauteur nulle — ce qui est vrai, et se + // lit d'un coup d'œil : « tout ce qui est produit est consommé sur place ». + // L'export devient une SURFACE au lieu d'un écart à mesurer à l'œil entre deux + // traits. + // + // Tracé du haut vers le bas : chaque aire est remplie jusqu'à zéro et la suivante + // repeint par-dessus. Même procédé que l'empilement par consommateur. lineBarsData: [ - _line(prodSpots, EtmTokens.amber), + _aire(prodSpots, EtmTokens.amber), // sommet = production + _aire(autoSpots, EtmTokens.selfUse), // repeint la part autoconsommée _line(consoSpots, EtmTokens.blue), - _line(autoSpots, EtmTokens.green, dashed: true, width: 1.8), - // Le SOC passe en indigo : il était vert pointillé, comme l'autoconsommation - // ci-dessus. Sur une installation AVEC batterie les deux auraient été - // indiscernables — collision créée à l'instant où l'autoconso est passée en - // pointillé, et qu'aucun banc sans batterie n'aurait montrée. + // Le SOC est en indigo, pas en vert pointillé : le vert pointillé a servi à + // l'autoconsommation le temps d'un essai, et sur une installation AVEC batterie + // les deux auraient été indiscernables. Aucun banc sans batterie ne pouvait le + // montrer. if (hasSoc) _line(socSpots, EtmTokens.ev, dashed: true, width: 1.5), ], lineTouchData: LineTouchData( @@ -520,8 +532,22 @@ class _EnergyScreenState extends State { ); } + /// Une bande PLEINE de l'empilement — remplie jusqu'à zéro, sans courbure. + /// + /// Pas de `isCurved` ici : une spline peut passer sous zéro entre deux points, et une + /// bande d'énergie qui plonge sous l'axe affiche une grandeur qui n'existe pas. + LineChartBarData _aire(List spots, Color color) => LineChartBarData( + spots: spots, + isCurved: false, + color: color, + barWidth: 1.2, + dotData: const FlDotData(show: false), + belowBarData: + BarAreaData(show: true, color: color.withValues(alpha: 0.62)), + ); + LineChartBarData _line(List spots, Color color, - {bool dashed = false, double width = 2.0}) => + {bool dashed = false, double width = 2.0, bool fill = false}) => LineChartBarData( spots: spots, isCurved: true, @@ -530,8 +556,11 @@ class _EnergyScreenState extends State { barWidth: width, dotData: const FlDotData(show: false), dashArray: dashed ? [4, 5] : null, + // Aucun remplissage par défaut depuis l'empilement : un voile, même à 7 %, + // recouvre les bandes et fausse leurs teintes. La consommation est une LIGNE + // au-dessus de la pile, pas une surface de plus. belowBarData: BarAreaData( - show: !dashed, + show: fill && !dashed, color: color.withValues(alpha: 0.07), ), ); diff --git a/lib/theme/etm_tokens.dart b/lib/theme/etm_tokens.dart index 4fd000a..1a63fa1 100644 --- a/lib/theme/etm_tokens.dart +++ b/lib/theme/etm_tokens.dart @@ -93,6 +93,17 @@ class EtmTokens { static const Color ev = Color(0xFF7C6CE4); // bornes — indigo static const Color evDark = Color(0xFF8E80EA); + /// **L'autoconsommé**, dans l'empilement du graphe de flux — orange, sous le jaune de + /// l'exporté dont le sommet donne la production totale. + /// + /// ⚠️ Voisine de [pac] (ambre-orange), et c'est assumé : les deux n'apparaissent jamais + /// dans le même graphe — celle-ci décompose la PRODUCTION, celle-là un CONSOMMATEUR. Si + /// la proximité gêne à l'écran sur la page complète, c'est [pac] qui bougera : la paire + /// jaune/orange a été choisie par Patrick pour ce graphe-ci. + static const Color selfUse = Color(0xFFF4661B); + static const Color selfUseDark = Color(0xFFFA7A34); + static Color selfUseOf(BuildContext c) => _isDark(c) ? selfUseDark : selfUse; + static Color pacOf(BuildContext c) => _isDark(c) ? pacDark : pac; static Color ecsOf(BuildContext c) => _isDark(c) ? ecsDark : ecs; static Color evOf(BuildContext c) => _isDark(c) ? evDark : ev;