fix(énergie): la date sous la légende, et l'échelle qui suit la FENÊTRE

LA DATE PASSE ENTRE LA LÉGENDE ET LE TRACÉ (demande de Patrick). Sa place n'est
pas cosmétique : elle QUALIFIE ce qu'on regarde, donc elle doit se lire avant le
graphe, pas se trouver après lui. 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.

Posée sur les DEUX cartes : elles partagent le cadrage, mais si on défile
jusqu'à ne voir que la seconde, elle doit se suffire. Une carte qui dépend d'une
autre pour être comprise ne tient pas dès qu'on la regarde seule.

🔴 ET L'ÉCHELLE SUIT DÉSORMAIS LA FENÊTRE, PAS TOUT LE JEU CHARGÉ.

Vu à l'écran : axe à 11,9 kW pour une fenêtre qui ne dépasse pas 2,5 — les
courbes rampaient au ras de l'abscisse, et la production était purement
invisible. Une pointe d'avant-hier écrasait le détail d'aujourd'hui.

C'est le passage à 72 h qui l'a créé, et c'est lui qui l'a rendu visible : sur
24 h, fenêtre et jeu de données avaient presque la même amplitude, donc le
défaut ne pouvait pas se voir. Élargir le glissement sans rééchelonner faisait
une loupe qui déplace le cadre sans grossir — exactement ce qu'il devait éviter.

Corrigé sur les deux graphes : cadrer l'un sur la fenêtre et l'autre sur tout
les rendrait incomparables.

Captures : docs/captures/2026-09-09_energie-plage-sous-legende.png
           docs/captures/2026-09-09_energie-echelle-fenetre.png

358 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
This commit is contained in:
Patrick Schurig 2026-09-09 09:49:06 +02:00
parent 2a1e7f2dbb
commit 9a034aecbc
3 changed files with 53 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

View File

@ -489,6 +489,9 @@ class _EnergyScreenState extends State<EnergyScreen> {
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<EnergyScreen> {
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<EnergyScreen> {
: '${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<EnergyScreen> {
}
// 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<EnergyScreen> {
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<double>(0.5, max);
: cumuls.last
.sublist(iDebut, iFin + 1)
.map((v) => v / 1000)
.fold<double>(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,