diff --git a/docs/captures/2026-09-09_energie-echelle-fenetre.png b/docs/captures/2026-09-09_energie-echelle-fenetre.png new file mode 100644 index 0000000..a5726e5 Binary files /dev/null and b/docs/captures/2026-09-09_energie-echelle-fenetre.png differ diff --git a/docs/captures/2026-09-09_energie-plage-sous-legende.png b/docs/captures/2026-09-09_energie-plage-sous-legende.png new file mode 100644 index 0000000..726068b Binary files /dev/null and b/docs/captures/2026-09-09_energie-plage-sous-legende.png differ diff --git a/lib/screens/energy_screen.dart b/lib/screens/energy_screen.dart index ac912b6..1aca96f 100644 --- a/lib/screens/energy_screen.dart +++ b/lib/screens/energy_screen.dart @@ -489,6 +489,9 @@ class _EnergyScreenState extends State { height: 220, loading: _loading, noData: _noData, + // Sur les DEUX cartes : elles partagent le cadrage, mais si on + // défile jusqu'à ne voir que la seconde, elle doit se suffire. + sousLegende: _bandeauPlage(context), child: _glissant(_buildLineChart()), ), // La note d'absence de batterie va SOUS ce graphe-ci : c'est ici que @@ -517,15 +520,11 @@ class _EnergyScreenState extends State { height: 220, loading: _loading, noData: _noData, + sousLegende: _bandeauPlage(context), child: _glissant(_buildConsumerChart()), ), // L'indice ne sort que si le geste existe : une aide permanente // cesse d'être lue, et celle-ci serait fausse sur les autres vues. - if (_glissable && _plageVisible() != null) - _NoteEcran( - icon: Icons.date_range_rounded, - texte: _plageVisible()!, - ), if (_glissable) const _NoteEcran( icon: Icons.swipe_rounded, @@ -572,6 +571,22 @@ class _EnergyScreenState extends State { : '${jour(a)} ${heure(a)} → ${jour(b)} ${heure(b)}'; } + /// La plage visible, prête à être posée sous une légende. `null` quand il n'y a rien à + /// qualifier — une vue non glissable montre tout, sa plage est celle de l'onglet. + Widget? _bandeauPlage(BuildContext context) { + final t = _plageVisible(); + if (t == null) return null; + return Row(children: [ + Icon(Icons.date_range_rounded, size: 13, color: EtmTokens.mutedOf(context)), + const SizedBox(width: 6), + Text(t, + style: EtmTokens.mono( + size: 11.5, + weight: FontWeight.w600, + color: EtmTokens.mutedOf(context))), + ]); + } + /// Rend un graphe **glissable à l'horizontale** quand la vue s'y prête. /// /// Le geste convertit des pixels en points d'échantillon : glisser d'une largeur d'écran @@ -631,7 +646,19 @@ class _EnergyScreenState extends State { } // Plage Y gauche en kW + // 🔴 **L'échelle suit ce qui est VISIBLE, pas tout le jeu chargé.** + // + // Calculée sur les 72 h, une pointe d'avant-hier écrase le détail d'aujourd'hui : vu à + // l'écran le 2026-09-09, un axe à 11,9 kW pour une fenêtre qui ne dépasse pas 2,5 — + // les courbes rampaient au ras de l'abscisse. Le glissement servait alors à déplacer + // une loupe sans grossissement. + // + // C'est le passage à 72 h qui l'a créé : sur 24 h, fenêtre et jeu de données avaient + // presque la même amplitude, et le défaut ne pouvait pas se voir. + final iDebut = _fenetreDebut.floor().clamp(0, _data.length - 1); + final iFin = _fenetreFin.ceil().clamp(0, _data.length - 1); final allKw = _data + .sublist(iDebut, iFin + 1) .expand((d) => [d.productionW.abs() / 1000, d.consumptionW.abs() / 1000]) .toList(); final maxKw = allKw.isEmpty ? 2.5 : allKw.reduce(max); @@ -1257,9 +1284,16 @@ class _EnergyScreenState extends State { cumuls.add([for (int i = 0; i < n; i++) prec[i] + bandes[b].valeurs[i]]); } + // Même règle que le graphe de flux : l'échelle suit la fenêtre, sinon les deux + // graphes ne se comparent plus — l'un cadré sur ce qu'on voit, l'autre sur tout. + final iDebut = _fenetreDebut.floor().clamp(0, n - 1); + final iFin = _fenetreFin.ceil().clamp(0, n - 1); final maxKw = cumuls.isEmpty ? 1.0 - : cumuls.last.map((v) => v / 1000).fold(0.5, max); + : cumuls.last + .sublist(iDebut, iFin + 1) + .map((v) => v / 1000) + .fold(0.5, max); final yMax = max(maxKw * 1.15, 0.5); final labelStyle = EtmTokens.sans(size: 9, color: EtmTokens.mutedOf(context)); @@ -1727,6 +1761,14 @@ class _EtmChartCard extends StatelessWidget { final bool noData; final Widget child; + /// Ce qui s'écrit **entre la légende et le tracé** — la plage visible, par exemple. + /// + /// Sa place n'est pas cosmétique : la date qualifie ce qu'on regarde, elle doit donc être + /// lue AVANT le graphe, pas trouvée après lui. Posée sous les deux cartes, elle arrivait + /// une fois la lecture faite — et sur trois jours, une courbe lue sans sa date est une + /// courbe lue de travers. + final Widget? sousLegende; + const _EtmChartCard({ required this.title, required this.subtitle, @@ -1735,6 +1777,7 @@ class _EtmChartCard extends StatelessWidget { required this.loading, required this.noData, required this.child, + this.sousLegende, }); @override @@ -1765,6 +1808,10 @@ class _EtmChartCard extends StatelessWidget { ), const SizedBox(height: 10), Wrap(spacing: 14, runSpacing: 4, children: legend), + if (sousLegende != null) ...[ + const SizedBox(height: 8), + sousLegende!, + ], const SizedBox(height: 14), SizedBox( height: height,