Repository navigation
iPhone charts: a tap enlarges, the edge goes back, no empty plot (UX pass 3, U4) - #71
Conversation
…Today's ghost, the forecast's curve (U4, shared logic) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecides, Today's ghost, the price at half the screen (U4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ring scrolls (U4) Any gesture on the ring held the page still, even beside the scroll. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e back swipe (U4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hart's pictures (U4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…creen; the spare sits above its lines (U4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an ellipsis; the ring test skips where the plan fits (U4) 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: 42f800c6ca
ℹ️ 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".
| guard b > a else { continue } | ||
| let prices = (0..<grid.count).filter { grid.startMs($0) < b && grid.endMs($0) > a }.compactMap { minor[$0] } | ||
| guard let lo = prices.min(), let hi = prices.max() else { continue } | ||
| func whole(_ m: Double) -> String { String(Int(PriceUnits.display(max(0, m), currency).rounded())) } |
There was a problem hiding this comment.
Preserve below-zero prices in the plan summary
When a charge or discharge run overlaps a negative tariff, max(0, m) converts every negative price to zero before building the displayed range, so a −30 öre slot is reported as 0 and a −30…20 range as 0–20. This makes the new plan summary misstate the prices that drove the plan; preserve the negative value or use the existing below-zero price presentation rather than clamping it.
Useful? React with 👍 / 👎.
Package U4 from the third UX review (
design/ftw-ios-polish-2026-10/ux-pass-3/ux-pass-3.md, section 6: findings 6, 7, 11 (the price view), 16 (the car's scale), 20, 21 (the plot), 25, 45 and 53). It builds on U1 (#64) and U2 (#65) and runs beside U3, which owns the watch; it touches no watch file.A tap on a chart lit the lock-on and ticked before the chart opened. A back swipe from the screen's edge over a chart, the dial or the style scale scrubbed instead of going back. A swipe up that started on the plan dial's ring held the page still. Since U2 made the node chart tall, a 446 W battery drew a line near zero over an empty plot, and Today stopped at now although the plan and forecast know the rest of the day.
What changes
A tap is a tap.
ScrubOverlayand the live trace's overlay own the tap (FingerGate, ruleTouchIntentin FTWShared). A touch that lifts before it moves 6 pt and before 150 ms callsonTapand never touches the scrub model, the hero or the haptics; the first scrub move waits for either. A second finger is a span at once.NodeViewpassesonTapand drops its simultaneousTapGesture.The edge goes back. The overlays and the style scale's track take no touch that starts within 20 pt of the screen's leading edge (
EdgeRule,leavesTheEdge()). A finger that starts further in and slides past the end still has the first slot, clamped.The dial ring. It scrubs on a move along the ring that is not mostly up or down, or after the finger rests 0.1 s (
RingIntent); a move up or down, or across the ring, scrolls the plan, and while the ring scrubs the page holds still (.scrollDisabled). At three and nine o'clock, where along the ring is up and down, the page wins and a rest starts the scrub.The live scale comes from its own five minutes (
LiveSeries.domain): each side ends on nice(1.1 × its peak), a side exists only where the window goes past rest, so one sign keeps zero at the edge. A source at rest (every reading withinFlow.idleW, 42 W; the home never rests) puts zero mid-plot and draws the dotted baseline under its readings in place of the solid zero, with the head on the last reading. The watch passes no rest and keeps its floor.Today draws the rest of the day as the ghost Ahead draws (
HistoryPicture.Ghost): the battery's and the grid's plan as faint bars (fill 0.14, a 1 px cap), the car's windows the same, the sun's and the home's forecast as a dashed line, from now to midnight on the fixed 00–24 axis. The scale holds the ghost too, so the day reads as one picture. The finger still stops at the last measured slot: a plan is not a reading.Ahead. The price strip is flat ink at 0.32 and 20 pt, Now's price row at its least, so it no longer speaks louder than the plan. A forecast runs through its own slots' middles on the Steffen path (
AheadPicture.marks), so an hourly forecast on a grid of quarters bends instead of stepping.Axes. The right gutter holds the widest label of the scale on screen (
ChartGutter, SF Mono's advance of 0.62 of its size), so "20 kW" and "15 kW" are whole on the SE. The direction words show only where no data runs under them (CornerWords).The car's scale on Ahead is the charger's most (
maxChargeW, 11 kW in the demo), with a plan or without one, so it no longer flips between 15 and 5 kW.The price view. The chart takes what the rest of the page leaves, from 240 pt up to 0.55 of the screen's height; what is left sits between the chart and its lines, never at the foot (as U2's node view). Under "incl. VAT" one mono line says what the plan already read does at these prices:
GIVES 19:02–21:15 AT 105–173 · STORES 23:00–01:30 AT 30–55 ÖRE(SwedishGER … VID …,LAGRAR … VID …). Where the line has no room (the SE at XXXL) it drops its second part, never ends in "…". No plan, no line.PriceBarsdraws its time axis withChartAxes.time.Where this departs from the review, and why
DragGesturefrom 0 pt, the same from 6 pt beside the scroll, a 0.1 sLongPressGesturebefore a drag, the empty-tap trick andSpatialEventGesture. A drag that waits 20 pt let the page scroll but never fired, even sideways. Only a gesture on theScrollViewitself received every touch while the page still scrolled. SoPlanViewholds oneDragGesturebeside its scroll and hands each touch to the dial (DialTouch), which asks whether it started on the ring. That gesture also leaves the screen's leading 20 pt alone: without that it stopped the back swipe on the whole plan view.PlanView.swiftis U7's file; this adds a binding, the gesture and one line, nothing else.-FTWDemoBatteryW 450, holds the demo battery near a power with a few watts of noise, for the 450 W pictures.Evidence
Demo, iOS 26.5 simulators, iPhone SE (3rd generation) and iPhone 17 Pro Max, English. Pictures and logs are on
claude/ios-u4-shots(u4/).Tests.
swift teston this branch over main c26af60 (#67): FTWShared 240 and FTWKit 426 tests pass (new:ChartFingerTests, 14 tests), and the app builds. UI tests, over main ea69097 (#67 changed the session and carriers, no chart or view file): on the SE, the final commits pass 19 of 19 (NowFitsTests17,ChartFingerTests2); on the Pro Max,NowFitsTests17 of 17 one commit earlier (before the plan line's short form) andChartFingerTests1 passed, 1 skipped (the whole plan fits a Pro Max, so nothing scrolls; the test checks that with a swipe under the dial and skips).testTheStyleScaleSendsOnceWhenTheFingerLetsGo, which U1 and U2 saw fail on main, passed in all three runs here.The tap.
ChartFingerTests.testATapOnEachChartEnlargesItAndADragOnlyScrubsdrags across Live, Today and Ahead (nothing opens) and taps each (the chart opens on the whole screen). The scrub log of that run: each drag logsscrub starts: moved 6.5 ptand its slots; each tap logstap: lifted after 65–76 ms, moved 0.0 pt, no scruband notouching index. Recording · logThe edge, in the simulator. The simulator control tool's edge swipe (the OS edge gesture, from x = 1) over the battery's Live chart: before, it scrubbed (slots 0 → 20) and stayed; after, it goes back to Now with no scrub line logged. The same swipe goes back over the price chart, the car's plan and the style scale (which stays on Balanced), and over the plan view's text.
before
after 
The dial.
testTheDialRingScrollsUpAndDownAndHoldsThePageWhileItScrubson the SE: a rest at one o'clock then round to four scrubs with the page still; a quick move along the top scrubs with the page still; a swipe up from nine o'clock scrolls the plan. Its log:dial rested 100 ms · dial scrubs, thendial decides after 46 ms on a move of 0.0, -9.5 pt · dial passes the touch to the page.bands.py.Frames, in the simulator. The debug probe (
-FTWFrameLog YES -FTWAutoScrub YES) on the SE, ten seconds of a fast thumb then a two-finger span: the price drew its surface 0 times while 23–97 slots a second changed, with no late frame in 9 of those 11 seconds and 25–28 ms late in the other two; 48 h the same, 0 surface draws, no late frame in 9 of 11 seconds and about 51 ms in two. Other sessions loaded the laptop at the time.Before and after
Each picture: before (main 0888d8c, U1 and U2 merged) on the left, after on the right. Today, Live and the price in dark, light and the largest standard size; the rest in dark.
SE, Today (battery with the sun and the home)
Pro Max, Today
SE, the price
Pro Max, the price
SE, a source at rest (the car) and the grid's whole "20 kW"
SE, the car's Ahead on its charger's 11 kW
Every screen:
The demo moves with the clock: the before and after of one screen are minutes apart, so a figure may differ. In the evening the demo battery gives about 2.3 kW, so the battery's Live sheets show both on 3 kW; the 450 W comparison is above. The sun was down during every shot, so the forecast's new curve shows in the unit test, not on screen.
Not checked
todayAtSixShowsTheWholeDayInOnePicturebuilds that morning: one scale for the measured night and the planned day, the plan's ghost from the 06:00 slot, the sun's forecast as a line, the finger stopping at the last measured slot.🤖 Generated with Claude Code