Skip to content

iPhone: history and ahead on the engine, in the live trace's material, the chart small and growing (WP7) - #44

Merged
frahlg merged 2 commits into
mainfrom
claude/ios-wp7-history
Oct 14, 2026
Merged

frahlg merged 2 commits into
mainfrom
claude/ios-wp7-history

Conversation

@frahlg

@frahlg frahlg commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

WP7 of the iPhone polish release (Phosphor): smooth history and ahead on the engine, in the live trace's material, with Fredrik's note 16. Swift Charts leaves the node view.

Fredrik on 0.3.5, as acceptance

He liked how the live trace feels and wants every other chart to feel and look the same. Today and 48 h, small and enlarged, now share all of this with Live; Ahead shares the finger, the haptics, the slide and the type:

  • Line, body and band from one place, TraceInk: a 2.5 pt line in each direction's colour, a body from nothing at zero to 0.30 (dark) or 0.18 (light) at the scale's edge, a band at 0.14 or 0.10. The live trace draws through it too, and both take their colours from NodeChartLook.
  • Glow only while live. The past does not glow. On Today only the slot still filling glows, and only while the node is live, in the dark, without Reduce Transparency, as the live line does.
  • The same finger. LockOn: the rule, the dot in the colour of its side of zero inside its brackets, and the inverse tag (08:30–09:00 82 W, and 07:30–14:30 avg 522 W for two fingers). Nothing dims behind the finger, as on Live.
  • The same readouts. Under a finger the hero gives the slot's mean and word (82 W, 08:30–09:00 · giving power), and the line under the chart says ↑ 3.5 kWh ↓ 3.5 kWh so far, each from the same functions as on Live. Two fingers give their mean in the hero and the energy between under the chart.
  • The same haptics. A tick per slot; a light impact at the high, the low, now, and each end of a run of data, where Live has the edges of a silence (test).
  • The same slide. A tile or a plan that lands slides each slot to its new value over 0.25 s; a new scale, or a series coming or going, moves as one spring over 0.35 s, with Live's ease (SlidingSurface).
  • The same type. One ChartAxes for Live and history: zero as one device pixel, mono magnitudes in the gutter, the direction words.

What still differs is listed at the end, to queue.

Fredrik's note 16, as acceptance

