Skip to content

fix(charts): a continuous line with a zero floor and visible readings - #2009

Merged
ryanbr merged 1 commit into
mainfrom
fix/chart-continuous-and-floor
Sep 8, 2026
Merged

ryanbr merged 1 commit into
mainfrom
fix/chart-continuous-and-floor

Conversation

@ryanbr

@ryanbr ryanbr commented Sep 8, 2026

Copy link
Copy Markdown
Owner

fix(charts): a continuous line with a zero floor and visible readings

Three corrections to my own chart work, from seeing it on a device.

Breaking the line on a missing day is removed. With points positioned by date a
gap already shows as a longer run between two readings, and breaking as well
fragmented the line into pieces with the odd orphan dot, which reads as a
rendering fault rather than as missing data. The reporter called it broken twice
and was right both times. VO2max keeps its break on an estimator change, which is
a different question about the same line.

The domain is a zero FLOOR rather than 0..100. The chart widens a domain to
contain the data, so a zero-width domain at 0 pins the bottom while the ceiling
follows the readings. Anchoring at 100 overcorrected: Effort peaks in the low
forties, so the top 58% of the chart sat permanently empty, and Rest at 46..93
wasted the bottom half. The zero floor keeps what mattered, comparable heights and
a 0.0 day reading as the floor, without spending most of the height on range
nobody reaches.

Readings are drawn as markers. A line alone cannot say WHERE the measurements
are, and once gaps stretch it between points a long straight run is either a
steady week or one reading either side of a gap. Markers say which without
fragmenting the stroke. Opt-in, so the dense live charts are untouched.

The gap-segment helpers go with the break they served.

What is kept from the earlier passes

Date-proportional spacing stays: a gap occupies the width it spans, which is what makes a stretched line
segment mean something. The isolated-point marker stays too, since VO2max can still produce a one-point
segment and a Path with a moveTo and no lineTo draws nothing.

Honest note

This is the sixth change to this chart today. Anchoring and spacing were right; breaking the line was not,
and I shipped it before the spacing it depended on. Seeing it on a device is what settled it.

674 classes / 5709 tests, 0 failures.

@ryanbr
ryanbr force-pushed the fix/chart-continuous-and-floor branch from cac64f1 to 0bf5f91 Compare September 8, 2026 12:53
Three corrections to my own chart work, from seeing it on a device.

Breaking the line on a missing day is removed. With points positioned by date a
gap already shows as a longer run between two readings, and breaking as well
fragmented the line into pieces with the odd orphan dot, which reads as a
rendering fault rather than as missing data. The reporter called it broken twice
and was right both times. VO2max keeps its break on an estimator change, which is
a different question about the same line.

The domain is a zero FLOOR rather than 0..100. The chart widens a domain to
contain the data, so a zero-width domain at 0 pins the bottom while the ceiling
follows the readings. Anchoring at 100 overcorrected: Effort peaks in the low
forties, so the top 58% of the chart sat permanently empty, and Rest at 46..93
wasted the bottom half. The zero floor keeps what mattered, comparable heights and
a 0.0 day reading as the floor, without spending most of the height on range
nobody reaches.

The floor needed one more thing to actually hold. A series that is entirely flat
at the floor has zero span, and the zero-span fallback puts a flat line mid-chart:
right when the scale came from the data, wrong when a floor was asked for. An
all-zero Effort week would have floated through the middle while claiming a floor
at zero. The domain widens by a unit in that case, and a test pins the property
rather than the constant.

Readings are drawn as markers. A line alone cannot say WHERE the measurements
are, and once gaps stretch it between points a long straight run is either a
steady week or one reading either side of a gap. Markers say which without
fragmenting the stroke. Opt-in, so the dense live charts are untouched, and drawn only while they can be
told apart: on the ALL range a daily series is hundreds of points and a dot every
few pixels merges into a smear that hides the line it annotates.

The gap-segment helpers go with the break they served, and so does an
isolated-point marker I added earlier on a false premise: #1586 already draws a
circle at every point whenever segments are supplied, exactly so neither side of a
transition disappears. The orphan dot in the reports was that, not a missing
reading, and nothing was ever hidden.
@ryanbr
ryanbr force-pushed the fix/chart-continuous-and-floor branch from 0bf5f91 to d547049 Compare September 8, 2026 13:02
@ryanbr
ryanbr merged commit e3461f9 into main Sep 8, 2026
3 checks passed
@ryanbr
ryanbr deleted the fix/chart-continuous-and-floor branch September 8, 2026 13:09
DX23876 added a commit to DX23876/noop that referenced this pull request Sep 16, 2026
Thirty real conflicts. The Android tree is dropped again per FORK_GUIDE; every
other hunk was resolved by asking which mechanism this fork actually runs, not
by preferring a side.

