Repository navigation
feat(today): draw the daily scores as bars, not as a line - #2011
Merged
Merged
Conversation
ryanbr
force-pushed
the
feat/vital-detail-bars
branch
from
September 9, 2026 00:16
e3617ae to
a5cfb18
Compare
The Effort trend still read as noise after anchoring, spacing and a continuous line, and the chart type was the reason. A line asserts continuity between readings: it says the value travelled from one to the next. For a daily score that is false, and on the reported series it is most of what looks chaotic. That Effort swings 19.8 points a day on a 0..42 range, so the line spends the whole chart climbing to and diving from values that never existed. Bars claim nothing between slots. A zero day is a short bar beside a tall one rather than a plunge and a climb. One slot per DAY rather than per reading, which positions the bars by date with no spacing machinery at all: a day with no reading is an empty slot of the right width. It carries NaN, which BarChart already treats as nothing to draw, and a real zero stays a real zero. Only the daily SCORES: Effort, Rest and Charge. Resting HR, HRV, skin temperature, respiratory rate and blood oxygen are LEVELS that do vary continuously between measurements, so a line is the honest shape for them, and drawing them as bars would be the mirror of the mistake this corrects. Fitness Age and Vitality are too sparse to fill a bar chart. Giving missing days their own slots made one thing reachable that was not before: BarChart flattens a non-finite value to 0.0 so it draws nothing, which is right for the geometry and wrong for the read-out, so an empty slot would have answered "0.0 %" on tap for a day that was never measured. A slot is now labelled only when its raw value is finite. The number itself still comes from the cleaned value, so no existing caller's label changes: cleaning also flattens NEGATIVES, and sleep debt may pass them. The densified slots and their labels are remembered, like the line path's derived lists beside them: densifying rebuilds a slot per day and formats a label for each, and on the ALL range that is hundreds of both on every recomposition. BarChart is the existing primitive, already used by the Sleep metric cards and Trends. Nothing new was built.
ryanbr
force-pushed
the
feat/vital-detail-bars
branch
from
September 9, 2026 00:28
a5cfb18 to
b6ba7ac
Compare
ryanbr
added a commit
that referenced
this pull request
Sep 9, 2026
…h platforms Reported: pick Line in Settings, tap a ring on Today, and the detail chart draws bars anyway. The cause is that this screen had never read the setting. It is honoured in exactly one place, the Trends screen, and the detail chart simply always drew a line, which happened to match Line and silently ignored Bar. #2011 then chose the shape per METRIC here, forcing bars for the daily scores, and that turned a dormant gap into a visible contradiction: the picker says Line and the chart draws bars. The inverse was broken the whole time and nobody reported it, because it fails quietly in the direction people do not notice. A reader who picked Bar got lines on this screen for every metric outside those three, while the trend chart for the same metric drew bars. So the setting decides, and the metric does not. Trends already applies the preference to every metric, so a detail chart reached from a ring now agrees with the trend chart for that metric rather than contradicting it. iOS had the same gap and no equivalent of the override, since the earlier change was Android only: MetricExplorerView never consulted the preference, so a chosen Bar drew a line there too. TrendChart already takes showsBars, so this is one rule expressed once per platform rather than two behaviours. That earlier reasoning is not discarded, it moves to where it can be acted on. If a line really does misrepresent a daily score, and it does assert travel between readings that never happened, then the honest response is to change the DEFAULT to bars, visibly, in the picker, where the setting and the chart agree. Overriding a reader who chose otherwise is a different claim and should not be made on their behalf. That default is left alone here, deliberately, because it is global and would move Trends too. The tests that pinned the per-metric rule are repinned rather than deleted: a chosen Line draws lines including for the daily scores, a chosen Bar draws bars, and the metric key no longer changes the answer for either.
ryanbr
added a commit
that referenced
this pull request
Sep 9, 2026
…rms (#2027) Reported: pick Line in Settings, tap a ring on Today, and the detail chart draws bars anyway. The cause is that this screen had never read the setting. It is honoured in exactly one place, the Trends screen, and the detail chart simply always drew a line, which happened to match Line and silently ignored Bar. #2011 then chose the shape per METRIC here, forcing bars for the daily scores, and that turned a dormant gap into a visible contradiction: the picker says Line and the chart draws bars. The inverse was broken the whole time and nobody reported it, because it fails quietly in the direction people do not notice. A reader who picked Bar got lines on this screen for every metric outside those three, while the trend chart for the same metric drew bars. So the setting decides, and the metric does not. Trends already applies the preference to every metric, so a detail chart reached from a ring now agrees with the trend chart for that metric rather than contradicting it. iOS had the same gap and no equivalent of the override, since the earlier change was Android only: MetricExplorerView never consulted the preference, so a chosen Bar drew a line there too. TrendChart already takes showsBars, so this is one rule expressed once per platform rather than two behaviours. That earlier reasoning is not discarded, it moves to where it can be acted on. If a line really does misrepresent a daily score, and it does assert travel between readings that never happened, then the honest response is to change the DEFAULT to bars, visibly, in the picker, where the setting and the chart agree. Overriding a reader who chose otherwise is a different claim and should not be made on their behalf. That default is left alone here, deliberately, because it is global and would move Trends too. The tests that pinned the per-metric rule are repinned rather than deleted: a chosen Line draws lines including for the daily scores, a chosen Bar draws bars, and the metric key no longer changes the answer for either.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(today): draw the daily scores as bars, not as a line
The Effort trend still read as noise after anchoring, spacing and a continuous
line, and the chart type was the reason. A line asserts continuity between
readings: it says the value travelled from one to the next. For a daily score that
is false, and on the reported series it is most of what looks chaotic. That Effort
swings 19.8 points a day on a 0..42 range, so the line spends the whole chart
climbing to and diving from values that never existed.
Bars claim nothing between slots. A zero day is a short bar beside a tall one
rather than a plunge and a climb.
One slot per DAY rather than per reading, which positions the bars by date with no
spacing machinery at all: a day with no reading is an empty slot of the right
width. It carries NaN, which BarChart already treats as nothing to draw, and a
real zero stays a real zero.
Only the daily SCORES: Effort, Rest and Charge. Resting HR, HRV, skin temperature,
respiratory rate and blood oxygen are LEVELS that do vary continuously between
measurements, so a line is the honest shape for them, and drawing them as bars
would be the mirror of the mistake this corrects. Fitness Age and Vitality are too
sparse to fill a bar chart.
BarChart is the existing primitive, already used by the Sleep metric cards and
Trends. Nothing new was built.
Why this rather than more line tweaks
Anchoring, date spacing and removing the breaks each helped, and none of them addressed the cause. The
reporter said "up and down" of the same series three times, and they were right every time: the line was
drawing travel that never happened. Changing the primitive is the fix; restyling the line was not.
Worth noting the earlier passes were not wasted, they were just aimed at the wrong thing. Date spacing in
particular is subsumed here, since one slot per day IS date positioning for a bar chart.
Tests
+7. The allow-list is pinned in both directions, including that levels stay lines. The densifier is pinned
on the property that matters: every day in the window gets a slot, a missing day is NaN rather than a zero,
a real zero survives as a zero beside it, a complete stretch is unchanged, and an unparseable day refuses
rather than shuffling readings into the wrong slots.
676 classes / 5719 tests, 0 failures. Android-only; the iOS detail chart is Swift Charts and separate.