Portage navigateur complet de l'app Android HormoneTrack v1.4.10 : - moteur PK fidèle (Estrannaise tables ODS, Transfem Science V3C, WHSAH fit Mona, Bateman) — paramètres identiques, invariants Android préservés (fixes #19-#23, #35, #52-#62), 121 tests Node épinglés - calibration par période d'ester et par modèle affiché (#60/#61) - UI 6 écrans + éditeur, CurveChart Canvas (zoom/pan fractionnaire, prévision, pics/creux, fuseau configurable), rappels web (Notification API), seuils d'alerte, i18n FR/EN - sauvegarde JSON v2 compatible Android bidirectionnelle (rétrocompat v1) - 100 % local : localStorage, aucun serveur applicatif, aucune télémétrie - processus : scripts/check.sh (syntaxe, i18n, tests, E2E navigateur, smoke HTTP), docs séparées (README + DEVELOPPEMENT + CHANGELOG) - dépôt GIT SÉPARÉ de l'Android : historique 100 % propre, versions alignées sur l'Android porté, changelogs indépendants Non porté (impossible dans un navigateur, documenté §12) : agenda récurrent, notifications onglet fermé, montre.
75 lines
3.0 KiB
JavaScript
75 lines
3.0 KiB
JavaScript
/**
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* Détection des pics et creux (partie 4/4 du moteur) — portage de
|
|
* `PharmacokineticEngine.detectExtrema` (v1.2.3) : triangles ▲▼ aux extrema
|
|
* locaux de chaque courbe, avec valeurs estimées (v1.2.6).
|
|
*
|
|
* PUR, testable en Node (web/tests/extrema.test.js).
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
*/
|
|
|
|
/**
|
|
* Un extremum détecté : pic (isPeak = true) ou creux, avec sa valeur.
|
|
*/
|
|
export class Extremum {
|
|
/** @param {number} timestamp · @param {number} value · @param {boolean} isPeak */
|
|
constructor(timestamp, value, isPeak) {
|
|
this.timestamp = timestamp;
|
|
this.value = value;
|
|
this.isPeak = isPeak;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Détecte les maxima et minima LOCAUX d'une courbe (les pics après chaque
|
|
* injection et les creux juste avant la suivante, en régime d'équilibre).
|
|
*
|
|
* - les plateaux (valeurs égales consécutives) comptent pour UN seul extremum ;
|
|
* - les extrema consécutifs de même type sont FUSIONNÉS (le plus extrême gagne) ;
|
|
* - les oscillations d'amplitude < minSwingAbs sont ignorées (bruit
|
|
* d'arrondi) — utiliser ~2 pg/mL pour l'E2 et ~0,02 ng/mL pour la T.
|
|
*
|
|
* @param {import('./pk-engine.js').LevelPoint[]} points Points de courbe
|
|
* @param {(p:object)=>number} selector Ce qu'on analyse (`p => p.e2` ou `p => p.t`)
|
|
* @param {number} [minSwingAbs=2.0] Amplitude minimale d'une oscillation
|
|
* @returns {Extremum[]}
|
|
*/
|
|
export function detectExtrema(points, selector, minSwingAbs = 2.0) {
|
|
if (points.length < 3) return [];
|
|
const values = points.map(selector);
|
|
const abs = Math.abs;
|
|
|
|
// 1) extrema locaux stricts (un plateau = son premier point)
|
|
const raw = []; // [index, isPeak]
|
|
let i = 1;
|
|
while (i < values.length - 1) {
|
|
const v = values[i];
|
|
const prev = values[i - 1];
|
|
let j = i;
|
|
while (j < values.length - 1 && values[j + 1] === v) j++; // plateau
|
|
if (j >= values.length - 1) break;
|
|
const next = values[j + 1];
|
|
if (v > prev && v > next) {
|
|
raw.push([i, true]);
|
|
} else if (v < prev && v < next) {
|
|
raw.push([i, false]);
|
|
}
|
|
i = j + 1;
|
|
}
|
|
|
|
// 2) alternance pic/creux + filtre d'amplitude
|
|
const out = [];
|
|
for (const [idx, isPeak] of raw) {
|
|
const last = out[out.length - 1] || null;
|
|
if (last && last.isPeak === isPeak) {
|
|
// doublon de même type : garder le plus extrême
|
|
const better = isPeak ? values[idx] > last.value : values[idx] < last.value;
|
|
if (better) out[out.length - 1] = new Extremum(points[idx].timestamp, values[idx], isPeak);
|
|
continue;
|
|
}
|
|
if (last && abs(last.value - values[idx]) < minSwingAbs) continue;
|
|
out.push(new Extremum(points[idx].timestamp, values[idx], isPeak));
|
|
}
|
|
return out;
|
|
}
|