Taken from upstream:
  • Protocol: ryanbr#2017/ryanbr#2018/ryanbr#2020 — the 5/MG historical optical and raw-IMU
    layouts, and the v26 window's ABSOLUTE base code beside its deltas. Its
    migration lands as `v57-ppg-waveform-base-code` (upstream's v44), since this
    fork's sequence is nine ahead of theirs.
  • BLE diagnostics ryanbr#1998-ryanbr#2003: a bond end that says WHY, named BLE integers, a
    session-budgeted hex dump, and "we hold the link" only when the OS says so.
  • Battery: ryanbr#1951 stop asking a 5/MG for a reading it refuses; ryanbr#1959 poll
    0x2A19 for an unbonded one.
  • Scoring: ryanbr#1978/ryanbr#1982/ryanbr#1990 the RHR/HRV baseline gates, ryanbr#1977 the strain
    denominator's domain, RMSSD withheld on an over-counted night, ryanbr#1943 the
    resting-HR bin gate, the closed final HR/HRV window, HR-recovery requiring
    one CONTINUOUS effort, ryanbr#1984 nights bridged under minSleepMin, ryanbr#2015 motion
    re-folded only when that day's gravity moved, ryanbr#2016 a provisional Rest.
  • Charts ryanbr#2004/ryanbr#2005/ryanbr#2007/ryanbr#2009/ryanbr#2011 and ryanbr#1995's hero-ring detail route,
    ryanbr#1164's "Pending sync" caption under Rest.
  • Widgets: the heart-rate widget with its trace (ryanbr#1957/ryanbr#1964) and the widget
    cost counters.
  • Health: ryanbr#1503 — the Apple Health external UUID drops the strap id, so a
    re-pair no longer strands every prior record, plus the one-off sweep that
    reclaims the stranded ones; ryanbr#1205 GPS routes read back from Apple Health.
  • Storage: rolling retention for ppgWaveformSample, the skin-temp absolute
    backfill outside the 21-night window, per-table byte estimates.
  • Units split (body vs exercise distance), per-day wake overrides, the
    wind-down nudge pinned to its own day, and the German Sie→du conversion
    (29 entries, merged per LOCALE so the fork's own de edits stayed).

Kept on the fork's side, with the reason:
  • ryanbr#1005's in-memory `dayScanCache` stays out, again: the persisted
    `DayScanFingerprint` survives relaunch and its key folds the per-day weight
    and max HR upstream's does not.
  • The steps-motion cache stays PERSISTED for the same reason. Upstream's
    failed-read-vs-empty-day distinction IS adopted into it — on a persisted
    cache a stale zero from a read error would outlive the process that made it.
  • Every stream read in the day loop keeps `limit: Int.max`. All five are
    uncapped together; taking StreamReadCap for three of them would leave the
    loop half-capped, which is worse than either choice.
  • The coach engine. Upstream's K1-K15 layer is not adopted: its single 40-row
    `coachMessage` table, its tool-less pre-baked context and its own brief are
    behind what this fork already ships. Its ADDITIVE half is kept — the
    on-device voice input, the Siri "Ask Coach" intent, the brief scheduler and
    brief widget, and streaming for the OpenAI-compatible providers this fork's
    Anthropic-only StreamingToolClient does not cover. They are wired up in the
    next commit; `CoachBriefWidget` is deliberately not registered until then.
  • Liquid's `DomainTheme` labels, value-sampled Charge tint, `LiquidScoreGauge`
    and `TodayCardSurface`; classic Today's ring order and `LiveEffort.today`.
  • RootTabView's four-tab shell and `MoreCatalog`: Coach hangs on content here,
    not on a tab.

Two duplicate mechanisms collapsed rather than carried:
  • `ChargeBreakdownWiring` (upstream) and `ChargeBreakdownFormat` (fork) are the
    same extraction done twice. The fork's survives, Coupled now reads it, and
    upstream's TEST is kept — it is coverage this path never had.
  • Upstream's `CoachMessageStore` is dropped; `CoachConversationStore` already
    autosaves the whole multi-conversation history.

One defect found while resolving, fixed here: `LiveEffort.today` read HR with no
explicit limit, so `hrSamples`' 8000-row default truncated it — at ~18k rows a
day that covered about ten hours and the live Effort silently stopped climbing.
It failed safe (`effectiveEffort` takes the max), which is why it went unseen.

`Live5RestFrozenTests` is removed by upstream, which replaces it with
`Live5NoGravityRestChainTests`. `local_day_windows_oracle.json` moves out of
upstream's android path into `Tests/StrandAnalyticsTests/Oracles/` — NOT
`Fixtures/`, which this repo's .gitignore excludes everywhere.

Verified: StrandAnalytics 2233, WhoopStore 627, WhoopProtocol 700, StrandImport
293, StrandDesign 120, NoopLocalAccess 14, SemanticMemory 19, OuraProtocol 209
and PolarProtocol 25 all green; macOS and iOS both build; doc_comment_lint clean;
`i18n_audit --ci` byte-identical to its pre-merge output (it is red on main for
reasons that predate this, and the seven new widget literals were given all nine
locales so the gate did not get worse).

Six StrandTests were ALREADY failing on main before this merge and still are —
`CoachMyLogsAndZonesTests` (the tool census pin was not bumped when the Hevy
tools landed) and five in `RawPhysiologyUnionTests`. Every input to them —
CoachTools.swift, ToolConsent.swift, Repository.swift and both test files — is
byte-identical to main, so this merge neither caused nor fixed them. They went
unnoticed because app-build.yml last ran on 2026-08-25.

Analysis migration required: YES. The scoring changes above make stored derived
values stale; the recipe bump and its resumable migration are the next commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant