Squirreleye LLC Squirreleye Service Notes Naked Labs Mirror · Revival Series Index Tools About
20
Section 20 · follows 19

Sparklines on KEY STATS, L/R-split cards on MEASURE

Written
2026-05-17

Third phase, smaller scope. Two specific changes to the Body tab’s sub-tabs, both directly traceable to APK item layouts:

  1. KEY STATS cards get an inline sparkline, per item_side_body_model_card_charts.xml.
  2. MEASURE cards consolidate symmetric pairs (left/right arms, thighs, calves) into a single split card per item_doble_bottom_body_model_card.xml, and add a derived waist-to-hip ratio card per item_waist_to_hip.xml.
§ 20.1

KEY STATS sparkline

The XML name gave it away: there’s both item_side_body_model_card.xml (plain stat card) and item_side_body_model_card_charts.xml (stat card with inline chart). The APK uses the chart variant when there’s enough scan history to render one; otherwise the plain variant. The chart is small, embedded inside the card, no axes, no labels, just a line showing trajectory.

In SwiftUI with Swift Charts, that’s just:

Fig. 20-1
swift
Chart { ForEach(Array(values.enumerated()), id: \.offset) { i, v in LineMark(x: .value("i", i), y: .value("v", v)) .interpolationMethod(.catmullRom) .foregroundStyle(AppTheme.accent) }}.chartXAxis(.hidden).chartYAxis(.hidden).chartLegend(.hidden).frame(height: 18)

The KEY STATS section pulls scan history via @Query(sort: \Scan.createdAt, order: .forward) so each card sees its values in chronological order. Each card filters to its specific metric (history.compactMap { $0.hasBodyFat ? $0.bodyFatRatio : nil }) and renders the sparkline only when there are at least 2 data points. A single dot is noise, not signal; if you only have one scan, the sparkline area on the card is just unused vertical space.

