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;