diff --git a/lib/compute/vendor_sleep.dart b/lib/compute/vendor_sleep.dart index 7a95b8a23..a534a3b85 100644 --- a/lib/compute/vendor_sleep.dart +++ b/lib/compute/vendor_sleep.dart @@ -47,6 +47,27 @@ const String kOurRingNightSource = 'hr_5min_stager'; /// ('auto_fallback'), not the device's. const String kHrWindowNightSource = 'hr_5min_window'; +/// The point list a night is drawn and read from: each epoch's stage from its +/// start, closed by a final point at wake. A stretch no epoch covers gets an +/// `unobserved` boundary where the epoch before it ends, so a reader of the +/// list never carries that epoch's stage across the hole. +List> vendorHypnogram( + List epochs, int wakeSec) { + final out = >[]; + for (var i = 0; i < epochs.length; i++) { + final e = epochs[i]; + out.add({'t': e.startSec, 'stage': e.stage}); + final next = i + 1 < epochs.length ? epochs[i + 1].startSec : wakeSec; + if (next > e.endSec) out.add({'t': e.endSec, 'stage': 'unobserved'}); + } + final last = epochs.last; + out.add({ + 't': wakeSec, + 'stage': last.endSec < wakeSec ? 'unobserved' : last.stage, + }); + return out; +} + /// One night of one device's epochs, as `vendor_sleep_epoch` groups them. class VendorNight { final String deviceId; diff --git a/lib/data/local_repository_impl.dart b/lib/data/local_repository_impl.dart index 48c940df8..1c708a471 100644 --- a/lib/data/local_repository_impl.dart +++ b/lib/data/local_repository_impl.dart @@ -29,6 +29,7 @@ import '../compute/manual_session.dart'; import '../compute/onehz_pipeline.dart' show kUnknownAbsenceNote, needInputNote; import '../compute/profile.dart'; import '../compute/quiet_level.dart'; +import '../compute/vendor_sleep.dart' show vendorHypnogram; import 'package:openstrap_protocol/openstrap_protocol.dart' as proto; import 'package:openstrap_analytics/onehz.dart' as ana; @@ -2334,10 +2335,7 @@ class LocalRepositoryImpl extends LocalRepository { 'stage_min': minutes, // Same point shape as our own night: each stage from its start, // closed by a final point at wake. - 'hypnogram': [ - for (final e in n.epochs) {'t': e.startSec, 'stage': e.stage}, - {'t': n.offsetSec, 'stage': n.epochs.last.stage}, - ], + 'hypnogram': vendorHypnogram(n.epochs, n.offsetSec), }); } return out; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 233bed5aa..aa6000649 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1176,6 +1176,10 @@ "metricDetailSlotNoValueRecording": "{day}, kein Wert · {who}", "metricDetailSlotNothingRecording": "{day}, keine Aufzeichnung", "metricDetailOpenDay": "{day} öffnen", + "chartPanelLatest": "ZULETZT", + "chartPanelSelected": "AUSGEWÄHLT", + "chartPanelSevenDayAvg": "7-Tage-Schnitt", + "chartPanelVsAvg": "ggü. Schnitt", "metricDetailNoRecordLabel": "Kein Eintrag", "metricDetailLowest": "Niedrigster", "metricDetailTypical": "Typisch", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ec5231ef9..3add13a51 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -5680,6 +5680,22 @@ } } }, + "chartPanelLatest": "LATEST", + "@chartPanelLatest": { + "description": "Caption prefix on the panel under a chart when nothing is selected: the newest reading. Shown upper-case where the script has case. It is not a judgement such as best." + }, + "chartPanelSelected": "SELECTED", + "@chartPanelSelected": { + "description": "Caption prefix on the panel under a chart while a finger is on the chart: the reading the finger is on." + }, + "chartPanelSevenDayAvg": "7-day avg", + "@chartPanelSevenDayAvg": { + "description": "Metric-detail panel stat: the last 7 days up to today, the same figure whichever day is shown." + }, + "chartPanelVsAvg": "vs avg", + "@chartPanelVsAvg": { + "description": "Metric-detail panel stat: the shown value minus that 7-day average." + }, "metricDetailNoRecordLabel": "No record", "@metricDetailNoRecordLabel": { "description": "Value text shown in the touched-day row when that day has no record." diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a236842ec..83eb37e74 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1133,6 +1133,10 @@ "metricDetailSlotNoRecord": "{day}, sin registro", "metricDetailSlotWithValue": "{day}, {value} {unit}", "metricDetailOpenDay": "Abrir {day}", + "chartPanelLatest": "ÚLTIMO", + "chartPanelSelected": "SELECCIONADO", + "chartPanelSevenDayAvg": "Media de 7 días", + "chartPanelVsAvg": "vs. media", "metricDetailNoRecordLabel": "Sin registro", "metricDetailLowest": "Mínimo", "metricDetailTypical": "Típico", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2875f5709..b0a5ce712 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1133,6 +1133,10 @@ "metricDetailSlotNoRecord": "{day}, aucun enregistrement", "metricDetailSlotWithValue": "{day}, {value} {unit}", "metricDetailOpenDay": "Ouvrir {day}", + "chartPanelLatest": "DERNIER", + "chartPanelSelected": "SÉLECTIONNÉ", + "chartPanelSevenDayAvg": "Moy. sur 7 jours", + "chartPanelVsAvg": "vs moy.", "metricDetailNoRecordLabel": "Aucun enregistrement", "metricDetailLowest": "Minimum", "metricDetailTypical": "Typique", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index b9fea6734..87445f536 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -1133,6 +1133,10 @@ "metricDetailSlotNoRecord": "{day}, कोई रिकॉर्ड नहीं", "metricDetailSlotWithValue": "{day}, {value} {unit}", "metricDetailOpenDay": "{day} खोलें", + "chartPanelLatest": "नवीनतम", + "chartPanelSelected": "चुना गया", + "chartPanelSevenDayAvg": "7 दिन का औसत", + "chartPanelVsAvg": "औसत से अंतर", "metricDetailNoRecordLabel": "कोई रिकॉर्ड नहीं", "metricDetailLowest": "न्यूनतम", "metricDetailTypical": "सामान्य", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index f25901a16..b78a34dd9 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -2507,6 +2507,10 @@ } } }, + "chartPanelLatest": "ПОСЛЕДНЕЕ", + "chartPanelSelected": "ВЫБРАНО", + "chartPanelSevenDayAvg": "Среднее за 7 дн.", + "chartPanelVsAvg": "от среднего", "metricDetailNoRecordLabel": "Нет записи", "metricDetailLowest": "Наименьшее", "metricDetailTypical": "Обычное", diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 19e7265dd..7b51c6b4d 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -5668,6 +5668,10 @@ } } }, + "chartPanelLatest": "ล่าสุด", + "chartPanelSelected": "ที่เลือก", + "chartPanelSevenDayAvg": "ค่าเฉลี่ย 7 วัน", + "chartPanelVsAvg": "เทียบค่าเฉลี่ย", "metricDetailNoRecordLabel": "ไม่มีข้อมูลบันทึก", "@metricDetailNoRecordLabel": { "description": "Value text shown in the touched-day row when that day has no record." diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 111dc74e2..c66c6cff0 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1133,6 +1133,10 @@ "metricDetailSlotNoRecord": "{day},无记录", "metricDetailSlotWithValue": "{day},{value} {unit}", "metricDetailOpenDay": "打开{day}", + "chartPanelLatest": "最新", + "chartPanelSelected": "已选", + "chartPanelSevenDayAvg": "7 天平均", + "chartPanelVsAvg": "与平均相比", "metricDetailNoRecordLabel": "无记录", "metricDetailLowest": "最低", "metricDetailTypical": "典型", diff --git a/lib/ui2/README.md b/lib/ui2/README.md index ae6e012e2..8174129f1 100644 --- a/lib/ui2/README.md +++ b/lib/ui2/README.md @@ -121,6 +121,53 @@ that did not exist. `Scrubber` is the only drag: it carries the slider role, so increase/decrease reach it without a pointer, and `describe` is what those steps say out loud. +### ChartPanel — what a chart reads, always on screen + +```dart +ChartPanel({required String? time, required bool scrubbed, required Color color, + required List cells, List stats = const [], + List timeSpan = const [], VoidCallback? onOpen, String? openLabel}) +ChartPanelCell(String label, {Color? color, String? value, Color? valueColor}) +``` + +Put it under a chart that has a `Scrubber`, in the same card. It is there at +rest and while a finger is on the chart, so the chart never changes height under +the thumb and nothing is readable only while it is held. There is no pop-in. + +- **Caption.** `LATEST ·