The sparkline color is AppTheme.accent (#ff0040), same hot pink-red as the sub-tab indicator. Consistency: anything that “highlights” or “indicates trend” uses accent; anything that’s a value or label uses one of the dark/white card colors.

§ 20.2

MEASURE pair cards

The original iOS implementation rendered 13 horizontally-scrolling cards on MEASURE, one per BodyPart. That’s faithful to the underlying data (every region is its own measurement) but not faithful to the APK’s presentation. The APK has 4 symmetric pairs that share a card each:

  • UPPER ARM (upperArmLeft + upperArmRight)
  • UPPER THIGH (upperThighLeft + upperThighRight)
  • LOWER THIGH (lowerThighLeft + lowerThighRight)
  • CALF (calfLeft + calfRight)

Plus 5 non-symmetric regions (SHOULDER, CHEST, WAIST, HIPS, STOMACH) and the derived WAIST-TO-HIP ratio. 5 + 4 + 1 = 10 cards instead of 13 + 1 = 14: slightly less horizontal scrolling, but more importantly each pair card shows both values side-by-side, which is how the user thinks about that part of their body. “Left upper arm is 35.2 cm” + “Right upper arm is 35.0 cm” reads better as one card with two columns than as two separate cards a swipe apart.

The card design is a small L/R split:

┌────────────────────────┐│ UPPER ARM ││ L R ││ 35.2 cm 35.0 cm │└────────────────────────┘

Tapping cycles through three states: nothing selected → left highlighted → right highlighted → nothing. The avatar’s measurement ring updates with each tap, so the user can see the L and R rings independently.

Fig. 20-2
stateDiagram-v2
    [*] --> None
    None --> Left: tap
    Left --> Right: tap
    Right --> None: tap

This is more complete than the single-card behavior of “both rings always visible”: the user actually wants to know which side is which when comparing asymmetry.

§ 20.3

Waist-to-hip ratio card

Strictly a derived stat (waist_circumference / hip_circumference). Shown only when both base measurements are available; tapping the card focuses the waist ring (since the waist is the more modifiable input of the ratio for most users). Format: String(format: "%.2f", ratio), two decimals, unitless.

The APK has item_waist_to_hip.xml and layout_waist_to_hip_description.xml (a longer descriptive surface, probably reached via an “info” tap). The iOS port has the card but not the description sheet yet; that’s Phase C+ polish if it turns out to matter.

§ 20.4

Card ordering

The APK orders the MEASURE cards from top-of-body downward: shoulder, chest, waist, hips, stomach, then arms, then thighs, then calves. iOS now matches that. Waist-to-hip slots in right after hips since it’s anatomically that region.

§ 20.5

What this commit does NOT do

  • No expanded card view. The APK has item_bottom_body_model_card_expanded.xml and item_bottom_double_card_expanded.xml for a tap-to-expand-with-history-graph pattern. Skipping for now: the sparkline on KEY STATS already covers “see the trajectory at a glance,” and the History tab covers “see the full chart.” An expanded variant on MEASURE cards would be a third surface for the same information, low priority.
  • No animation between symmetric L/R toggling. Tap cycles L → R → off, but the ring change is immediate. The MEASURE camera animation I already had (0.6s ease into region centroid) still runs.
  • Sparkline doesn’t show the current scan’s position. Just the line. The APK probably draws a dot for the currently-selected scan; that’s a small enough addition to defer.

Phases A, B, C are in. D is the scan-flow polish: replacing the bare ScanFlowView with the APK’s staged sequence (searching → connecting → scale-align tip → warming up → phone down → countdown → scanning → step off → success/error). That one is real work, about 15 layout XMLs to read through.

§ 20.6
§ 20.7
Agent prompts Procedure 20-A as prompts for a coding agent · 6 prompts

Paste into Claude Code or a similar agent, one at a time, from the root of your iOS project. Each prompt maps to the step with the same number above.

1
Gate chart variant on data availability
Check the decoded APK's layout resources for a plain and a chart-augmented variant of the same card (here, `item_side_body_model_card.xml` vs `item_side_body_model_card_charts.xml`). Use the presence of this naming pattern as the feature-gate signal in your SwiftUI port: render the chart variant only when there's enough scan history to plot, and fall back to the plain stat card otherwise.
2
Guard sparkline on minimum data points
In the KEY STATS sparkline view, pull chronological values via something like `@Query(sort: \Scan.createdAt, order: .forward)` filtered to the specific metric, and only render the Swift Charts `LineMark` sparkline when there are at least 2 data points. With fewer than 2 points, show the plain card layout (no empty chart area).
3
Collapse symmetric L/R cards
Consolidate the MEASURE sub-tab's symmetric body-region cards (upper arm, upper thigh, lower thigh, calf — left and right each) from separate per-region cards into one card per pair with two columns (L value, R value side by side), matching the APK's `item_doble_bottom_body_model_card.xml`. Implement a three-state tap cycle on each pair card — none selected → left highlighted → right highlighted → none — where the current state drives which side's measurement ring is shown on the 3D avatar.
4
Add derived waist-to-hip card
Add a waist-to-hip ratio card to the MEASURE sub-tab, computed as `waist_circumference / hip_circumference` (matching the APK's `item_waist_to_hip.xml`), displayed with two decimal places and no unit (`String(format: "%.2f", ratio)`). Show the card only when both waist and hip measurements are available on the scan, and route a tap on this card to focus the waist measurement ring on the avatar (the more actionable of the two inputs), not the hip ring.
5
Match APK's card ordering
Read the decoded APK's layout/adapter source to determine the MEASURE card display order (here: shoulder, chest, waist, hips, stomach, then the arm/thigh/calf pair cards, then waist-to-hip immediately after hips) and reorder the SwiftUI card list to match exactly, rather than keeping whatever ad hoc order currently exists.
6
Record deferred polish items
Add a note (comment or commit message) listing the UI behaviors you deliberately did not implement in this pass — e.g. no expanded-card-with-history-graph variant, no animation on L/R toggle beyond the existing camera ease, no current-scan marker dot on the sparkline — so a future phase starts from an accurate gap list.