Repository navigation
iPhone: chart geometry and scrub rules in FTWShared (WP1) - #32
Conversation
The first step of the charts lane in the polish release. No view uses it yet: WP3, WP6 and WP7 draw with it. Under FTWShared/Charts, all Sendable values made by nonisolated code: - SlotGrid: finger x or a moment to a slot by arithmetic, clamped to the first and last slot; one binary search for the nearest live sample. - YDomain.stable: each side of zero ends on 1, 1.5, 2, 3, 5 or 7.5 times a power of ten, at least 10 % past its peak. It grows at once and shrinks only when the data fills less than 55 % of it. YMap maps values to screen. - DisplayBucket: the smallest of 5, 15, 30 or 60 minutes that leaves 2.5 pt a point, on the local clock. A bucket's mean is the exact mean of the five-minute means it holds, with their low, high and energy. - ChartPath and its builders: Steffen's monotone cubic and its body, rounded steps, Ramer-Douglas-Peucker, a rounded polyline that cuts a corner no deeper than 0.5 pt, the live trace (each step takes half of that budget), and equal slots merged into pixel-snapped runs. - ChartSeries.history: buckets, grid, domain, line, body, the point of each slot and a ReadoutTable (when, what with mean and range, prefix sums of energy, landmarks). - ScrubStep: a tick only when the slot changes, at most one per 40 ms; a landmark always gives an impact. - ZoneClock: one Calendar per zone, kept; "07:00" from minute-of-day arithmetic over a table. AppWords.clock, Readouts.seconds and ChargeNowActivity.clock use it, so none of them builds a Calendar or calls String(format:) per call. ChartGeometryTests proves the WP1 rules. The 720-point build takes about 0.09 ms optimised; the debug test run allows 20 ms, as it runs 20 to 60 times slower. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 754210154a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var low = [Double](repeating: .infinity, count: count), high = [Double](repeating: -.infinity, count: count) | ||
| var held = [Int](repeating: 0, count: count) | ||
| let whPerW = sampleMs / 3_600_000 | ||
| for s in samples where s.startMs >= fromMs && s.startMs < toMs { |
There was a problem hiding this comment.
Include samples overlapping the window start
When fromMs falls inside a five-minute sample, this filter drops that sample entirely even though HistoryModel.samples deliberately returns the overlapping first sample and the existing EnergyToday.sum counts only its covered portion. This occurs for the 48-hour view, whose fromMs is site.nowMs - spanMs and is generally not bucket-aligned, causing the first display bucket's mean/range and every prefix-energy readout to omit up to five minutes of known data; accept samples whose end exceeds fromMs and prorate their energy to the visible overlap.
Useful? React with 👍 / 👎.
| var out: [ChartBucket] = [] | ||
| out.reserveCapacity(count) | ||
| for k in 0..<count { | ||
| let start = origin + Double(k) * stepMs |
There was a problem hiding this comment.
Re-align buckets after non-hour DST transitions
In zones whose UTC offset changes by less than an hour, such as Australia/Lord_Howe, deriving every boundary by adding a fixed stepMs to the initial origin stops preserving local-clock alignment across the transition. With one-hour buckets on the 2026 fall-back day, starts become 00:00, 01:00, 01:30, 02:30, and so on, so subsequent samples are grouped into :30 buckets rather than local hours; each boundary needs calendar-aware alignment or re-alignment using the offset applicable at that boundary.
Useful? React with 👍 / 👎.
Problem
On 0.3.0 Fredrik found the charts "angular and ugly, not smooth at all", the lines too thin, and the scrub slow: "it lagged and felt sluggish when I dragged over the plan". The charts audit traced most of it to Swift Charts doing all the work on every touch. Each chart rebuilds 600 to 2,200 marks per touch, makes a
Calendarand callsString(format:)per readout, and draws hairy lines through raw five-minute noise.The polish release moves the charts onto a small engine: one Canvas for the chart and one for the finger. This PR is WP1, the engine's geometry with no UI. WP3, WP6 and WP7 draw with it.
Change
New, under
appleApp/FTWKit/Sources/FTWShared/Charts/. All values areSendable, and the code is nonisolated, so a model can build them off the main actor once per change of data.SlotGrid.swiftYDomain.swiftYDomain.stable: each side of zero ends on 1, 1.5, 2, 3, 5 or 7.5 × 10^k, at least 10 % past its peak. It grows at once and shrinks only below 55 %.YMapmaps values to screen.ChartSeries.swiftDisplayBucket: the smallest of 5, 15, 30 or 60 min that leaves 2.5 pt a point, on the local clock.ChartSeries.history: buckets, grid, domain, line, body, each slot's point and its readout table.ChartPaths.swiftChartPathas plain elements (Path(p.cgPath)draws it). Steffen's monotone cubic and its body, rounded steps, RDP, the rounded polyline, the live trace, and equal slots merged into pixel-snapped runs.ReadoutTable.swiftScrubStep.swiftZoneClock.swiftCalendarper zone, kept; "07:00" from minute-of-day arithmetic over a table.AppWords.clock,Readouts.secondsandChargeNowActivity.clocknow useZoneClock, so none of them builds aCalendaror callsString(format:)per call.Choices the design left open
ChartPaths.trace(RDP, then rounding) gives each step half of the 0.5 pt, so no live sample ends up more than 0.5 pt from the drawn line. If the corners look too sharp on a device, the 0.5 pt rule is the one to relax.0,6 kW, decimal comma) live in FTWKit. SoChartSeries.historytakes aReadoutWordsand keeps only the strings it made.ReadoutTable.swift, notReadouts.swift, since FTWKit already hasenum Readoutsand both reach the app.Evidence
ChartGeometryTests(18 tests) proves the WP1 rules:swift test -c release -Xswiftc -enable-testing): about 0.09 ms for the history series and 0.02 ms for the trace. Plainswift testbuilds without optimisation and runs 20 to 60 times slower beside other suites, so its bound there is 20 ms. That still catches a slip such as a loop inside a loop. Debug runs took 1.1 to 2.1 ms.Also covered:
ZoneClockmatchesCalendaracross the end of summer time in Stockholm, and hours start at :00 in India.swift test: 397 tests pass, three runs in a row; the geometry suite also passes in release.No screen changes, so there is no recording. WP3 is the first package that draws with this and needs one.
Next
WP3 builds
ChartSurface,ScrubOverlayandScrubModelonSlotGrid,ScrubStepandReadoutTable, and then the price chart. At that pointSlotLookupfrom #31 can give way toSlotGrid.🤖 Generated with Claude Code