From 2a1e7f2dbb23859918fec26c3064259eb1200c4a Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Wed, 9 Sep 2026 09:20:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(=C3=A9nergie):=20glisser=20sur=20trois=20j?= =?UTF-8?q?ours=20=E2=80=94=20fen=C3=AAtre=20fixe=20de=2012=20h,=20et=20la?= =?UTF-8?q?=20date=20avec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demande de Patrick : élargir le glissement. 72 h chargées au quart d'heure, la fenêtre en montre 12 — on remonte donc sur trois jours sans changer d'onglet. TROIS CHOSES QUE CE CHANGEMENT RENDAIT NÉCESSAIRES. La taille de fenêtre était « la moitié du jeu de données ». Avec 24 h ça donnait 12 h ; avec 72 h ça en aurait donné 36, et la fenêtre aurait grandi avec les données au lieu de rester une fenêtre. Elle est fixée en DURÉE : la densité de lecture ne dépend plus de la profondeur chargée. La fenêtre s'ouvre sur le plus RÉCENT. Calée au début, elle aurait montré avant-hier soir à l'ouverture — la seule vue dont personne n'a besoin en premier. Et la plage visible porte sa DATE : « 8/9 21:00 → 9/9 09:00 ». Sur 24 h, « 09:00 » désignait sans ambiguïté ce matin ; sur trois jours, c'est l'un de trois matins, et l'axe n'affiche que des heures. Sans cette ligne, l'écran répondait à côté de la question « c'était quand ? ». Et les étiquettes des deux BORDS de l'axe sont retirées : fl_chart en pose une à `min` et à `max` en plus de celles de l'intervalle, et elles se superposaient à leur voisine — « 20:4500 » à gauche, « 08:1509:00 » à droite. J'avais corrigé le bord gauche sans penser que le droit avait le même défaut : la recherche auto-confirmante, appliquée à un axe. On ne perd rien, la ligne de plage donne les deux bornes exactes avec leur jour. Vérifié à l'écran : fenêtre 21:15 → 09:00 à l'ouverture, ligne de plage et indice de geste présents. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw --- lib/screens/energy_screen.dart | 74 +++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/lib/screens/energy_screen.dart b/lib/screens/energy_screen.dart index ec97a43..ac912b6 100644 --- a/lib/screens/energy_screen.dart +++ b/lib/screens/energy_screen.dart @@ -79,7 +79,9 @@ class _EnergyScreenState extends State { /// et personne ne pouvait obtenir la journée que le premier promettait. Corrigé le /// 2026-09-09 ; l'onglet « Année » manquait, puisque c'est « Mois » qui la montrait. static const _tabs = [ - _Tab('Heures', Duration(hours: 24), 'SampleRate15Mins', showTime: true), + // 72 h au quart d'heure : la fenêtre n'en montre que 12, mais on peut remonter sur + // trois jours sans changer d'onglet (demande de Patrick, 2026-09-09). + _Tab('Heures', Duration(hours: 72), 'SampleRate15Mins', showTime: true), _Tab('Jour', Duration(days: 1), 'SampleRate1Hour', showTime: true, rendu: _Rendu.barres, journeeCivile: true), _Tab('Semaine', Duration(days: 7), 'SampleRate1Day', @@ -110,10 +112,21 @@ class _EnergyScreenState extends State { /// heures. Les séries restent entières, la fenêtre ne fait que cadrer. double _fenetreDebut = 0; - /// Nombre de points visibles à la fois. `null` = tout, donc rien à glisser. + /// **Douze heures visibles à la fois**, quelle que soit la profondeur des données. + /// + /// C'était « la moitié du jeu » : avec 24 h de données, ça donnait 12 h — mais en passant + /// à 72 h ça en aurait donné 36, et la fenêtre aurait grandi avec les données au lieu de + /// rester une fenêtre. Une durée fixe garde la même densité de lecture quel que soit + /// l'historique chargé. + static const Duration _fenetreDuree = Duration(hours: 12); + int? get _fenetreTaille { - if (!_tabs[_tabIdx].showTime) return null; // seule la vue Heures glisse - return _data.length > 40 ? _data.length ~/ 2 : null; + final tab = _tabs[_tabIdx]; + if (tab.rendu != _Rendu.courbes) return null; // seules les courbes glissent + final pas = _pasEchantillon(tab).inSeconds; + if (pas <= 0) return null; + final points = _fenetreDuree.inSeconds ~/ pas; + return _data.length > points + 1 ? points : null; } bool get _glissable => _fenetreTaille != null; @@ -264,13 +277,17 @@ class _EnergyScreenState extends State { ]); if (!mounted) return; setState(() { - // La fenêtre repart du début à chaque lecture : la garder ferait cadrer une plage - // d'index qui ne désigne plus les mêmes heures. - _fenetreDebut = 0; _data = results[0] as List; _socData = results[1] as List; _conso = results[2] as ConsumerHistory; _bilan = results[3] as DailyBalance?; + // **La fenêtre s'ouvre sur le plus RÉCENT.** Avec 72 h chargées, la caler au début + // montrerait avant-hier soir à l'ouverture de l'écran — la seule vue dont personne + // n'a besoin en premier. Recalculée à chaque lecture : garder l'ancienne cadrerait + // une plage d'index qui ne désigne plus les mêmes heures. + final taille = _fenetreTaille; + _fenetreDebut = + taille == null ? 0 : max(0, _data.length - 1 - taille).toDouble(); _loading = false; _noData = _data.isEmpty; }); @@ -504,11 +521,16 @@ class _EnergyScreenState extends State { ), // 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, - texte: 'Glissez horizontalement sur un graphe pour vous déplacer ' - 'dans la journée. Les deux gardent le même cadrage.', + texte: 'Glissez horizontalement pour remonter sur trois jours. ' + 'Les deux graphes gardent le même cadrage.', ), ], // Les notes parlent des séries par consommateur : elles n'ont aucun @@ -532,6 +554,24 @@ class _EnergyScreenState extends State { // ── ① Line chart : puissances + SOC ──────────────────────────────────────── + /// **Ce qu'on regarde, avec sa date** — `null` hors vue glissable. + /// + /// Sur 24 h, « 09:00 » désignait sans ambiguïté ce matin. Sur trois jours, la même + /// étiquette peut être l'un de trois matins, et l'axe seul ne le dit pas : il n'affiche + /// que des heures. La plage visible porte donc le jour, faute de quoi l'écran répond + /// exactement à côté de la question « c'était quand ? ». + String? _plageVisible() { + if (!_glissable || _data.isEmpty) return null; + final a = _data[_fenetreDebut.round().clamp(0, _data.length - 1)].timestamp; + final b = _data[_fenetreFin.round().clamp(0, _data.length - 1)].timestamp; + String jour(DateTime t) => '${t.day}/${t.month}'; + String heure(DateTime t) => + '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}'; + return jour(a) == jour(b) + ? '${jour(a)} · ${heure(a)} → ${heure(b)}' + : '${jour(a)} ${heure(a)} → ${jour(b)} ${heure(b)}'; + } + /// 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 @@ -681,11 +721,17 @@ class _EnergyScreenState extends State { reservedSize: 22, interval: xInterval, getTitlesWidget: (v, meta) { - // ⚠️ Pas d'étiquette sur le bord GAUCHE : fl_chart en pose une à `min` en - // plus de celles de l'intervalle, et les deux se superposent quand `min` - // n'est pas un multiple — « 20:4500 » à l'écran le 2026-09-09. Deux heures - // imprimées l'une sur l'autre ne se lisent ni l'une ni l'autre. - if (v <= meta.min) return const SizedBox.shrink(); + // ⚠️ **Aucune étiquette sur les deux BORDS.** fl_chart en pose une à `min` + // et à `max` en plus de celles de l'intervalle : quand les bornes ne + // tombent pas sur un multiple, elles se superposent à leur voisine — + // « 20:4500 » à gauche, « 08:1509:00 » à droite, vus à l'écran le + // 2026-09-09. Deux heures imprimées l'une sur l'autre ne se lisent ni + // l'une ni l'autre. + // + // On ne perd rien : la ligne de plage sous le graphe donne les deux bornes + // exactes, avec leur jour — ce que l'axe ne pouvait pas dire de toute + // façon sur trois jours. + if (v <= meta.min || v >= meta.max) return const SizedBox.shrink(); final idx = v.round(); if (idx < 0 || idx >= _data.length) return const SizedBox.shrink(); return SideTitleWidget(