"The history charts could be a bit smaller, and then you can enlarge them. And maybe customise them, adding and removing sources by tapping, so it feels more dynamic."

  • Compact first. The node view's chart is 150 to 230 pt, so the view never scrolls for it at the standard text sizes.
  • Tap to enlarge. A tap on the chart, or the enlarge glyph beside the node's state, opens it on the whole screen with the system zoom transition, growing out of the small chart. The same timebases, series, band and finger are there. The app is portrait only, so there is no landscape.
  • Sources by tap. A row of quiet chips (SOLAR · BATTERY · USAGE · GRID, and CAR on the car's own view) over Today and 48 h. A tap adds or takes away a series; the last one stays. The scale re-fits by the stable rules and moves as one spring, a new series fades in and an old one out. The choice is kept per viewer, per node, in the phone's defaults.
  • Honest still. Every series keeps its band, its holes and its gap rules. The line under the chart gives the energy so far, then each other series' mean; taking a series away changes nothing the others say (test).

What changes

Today and 48 h (research §4.2 and §4.3), HistoryPicture and HistoryDrawing:

  • Today on a fixed day from midnight to midnight, 25 hours on the day the clocks go back. The line grows through the day and the hours ahead stay empty, so neither the scale nor the slots move. 48 h ends on the five-minute step.
  • 30-minute slots for Today and 60 for 48 h on a phone (the 6 pt rule from WP3). Each slot is the exact mean of the box's five-minute means, drawn through Steffen; its band runs from the lowest to the highest five-minute mean. The node's own series has a body.
  • One stable scale across the chosen series, fitted to their bands, with the node's floor.
  • Holes say what they are: a hole on its way is a faint hatch with FETCHING… and a dashed straight estimate across it; a hole the box has no record for is a dotted baseline. Slots after now are neither.
  • AS OF 14:05 where the newest data ends, when it is more than a slot old.
  • Data from the cache starts settled. A new window draws at once, on the scale's spring.
  • The finger reads one slot at a time and stops at the last slot with data. Other series speak under the chart: ↑ 3.5 kWh ↓ 3.5 kWh so far · Solar 2,1 kW.
  • Axes in the Canvas: zero as one device pixel, the scale's ends in the right gutter, the direction words only with both signs, hours at 00, 06, 12 and 18, a weekday at each midnight on 48 h, and NOW under the now rule.

Ahead (design §7.6), AheadPicture and AheadDrawing:

  • The plan's quarters as ghost bars (a 0.14 fill with a 1 px cap at 0.7, equal neighbours merged, corners min(4, w/2)), the battery cyan storing above and gold giving below; the car's windows the same in violet.
  • The sun's and the home's forecast as a dashed line (1.75 pt, dash 3/4, 0.5 alpha).
  • The price under the same slots, in the same Canvas: block bars in eight steps, ink by thirds.
  • The finger lights a plan slot as its whole bar, as on the price chart.
  • A new plan or forecast for the same slots slides each slot to its new value (AheadPicture.blended, test).

Also:

  • ChartSeries gains holes on their way and on no record, the dashed estimate, the slot highs and lows, a given scale for several series, and blended(from:progress:) for the slide between tiles.
  • SeriesPick in FTWKit holds the chips' rule, with tests.
  • The car's Today and 48 h come from its hourly ledger on the engine, each hour's mean power; the box keeps no car tiles.
  • Gone: NodeCharts.swift, ChartMotion.swift, ChartScrub.swift, and the readouts only they used. HistoryPlot stays in HistoryModel.swift for now, because WP5 is rewriting that file. Swift Charts stays in PlanCard and the widgets.
  • A UI tour, HistoryTour (skipped unless asked), drives Live, Today, two series, enlarged, 48 h and back.
  • NowFitsTests taps Live before it looks for the live chart: the node view now opens on the timebase this phone chose last.

Evidence

Tests. swift test: FTWShared 160, FTWKit 297, all green. NowFitsTests 8/8 on an SE (3rd gen), iOS 26.5, on the last commit. HistoryTour passed on both phones in dark, light and the largest text, six runs.

With the Watch PR. #43 builds the watch app and its complications from FTWCharts, where this PR adds HistoryCharts.swift and TraceInk.swift. The two merge without conflict, and the merged tree builds FTWWatch for the watchOS simulator with both files in both targets.

New tests hold: a hole on its way is dashed and one with no record is not; slots after now are neither; series share one scale and a hidden one changes nothing the others say; each series keeps its band; Today is a fixed day, 25 hours when the clocks go back; AS OF appears only when data is a slot old; 48 h names each midnight; a tile that lands moves each slot between its old and new mean, and another window or scale does not morph; the plan is ghost slots and a forecast an estimated line; the price strip's thirds and eight steps; a chip never takes the last series away.

Calm on the home box's own days. Five-minute means of the Sungrow's battery, panels and grid meter on 12 and 13 October 2026 (from /api/series, read only; fixture sungrow-days.json, 32 KB), Today drawn as here (30-minute means) against 0.3.2 (every five-minute mean through a monotone curve), on 350 × 280 pt:

Series Day Turns per 100 pt 0.3.2 Ink per 100 pt 0.3.2
Battery 12 Oct 5.8 21.2 268 1 013
Battery 13 Oct 3.5 11.5 175 483
Panels 12 Oct 1.5 10.6 184 527
Panels 13 Oct 2.0 4.9 193 258
Grid meter 12 Oct 5.0 14.0 273 620
Grid meter 13 Oct 4.1 16.1 178 442

Every day holds the research's thresholds: at most 12 turns per 100 pt and under half of 0.3.2's; a test runs them in CI. The heat pump reports no power to the box, so there is no heat-pump trace of its own; its load is inside the grid meter.

Live and Today side by side, from the demo after five minutes of live trace: Live, Today, a finger on Live, a finger on Today, two fingers on Today. The figure, its word, the tag and the line under the chart change the same way on both.

Pro Max, dark:

material pm dark

SE, light:

material se light

Also Pro Max, light and SE, dark.

Screenshots from the demo on an iPhone SE (3rd gen) and a 17 Pro Max, iOS 26.5, taken by HistoryTour. Each strip runs Live eight seconds after launch, Today small, small with solar added, enlarged with grid added, enlarged on 48 h.

Pro Max, dark:

pm dark

Pro Max, light:

pm light

Pro Max, largest text:

pm ax5

SE, dark:

se dark

SE, light:

se light

SE, largest text:

se ax5

Ahead under a finger, the plan slot lit as its whole bar:

ahead finger

The battery's plan and the sun's forecast at rest, both phones, dark and light:

ahead

At the largest text the hero numbers, the chart's labels, the chips and the footer now stop at a set size, so the chart keeps its height. The caps sat inside the views before, where @ScaledMetric never saw them; they now sit where each view is placed. This also fixes the price hero from WP3 and the node hero from WP6.

Recording. HistoryTour on the 17 Pro Max in dark, 54 s: Live and a finger on it, Today and a finger on it, a tap on SOLAR, a tap on GRID, a finger, the zoom into the whole screen, a finger, 48 h, back to one series, and shut.

tour

What still differs, to queue

In this PR, on purpose:

  • The hours ahead keep their marks: a plan quarter is a ghost bar that lights as its whole bar under the finger, as on the price chart, and a forecast is a dashed line, because an estimate must not look measured. The hero stays on now there: a plan is not a reading.
  • A second series on Today or 48 h is a 2 pt line at 85 % with its band and no body, so the node's own reads on top. Live shows one series.
  • Live greys its line once the reading is not live; history keeps its colours, because it is the box's record.
  • Live's dot sits on the raw reading, inside the band; history's sits on the slot mean, on the line.
  • Live has no time axis, being the last five minutes; Today and 48 h have hours, the now rule and NOW.
  • The car's Today and 48 h are hour means, from its ledger, so they step by the hour.

Elsewhere, for later PRs:

  • Price chart (WP3): on the engine with the same LockOn, haptics and type, but a price is bars, not a trace, and tomorrow's prices draw at once, with no slide or spring.
  • Plan strip and lanes on Now (PlanCard, PlanLanes, WP10): still Swift Charts. No finger, no haptics, the system's type on the axes.
  • Widgets (GridDayChart, the plan widget, WP14): still Swift Charts, with the system's curve and type. A widget takes no finger and does not move, so line, body, band and type are what can match.

Departures from the design document

  • The chart is compact by Fredrik's note 16, with the large chart one tap away.
  • No landscape: the app is portrait only.
  • The widget's band waits for WP14, as decided (D).
  • No trend line, as decided (C).

🤖 Generated with Claude Code

…(WP7)

Today and 48 h draw from HistoryPicture: a fixed day with 30-minute
slots, 48 h with 60-minute slots, each slot the mean of the box's
five-minute means through Steffen, with its band. One stable scale for
the chosen series. Holes on their way are hatched with a dashed
estimate; holes with no record are a dotted baseline. AS OF marks old
data. A tile that lands moves each slot to its new mean.

Ahead draws from AheadPicture: plan quarters as ghost bars, forecasts
as a dashed line, the price strip under the same slots.

Fredrik's note 16: the chart is compact and a tap zooms it to the
whole screen. Chips add or take away series, kept per viewer per node;
the last one stays and a hidden series changes nothing the others say.
The scale re-fits as one spring.

Size caps for the largest text move to where each view is placed, so
@ScaledMetric sees them; this fixes the price and node heroes too.

NodeCharts, ChartMotion and ChartScrub are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T11:15:07.669451Z eed9e01 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eed9e01ffa

ℹ️ 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".

Comment on lines +353 to +354
guard !bars.isEmpty || node == .car else { return nil }
let prices = (window?.slots ?? []).map { ChartPrice(startMs: $0.startMs, endMs: $0.startMs + $0.durationMs, minor: $0.totalMinor) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve prices embedded in plan and forecast slots

When the box supplies PlanSlot.priceMinor or Forecast.priceMinor but does not expose the separate spot-price capability—or that request has not completed—window is nil and this now passes no prices to AheadPicture. The previous implementation used the prices embedded in those slots, so affected battery, grid, solar, and home Ahead charts lose both the price strip and price readouts despite having that data; use the plan/forecast prices as a fallback.

Useful? React with 👍 / 👎.

Comment on lines +182 to +184
for n in series {
let samples = Self.samples(n, home: home, fromMs: window.fromMs)
if !samples.isEmpty || n == node { inputs.append((n.rawValue, samples)) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the unsupported-history state visible

When a connected box lacks history.5m, HistoryModel.available is false and no frame or problem is produced, but this unconditional inclusion of the node with an empty sample array still creates a non-nil HistoryPicture. Consequently the chart branch always renders an all-missing graph and the previous “Your box keeps no history.” state disappears entirely; gate picture construction on history availability and show the unsupported message instead.

Useful? React with 👍 / 👎.

Comment on lines +179 to +180
let now = site.minuteMs + 60_000
let window = period == .today ? HistoryPicture.day(now, clock: clock) : HistoryPicture.twoDays(now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive Today from the current minute, not its end

During every day's 23:59 minute, adding 60 seconds makes now exactly midnight of the following day, so HistoryPicture.day switches the Today window early and the current day's chart becomes empty for up to a minute. Keep the day selection based on site.minuteMs (while using a separate end-of-minute value if needed for the data limit).

Useful? React with 👍 / 👎.

Comment on lines +243 to +244
let series = AXDataSeriesDescriptor(name: node.title, isContinuous: true, dataPoints: buckets.compactMap { b in
b.mean.map { AXDataPoint(x: b.startMs, y: $0, additionalValues: [], label: Readouts.word(node, $0)) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Describe the selected primary series to VoiceOver

If the user deselects the viewed node's own series, SeriesPick allows another node to become picture.layers.first, but this descriptor still names and interprets those buckets using the original node. For example, solar values on a battery page are announced as Battery with charging/giving labels; derive the series name and Readouts.word node from the first layer's key.

Useful? React with 👍 / 👎.

Comment on lines +568 to +569
.opacity(timebase == .today || timebase == .twoDays ? 1 : 0)
.allowsHitTesting(timebase == .today || timebase == .twoDays)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide inactive full-screen series controls from accessibility

In the enlarged Live and Ahead views these chip buttons are only made transparent and non-interactive; unlike the compact view, this copy does not apply accessibilityHidden. VoiceOver therefore still navigates and announces invisible series controls on timebases where they do not apply.

Useful? React with 👍 / 👎.

Fredrik on 0.3.5: the live view feels right, so every other chart should
feel and look the same. Live, Today and 48 h, small and enlarged, now
draw through one place, TraceInk: the same band, body, 2.5 pt line, and
glow only while live (on Today, only the slot still filling). Live uses
ChartAxes and NodeChartLook like the rest.

SlidingSurface gives history and Ahead Live's motion: a change for the
same slots slides over 0.25 s, a new scale or a series coming or going
springs over 0.35 s, with the same ease. AheadPicture.blended moves a
new plan or forecast slot by slot.

Under a finger the hero follows on Today and 48 h, as on Live, and the
line under the chart says the energy so far or between, from the same
functions. The dot takes its side's colour, the span tag gives the
average, and nothing dims behind the finger. History's landmarks are the
high, the low, now and each end of a run of data, as on Live.

The debug finger reads the newest track; HistoryTour starts on Live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg frahlg changed the title iPhone: history and ahead on the engine, the chart small and growing (WP7) iPhone: history and ahead on the engine, in the live trace's material, the chart small and growing (WP7) Oct 14, 2026
@frahlg
frahlg merged commit 1d65dc5 into main Oct 14, 2026
1 check passed
frahlg added a commit that referenced this pull request Oct 14, 2026
…complications (W1) (#43)

* Apple Watch: Now, the plan dial on the Crown, the car, the price and complications (W1)

A watch app and its complications, reading and commanding through the
iPhone. watchOS gives an ordinary app no WebSocket (TN3135), so the
phone hands the watch the records its widgets read after each cache
write, sends Now's frames while the watch looks, and runs each command
over its own session. FTW Cloud never controls.

- FTWWatch: Now (lit only while a live frame is fresh, ink with its
  time otherwise), the plan dial with the Crown stepping by quarter
  hour, the car with Charge now, Pause and Back to the plan, the price,
  and the mode. Each command shows SENT ▸ ACCEPTED ▸ CONFIRMED from the
  box's answers.
- FTWWatchWidgets: the dial as accessoryCircular, plus rectangular,
  corner and inline, and a RelevanceConfiguration card for the
  cheapest hours.
- Built to stand alone: its own cache, its own key, its own cmdId, and
  two seams (ReadPath, CommandPath). PhonePath is the only path today.
- Phone side: WatchBridge in the app, WatchShare in FTWKit, the wire in
  FTWShared/Watch. Session.command takes the watch's cmdId.
- Each colour set gains a watch variant equal to its dark one, since
  watchOS compiles no dark appearance; ThemeTests holds them equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* iPhone: history and ahead on the engine, in the live trace's material, the chart small and growing (WP7) (#44)

* iPhone: history and ahead on the engine, the chart small and growing (WP7)

Today and 48 h draw from HistoryPicture: a fixed day with 30-minute
slots, 48 h with 60-minute slots, each slot the mean of the box's
five-minute means through Steffen, with its band. One stable scale for
the chosen series. Holes on their way are hatched with a dashed
estimate; holes with no record are a dotted baseline. AS OF marks old
data. A tile that lands moves each slot to its new mean.

Ahead draws from AheadPicture: plan quarters as ghost bars, forecasts
as a dashed line, the price strip under the same slots.

Fredrik's note 16: the chart is compact and a tap zooms it to the
whole screen. Chips add or take away series, kept per viewer per node;
the last one stays and a hidden series changes nothing the others say.
The scale re-fits as one spring.

Size caps for the largest text move to where each view is placed, so
@ScaledMetric sees them; this fixes the price and node heroes too.

NodeCharts, ChartMotion and ChartScrub are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* iPhone: every power trace in the live trace's material (WP7)

Fredrik on 0.3.5: the live view feels right, so every other chart should
feel and look the same. Live, Today and 48 h, small and enlarged, now
draw through one place, TraceInk: the same band, body, 2.5 pt line, and
glow only while live (on Today, only the slot still filling). Live uses
ChartAxes and NodeChartLook like the rest.

SlidingSurface gives history and Ahead Live's motion: a change for the
same slots slides over 0.25 s, a new scale or a series coming or going
springs over 0.35 s, with the same ease. AheadPicture.blended moves a
new plan or forecast slot by slot.

Under a finger the hero follows on Today and 48 h, as on Live, and the
line under the chart says the energy so far or between, from the same
functions. The dot takes its side's colour, the span tag gives the
average, and nothing dims behind the finger. History's landmarks are the
high, the low, now and each end of a run of data, as on Live.

The debug finger reads the newest track; HistoryTour starts on Live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg frahlg mentioned this pull request Oct 14, 2026
frahlg added a commit that referenced this pull request Oct 14, 2026
WP7 (#44) replaced the node view's Swift Charts with its own surfaces and
deleted ChartMotion.swift, so this branch's change to MovingPoints goes
with it. WP7's SlidingSurface already starts settled when it appears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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