From ab84f5e0ce8430f1860cf6ab611efbe029f27d96 Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sat, 5 Sep 2026 22:51:27 +0200 Subject: [PATCH] =?UTF-8?q?v1.2.9=20:=20zoom=20du=20graphique=20(pinch=20+?= =?UTF-8?q?=20boutons,=206=20h=20=E2=86=92=20300=20j,=20focal=20stable,=20?= =?UTF-8?q?pas=20adaptatif)=20+=20disclaimer=20IA=20dans=20le=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChartScreen : detectTransformGestures unique (pan 1 doigt + pinch 2 doigts, pas de conflit de consommation) ; zoom autour du point focal (le temps sous le doigt reste fixe) ; plage bornée 6 h → 300 j (MIN/MAX_RANGE_H) ; boutons − / + en accessibilité - CurveChart : stepForRange (15 min ≤ 12 h, 30 min ≤ 24 h, sinon 1 h) passé à computeCurve (courbes lisses à fort zoom) ; labels X adaptatifs 1 h/3 h - README : disclaimer IA en en-tête (développé avec l'IA, contribution humaine essentielle = feedback/retours/suggestions, renvoi §2 doc dev) ; DEVELOPPEMENT §1 mention IA-assistée, §11 zoom, §8 67 tests - ChartZoomTest : 3 tests stepForRange — 67 tests verts - versionCode 12, versionName 1.2.9 --- README.md | 14 ++++- app/build.gradle.kts | 4 +- .../hormonetrack/ui/components/CurveChart.kt | 14 +++++ .../hormonetrack/ui/screens/ChartScreen.kt | 60 ++++++++++++++++--- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../java/com/hormonetrack/ui/ChartZoomTest.kt | 31 ++++++++++ docs/CHANGELOG.md | 16 ++++- docs/DEVELOPPEMENT.md | 24 +++++++- 9 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt diff --git a/README.md b/README.md index 2948f59..85be40f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # HormoneTrack +> **🤖 Développé avec l'IA** : ce projet a été conçu et codé avec un assistant IA. +> La contribution humaine a été **essentielle** : feedback continu, retours +> utilisateur (tests sur vrai téléphone, rapports de bugs accompagnés d'exports +> réels), suggestions d'améliorations et validation de chaque release. Le +> détail session par session est documenté dans +> [docs/DEVELOPPEMENT.md §2](docs/DEVELOPPEMENT.md). + Suivi de thérapie hormonale (THS) sur Android, avec courbes estimées **heure par heure** d'estradiol (E2) et de testostérone (T), calibration sur les prises de sang, rappels affichés sur smartwatch (Huawei Watch GT 3 via Gadgetbridge ou Huawei Health) et @@ -9,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** > à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de > sang et aux consignes de ton endocrinologue. -- **Statut** : v1.2.8 — build Android ✅, **64 tests unitaires** ✅ (44 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ +- **Statut** : v1.2.9 — build Android ✅, **67 tests unitaires** ✅ (44 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ - **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md) - **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md) - **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md) @@ -18,8 +25,9 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** ## Fonctionnalités - **Courbes estimées heure par heure** : E2 (pg/mL) et T (ng/mL), vue 24 h / 7 j / 30 j, - **panoramique** (glisser pour remonter dans le passé), **pics & creux** affichables - (triangles ▲▼ aux extrema locaux, toggle) + **panoramique** (glisser pour remonter dans le passé), **zoom** (pinch ou boutons + − / +, 6 h → 300 j, échantillonnage adaptatif), **pics & creux** affichables avec + leurs **valeurs estimées** (triangles ▲▼ aux extrema locaux, toggle) - **Deux modèles PK au choix, superposables** (issus de la feuille `Estrogen.ods`) : **Estrannaise (EstraNase)** et **Transfem Science** pour les injections EV / EU / EEn, affichés côte à côte avec toggles indépendants diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bb20ea3..702bb58 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.hormonetrack" minSdk = 26 targetSdk = 36 - versionCode = 11 - versionName = "1.2.8" + versionCode = 12 + versionName = "1.2.9" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt index 615788b..9c546a9 100644 --- a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt +++ b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt @@ -66,6 +66,17 @@ fun niceCeil(value: Double): Double { return nice * exp } +/** + * Pas d'échantillonnage de la courbe selon la plage affichée (v1.2.9) : + * plus on zoome, plus le pas finit pour des courbes lisses + * (≤ 12 h → 15 min ; ≤ 24 h → 30 min ; sinon 1 h). + */ +fun stepForRange(rangeHours: Long): Long = when { + rangeHours <= 12L -> 900_000L // 15 min + rangeHours <= 24L -> 1_800_000L // 30 min + else -> 3_600_000L // 1 h +} + /** * Graphique Canvas pur, multi-séries : * - axe gauche = E2 (pg/mL), axe droit = T (ng/mL) ; @@ -309,7 +320,10 @@ private fun DrawScope.drawXLabels( ) { val paint = labelPaint(android.graphics.Paint.Align.CENTER) val spanH = (t1 - t0) / 3_600_000.0 + // Pas des labels X adaptatif — le zoom (v1.2.9) descend jusqu'à quelques heures val stepHours = when { + spanH <= 8 -> 1 + spanH <= 18 -> 3 spanH <= 25 -> 6 spanH <= 25 * 7 -> 24 else -> 24 * 5 diff --git a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt index 7bce7f8..d9acbd3 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt @@ -1,6 +1,6 @@ package com.hormonetrack.ui.screens -import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -9,12 +9,18 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.Icon import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -51,6 +57,9 @@ import kotlinx.coroutines.withContext private const val HOUR_MS = 3_600_000L private const val DAY_MS = 24 * HOUR_MS +// Zoom (v1.2.9) : plage temporelle bornée 6 h → 300 j +private const val MIN_RANGE_H = 6L +private const val MAX_RANGE_H = 24L * 300 /** * Écran Graphiques. @@ -79,6 +88,7 @@ fun ChartScreen() { val tConfig by container.settings.tConfig.collectAsState(initial = TConfig()) val autoCalibrate by container.settings.autoCalibrate.collectAsState(initial = false) + // Zoom temporel (v1.2.9) : plage bornée 6 h → 300 j, pinch + boutons var rangeHours by remember { mutableLongStateOf(24L) } var panHours by remember { mutableLongStateOf(0L) } var showT by remember { mutableStateOf(true) } @@ -144,10 +154,13 @@ fun ChartScreen() { } } else emptyList() val allDoses = doseLogs + forecastDoses + // Pas d'échantillonnage adaptatif au zoom (v1.2.9) + val step = com.hormonetrack.ui.components.stepForRange(rangeHours) if (showEse) { list.add( "ESE" to PharmacokineticEngine.computeCurve( effTreatments, allDoses, startMs, endMs, + stepMs = step, tConfig = effTConfig, modelOverride = "ESE", scalePerEster = effScales, tKPerEster = effectiveAuto?.tKPerEster @@ -158,6 +171,7 @@ fun ChartScreen() { list.add( "TFS" to PharmacokineticEngine.computeCurve( effTreatments, allDoses, startMs, endMs, + stepMs = step, tConfig = effTConfig, modelOverride = "TFS", scalePerEster = effScales, tKPerEster = effectiveAuto?.tKPerEster @@ -217,6 +231,20 @@ fun ChartScreen() { onClick = { rangeHours = 24L * 30 }, label = { Text(stringResource(R.string.chart_30j)) } ) + Spacer(Modifier.width(4.dp)) + // Zoom manuel (accessibilité — le pinch fonctionne aussi) + FilledTonalIconButton( + onClick = { rangeHours = (rangeHours * 2).coerceAtMost(MAX_RANGE_H) }, + enabled = rangeHours < MAX_RANGE_H + ) { + Icon(Icons.Filled.Add, contentDescription = stringResource(R.string.zoom_in)) + } + FilledTonalIconButton( + onClick = { rangeHours = (rangeHours / 2).coerceIn(MIN_RANGE_H, MAX_RANGE_H) }, + enabled = rangeHours > MIN_RANGE_H + ) { + Icon(Icons.Filled.Remove, contentDescription = stringResource(R.string.zoom_out)) + } } Spacer(Modifier.height(8.dp)) @@ -268,15 +296,33 @@ fun ChartScreen() { Modifier .fillMaxWidth() .onSizeChanged { chartWidthPx = it.width } - .pointerInput(rangeHours, maxPanHours) { - // Glisser vers la droite = remonter dans le passé - detectHorizontalDragGestures { change, dragAmount -> - change.consume() - if (chartWidthPx > 0 && maxPanHours > 0) { - val hoursDragged = (dragAmount / chartWidthPx * rangeHours).toLong() + .pointerInput(Unit) { + // Pan (1 doigt) + pinch-zoom (2 doigts, v1.2.9) — un seul + // handler pour éviter les conflits de consommation + detectTransformGestures { centroid, pan, zoom, _ -> + val forecastMs = + (if (showForecast) (forecastHorizonHours ?: 0L) * HOUR_MS else 0L) + if (chartWidthPx > 0) { + // Pan horizontal : glisser à droite = remonter le temps + val hoursDragged = (pan.x / chartWidthPx * rangeHours).toLong() if (hoursDragged != 0L) { panHours = (panHours + hoursDragged).coerceIn(0L, maxPanHours) } + // Zoom autour du point focal : le temps sous le doigt + // reste fixe pendant le pinch + if (zoom != 1f && zoom > 0f) { + val f = (centroid.x / chartWidthPx.toDouble()).coerceIn(0.0, 1.0) + val newRange = (rangeHours / zoom).toLong() + .coerceIn(MIN_RANGE_H, MAX_RANGE_H) + if (newRange != rangeHours) { + val endAnchor = nowMs + forecastMs - panHours * HOUR_MS + val tFocal = endAnchor - ((1.0 - f) * rangeHours * HOUR_MS).toLong() + val newEnd = tFocal + ((1.0 - f) * newRange * HOUR_MS).toLong() + val newPan = ((nowMs + forecastMs - newEnd) / HOUR_MS).toLong() + panHours = newPan.coerceIn(0L, maxPanHours) + rangeHours = newRange + } + } } } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index de1de6e..77974d8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -31,6 +31,8 @@ Analyses Prévision Pics / creux + Zoom avant (fenêtre plus courte) + Zoom arrière (fenêtre plus longue) ▲▼ pics & creux estimés (par courbe) Revenir à maintenant Fais glisser le graphique vers la droite pour remonter dans le passé. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8535738..44aaff1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,8 @@ Labs Forecast Peaks / troughs + Zoom in (shorter window) + Zoom out (longer window) ▲▼ estimated peaks & troughs (per curve) Back to now Drag the chart to the right to travel into the past. diff --git a/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt b/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt new file mode 100644 index 0000000..baedb79 --- /dev/null +++ b/app/src/test/java/com/hormonetrack/ui/ChartZoomTest.kt @@ -0,0 +1,31 @@ +package com.hormonetrack.ui + +import com.hormonetrack.ui.components.stepForRange +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Pas d'échantillonnage adaptatif au zoom (v1.2.9) : plus la fenêtre est + * courte, plus le pas est fin — sinon les courbes paraissent anguleuses. + */ +class ChartZoomTest { + + @Test + fun `deep zoom uses 15 minute steps`() { + assertEquals(900_000L, stepForRange(6L)) + assertEquals(900_000L, stepForRange(12L)) + } + + @Test + fun `mid zoom uses 30 minute steps`() { + assertEquals(1_800_000L, stepForRange(13L)) + assertEquals(1_800_000L, stepForRange(24L)) + } + + @Test + fun `wide ranges use hourly steps`() { + assertEquals(3_600_000L, stepForRange(25L)) + assertEquals(3_600_000L, stepForRange(24L * 30)) + assertEquals(3_600_000L, stepForRange(24L * 300)) + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9e5e189..79d0e87 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,21 @@ # Changelog — HormoneTrack Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/). -Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.8`, …). +Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.9`, …). + +## [1.2.9] — 2026-09-05 (versionCode 12) + +### Ajouté +- **Zoom du graphique de simulation** : pinch à 2 doigts **et** boutons − / + + (accessibilité), plage bornée **6 h → 300 j** ; le point focal du pinch reste + fixe dans le temps ; échantillonnage adaptatif (15 min ≤ 12 h, 30 min ≤ + 24 h, sinon 1 h — courbes lisses à fort zoom) ; labels X adaptatifs + (1 h / 3 h). Un seul gestionnaire de gestes gère pan + zoom (pas de + conflit de consommation). +- **README : disclaimer IA** en en-tête — projet développé avec l'IA, + contribution humaine essentielle (feedback, retours utilisateur, suggestions) ; + mention factuelle reprise en §1/§2 de la doc de développement. +- 3 tests `stepForRange` (67 au total). ## [1.2.8] — 2026-09-05 (versionCode 11) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index c632c75..4d9223a 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -45,6 +45,11 @@ Android, apps tierces au poignet quasi impossibles (cf §17). Utilisatrice équi **Gadgetbridge** (FOSS) sur son téléphone → v1 = **app téléphone + notifications miroir sur la montre** via GB (ou Huawei Health). +> **Développement IA-assisté** : le code a été produit avec un assistant IA ; +> la contribution humaine = feedback continu, retours utilisateur (tests réels +> sur téléphone, bugs avec exports), suggestions et validation des releases +> (détail §2 — factuel, session par session). + Fonctionnalités v1 : - Courbes estimées heure par heure : E2 (pg/mL) + T (ng/mL) — 24 h / 7 j / 30 j - Deux modèles PK du `.ods` (Estrannaise / TFS) pour injections EV/EU/EEn ; @@ -71,6 +76,7 @@ Fonctionnalités v1 : | 5 sept. 2026 (session v1.2.1) | Remontées UX/utilisateur : calibration **par période d'ester** (labs EV → doses EV, labs EEn → doses EEn — corrige les courbes gonflées), lab T "pg/mL" neutralisé, **édition des labs**, tap accueil → Graphiques + mini-légende, auto-cal fusionnée (E2+T, un seul switch) ; 2ᵉ régression épinglée sur le nouvel export ; **44 tests verts**, APK v1.2.1. | | 5 sept. 2026 (session v1.2.2) | Toggle T masque aussi les labs T ; **prise de sang E2 + T en une entrée** (chacune optionnelle) affichée **côte à côte** ; sélecteur d'édition par marqueur ; suppression par prise ; 4 tests de regroupement ; **48 tests verts**, APK v1.2.2. | | 5 sept. 2026 (session v1.2.6) | Valeurs estimées affichées sur les pics/creux (drawExtremum) ; **import JSON en mode écrasement** (bug : conflit d'IDs si données existantes) avec restauration du tConfig et reprogrammation des rappels ; docs ; 62 tests verts, APK v1.2.6 + releases. | +| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. | | 5 sept. 2026 (session v1.2.8) | « Fréquence d'injection » → **« Posologie »** (terme adapté à toutes les voies) ; quirk Gitea découvert (noms d'assets normalisés à l'upload) → renommage PATCH dans le script ; releases avec les 2 APK ; 64 tests verts. | | 5 sept. 2026 (session v1.2.7) | Prévision : les créneaux passés ne sont plus simulés (bug oubli → faux pic historique) ; retard = décalage voulu ; mismatch doc-code §7.3b corrigé ; 2 tests ; **64 tests verts**, APK v1.2.7 + releases. | @@ -396,7 +402,7 @@ dépôt (ni maintenant, ni si le repo devient public un jour). - `.gitignore` contient `local-test-data/` → jamais commités ; - les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI - exécute 44 tests au lieu de 56 ; + exécute 44 tests au lieu de 67 ; - le workdir des tests Gradle est le dossier du module (`app/`) → les tests cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ; - **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous @@ -455,7 +461,8 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17). (+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran Graphiques** (v1.2.1) avec mini-légende E2/T ; données auto-calibrées si l'option est active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s -- `ChartScreen` (v1.2, le plus riche) : plages 24 h/7 j/30 j ; **panoramique** +- `ChartScreen` (v1.2, le plus riche) : plages 24 h/7 j/30 j + **boutons zoom − / +** + (v1.2.9) ; **panoramique** (`detectHorizontalDragGestures` — tirer vers la droite remonte dans le passé, `panHours` borné à [0, âge de la 1ʳᵉ dose + plage], bouton « Revenir à maintenant ») ; **toggles indépendants Estrannaise/TFS** → deux `computeCurve` avec `modelOverride` @@ -518,6 +525,15 @@ triangles ▲▼ via `PharmacokineticEngine.detectExtrema` **accompagnés de la valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur de la série (`drawExtremum`). +**Zoom (v1.2.9)** : pinch **et** boutons − / + — plage temporelle bornée +**6 h → 300 j** (`MIN_RANGE_H`/`MAX_RANGE_H`), point focal du pinch maintenu +fixe dans le temps (formule centroid), échantillonnage **adaptatif** +(`stepForRange` : 15 min ≤ 12 h, 30 min ≤ 24 h, sinon 1 h — courbes lisses à +fort zoom) et labels X adaptatifs (1 h / 3 h ajoutés). Le pan ET le zoom +partagent un seul `detectTransformGestures` (un handler = pas de conflit de +consommation) ; le zoom/pan restent gérés **par le parent**, le Canvas demeure +purement déclaratif. + Pièges : - `DrawScope` implémente `Density` → `X.dp.toPx()` direct ; ne PAS écrire de helper custom - Tout label passe par `drawContext.canvas.nativeCanvas` + `android.graphics.Paint` @@ -918,6 +934,8 @@ Sur le téléphone de test (à compléter par l'utilisatrice) : - [ ] **v1.2.x** : édition d'une dose (tap ligne Doses) et d'un lab (tap ligne Analyses → sélecteur E2/T si paire) ; suppression par prise entière - [ ] **v1.2.x** : prise de sang E2 + T en une entrée (champs optionnels) +- [ ] **v1.2.9** : zoom du graphique (pinch 2 doigts + boutons − / + ; labels + X adaptatifs ; courbes lisses à fort zoom) - [ ] **v1.2.7** : prévision après un OUBLI (pas de faux pic dans le passé) et après un RETARD (prévision décalée, suivant la dernière prise) - [ ] **v1.2.6** : import d'un backup avec l'app déjà remplie → ÉCRASER & @@ -927,4 +945,4 @@ Sur le téléphone de test (à compléter par l'utilisatrice) : sur le graphique** et la calibration couvre toujours ses périodes --- -*Doc mise à jour le 5 sept. 2026 (v1.2.8) — build OK, 62/62 tests verts (44 sans les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée de santé dans le dépôt ni l'historique.* +*Doc mise à jour le 5 sept. 2026 (v1.2.9) — build OK, 62/62 tests verts (44 sans les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée de santé dans le dépôt ni l'historique.*