KMPCharts/.paul/phases/v0.5.0-architecture-plan.md
Trentin Davide af6814e2b8 feat(chart): v0.5.0 architecture + interaction
Ships v0.5.0 via kmp-manager 6-phase flow. 13 plan tasks (D1, T1-T10, D3, D4),
14 agent calls, 6 P-groups. 107 → 207 tests on iosSimulatorArm64.

Toolchain (D1):
- Kotlin 2.1.0 → 2.3.21
- Compose-Multiplatform 1.8.0 → 1.11.0
- Android Gradle Plugin 8.7.3 → 9.2.0
- Gradle 8.11.1 → 9.5.1
- coroutines 1.9.0 → 1.11.0, kotlin-test → 2.3.21
- Drop kotlinx-datetime; use stdlib kotlin.time.Clock
- Drop iosX64 target (Compose-MP 1.11.0 has no ios_x64 variant)

Architecture (T1-T6, T10):
- TieredBuffer.snapshotWindow: bisect-based windowed snapshot
- New lod/ package: LodStrategy interface + MinMax/Lttb/MinMaxLttb impls
  (MinMaxLttb SOTA per arXiv 2305.00332, 1.80× faster than pure LTTB)
- New render/SignalRenderer: public interface + LineSignalRenderer object
- New render/AxisFormatter: 4 default impls (Time, Decimal, DateTime, Unit)
- HARD BREAK: deleted LodMode, LodDecimator, ChartConfig.targetFps
- ChartConfig split: DataConfig + AxisConfig + RenderConfig + FrameRate sealed
- @Immutable/@Stable on all public types (0 unstable)
- RealtimeChartState.clear() API

Interaction layer (T7):
- New interaction/ package
- ChartInteractionState + rememberChartInteractionState()
- ViewportMode sealed: Following / Frozen / History(anchorMs)
- Pinch zoom + drag pan + tap crosshair gestures
- Swipe-to-edge resumes Following
- InverseProjection: pixel → ms + bisect nearest-sample

Perf finishing (T8, T9, D3):
- LineSignalRenderer Stroke cache, AxisRenderer TextStyle cache
- resolveYRange Pair<Float,Float> → FloatArray out-param
- RealtimeChartState.signalsArray cached (invalidated on add/remove only)
- LTTB upper-bound aligned to half-open [start, start+windowMs) semantic

Correctness (D4):
- NumberFormat.formatFixed Long overflow guard @ |v|≥1e19

ABI baseline regenerated:
- chart-realtime.api: 161 → 428 LOC
- chart-realtime.klib.api: 211 → 501 LOC

Modules touched: chart-realtime (lib), app (consumer), gradle (toolchain),
.paul (state).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 00:05:43 +02:00

236 lines
13 KiB
Markdown

# v0.5.0 — Architecture + Interaction Plan
## Source
mobile-architect verdict 2026-05-21: "Library is a closed renderer with single skin. Anything beyond first-party use case requires forking." Senza interaction layer "no senior reviewer will take a real-time chart claim seriously."
## Goal
Aprire estensibilità + aggiungere interaction layer (S0 per portfolio). MinMaxLTTB SOTA algorithm. Config split. Bisect tier snapshot per perf.
## Prereq
v0.4.0 shipped (hygiene + correctness).
## Scope IN
- Interaction layer: pinch zoom, drag pan, crosshair, tap-to-inspect
- Renderer/LodStrategy/AxisFormatter interfaces
- MinMaxLTTB algorithm (replaces both MIN_MAX and LTTB attuali)
- Bisect-based windowed snapshot (30x perf win)
- ChartConfig split DataConfig/AxisConfig/RenderConfig
- Compose stability annotations (@Stable, @Immutable)
## Scope OUT
- Multi-Y axis / log scale → v0.6.0+
- Annotations / threshold lines → v0.6.0+
- Headless data export → backlog
---
## Tasks
### T1 — Bisect tier snapshot (perf S0)
- Files: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/CircularBuffer.kt`, `TieredBuffer.kt`
- Goal: `snapshotWindow(startMs, endMs, outTs, outV): Int` bisect su timestamps monotonici. Cuts 30x copies su 10s window in 5min buffer.
- Agent: data-impl + mobile-performance-reviewer review
- Verify: macrobench frame time -50% addizionale post v0.4.0 T11
### T2 — LodStrategy interface + MinMaxLTTB impl
- New files:
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/LodStrategy.kt`
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/MinMaxLttb.kt`
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/MinMaxLod.kt`
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/LttbLod.kt`
- Move + rename: `buffer/LodDecimator.kt``lod/LodDecimator.kt` (delegate to strategy)
- Goal: interface `LodStrategy { fun decimate(...): Int }`. Default = `MinMaxLttb` (10x faster than LTTB puro per arXiv 2305.00332).
- API change: `LodMode` enum → `LodStrategy` interface. `ChartConfig.lodMode: LodMode``lodStrategy: LodStrategy`. Backward compat: `LodMode.MIN_MAX_LTTB` shorthand.
- Agent: kmp-architect + data-impl
- Verify: nuovi test `MinMaxLttbVisualFidelityTest`, perf ≥ MIN_MAX puro su large datasets
- Pattern: [MinMaxLTTB arXiv 2305.00332](https://arxiv.org/pdf/2305.00332), [Rust ref impl](https://github.com/andrei-ng/minmaxlttb-rs)
### T3 — SignalRenderer interface
- New file: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/SignalRenderer.kt` (interface)
- Move impl → `LineSignalRenderer.kt`
- Goal: pluggable rendering. Future-proof per `ScatterRenderer`, `AreaRenderer`, `BarRenderer`. Default `LineSignalRenderer`.
- `SignalConfig.renderer: SignalRenderer = LineSignalRenderer` (per-signal opt-in)
- Agent: kmp-architect
- Verify: existing rendering unchanged, plus `customRendererPlugsIn` test
### T4 — AxisFormatter interface
- New file: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisFormatter.kt`
- Default impls: `TimeAxisFormatter` (current behavior), `DecimalAxisFormatter`, `DateTimeAxisFormatter`, `UnitAxisFormatter("mV", "°C", "Hz")`.
- `AxisConfig.xFormatter: AxisFormatter = TimeAxisFormatter`, `yFormatter: AxisFormatter = DecimalAxisFormatter`
- Agent: kmp-architect + ui-impl
- Verify: user can pass `UnitAxisFormatter("ms")` and labels show "5 ms"
### T5 — Split ChartConfig → DataConfig + AxisConfig + RenderConfig
- File: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/ChartConfig.kt`
- New: `DataConfig`, `AxisConfig`, `RenderConfig`. `ChartConfig` = wrapper o builder DSL.
- `targetFps: Int?``sealed class FrameRate { object Display; data class Fixed(val fps: Int) }`
- Mark old `ChartConfig` ctor `@Deprecated(level = WARNING)` con `ReplaceWith`.
- Agent: kmp-architect
- Verify: README example updated, compile green con deprecation warning
### T6 — Compose stability annotations
- Files (tutti i data class in model/):
- `ChartConfig.kt`, `SignalConfig.kt`, `ChartTheme.kt`, `T0.kt`, `YRange.kt`, `LodMode.kt`, `AxisLabelMode.kt`
- `RealtimeChartState.kt`
- Action: `@Immutable` su data class + sealed root. `@Stable` su `RealtimeChartState`.
- Agent: compose-guide
- Verify: Compose compiler report `chart-realtime` mostra 0 unstable classes nel public API. `./gradlew :chart-realtime:compileReleaseKotlinAndroid -Pkotlinx.compose.report` etc.
### T7 — Interaction layer S0 (touch/zoom/pan/crosshair)
- New files:
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/interaction/ChartInteractionState.kt`
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/interaction/InteractionConfig.kt`
- Modify: `RealtimeChart.kt` aggiunge `Modifier.pointerInput` con `detectTransformGestures` (pinch) + `detectDragGestures` (pan) + `detectTapGestures` (crosshair toggle).
- State:
- `viewportOffsetMs: Long` — pan offset history
- `viewportScale: Float` — zoom factor on xWindowSeconds
- `mode: ViewportMode { Following, Frozen, History(offsetMs) }`
- `crosshair: CrosshairState?` — pixel position + projected (timestampMs, value) per ogni signal
- Inverse projection helper (pixel → data) per tooltip
- Agent: ui-impl + compose-guide
- Verify:
- pinch zoom restringe/allarga xWindowSeconds
- drag pan sposta viewport (mode Frozen automatically)
- tap mostra crosshair con valori istantanei
- tap su crosshair attivo → dismiss
- swipe a destra estrema → mode Following (auto-scroll resumes)
- New tests: `ChartInteractionStateTest`, integration test composable
### T8 — Cache Stroke + TextStyle + TextLayoutResult per zero-alloc reale
- Files:
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/SignalRenderer.kt:50``Stroke` cached per signal
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:54-131``TextStyle` hoisted in `remember`, label `TextLayoutResult` cached via `remember(label, style)`
- Action: introduce `internal class FrameCaches` holding stroke per signal + label LRU.
- Agent: compose-guide + mobile-performance-reviewer
- Verify: allocation tracker show < 1 KB/s steady-state at 30fps, 4 signals
- Pattern: [TextMeasurer LRU - composables.com](https://composables.com/jetpack-compose/androidx.compose.ui/ui-text/classes/TextMeasurer/api), [Modifier.drawWithCache - Android Devs](https://developer.android.com/develop/ui/compose/graphics/draw/overview)
### T9 — Replace Pair<Float,Float> return + Map iteration alloc
- Files:
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:144-152` `resolveYRange` riempie `FloatArray(2)` out param
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt:70,79,93` Map iter via cached `signalsArray`
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt` invalidate signalsArray su addSignal/removeSignal
- Agent: mobile-performance-reviewer + data-impl
- Verify: Map iter allocation count = 0 in macrobench
### T10 — clear() / reset() su RealtimeChartState
- File: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt`
- Goal: `fun clear()` purge tutti i buffer + reset dataVersion + reset resolvedT0Ms. Public API.
- Agent: data-impl
- Verify: test `clearResetsState`
---
## Acceptance v0.5.0
- A1: `LodStrategy` interface + `MinMaxLttb` default, perf MIN_MAX puro su 100k+ samples
- A2: `SignalRenderer` interface, default `LineSignalRenderer`, user custom renderer impl-able in 50 LoC
- A3: `AxisFormatter` interface con 4 default impls (Time, Decimal, DateTime, Unit)
- A4: ChartConfig split, FrameRate sealed, deprecated old API con ReplaceWith
- A5: Compose compiler report: 0 unstable public types
- A6: Interaction layer: pinch zoom + drag pan + crosshair tap funzionanti
- A7: Bisect snapshot taglia copy 30x su 10s window in 5min buffer (macrobench)
- A8: Render zero-alloc reale (< 1 KB/s in macrobench steady)
- A9: `RealtimeChartState.clear()` API + test
## Parallelism
| Group | Tasks |
|-------|-------|
| P0 | T1, T6, T10 (parallel) |
| P1 | T2, T3, T4 (interfaces parallel) |
| P2 | T5 (config split, depends on T2-T4 interfaces) |
| P3 | T7 (interaction layer, biggest task) |
| P4 | T8, T9 (perf finishing) |
## Estimated agent calls
~14 agent calls. ~6-8 giorni full-time (with v2 deltas).
## Risks
- T2 (MinMaxLTTB): break LodMode enum API migration required by consumers
- T7 (interaction): biggest scope, multi-modal touch testing manual
- T5 (config split): breaking API change HARD BREAK per intake decision
- D1 (toolchain bump): Kotlin 2.12.3 + Compose 1.81.11 behavior surprises
- BCV 0.18.1 (Jul 2022 release age): agent verifies latest at apply, fallback 0.16.3
- AGP 9.2.0 may need Gradle wrapper bump
## Verification commands
```bash
./gradlew :chart-realtime:apiDump
./gradlew :chart-realtime:apiCheck
./gradlew :chart-realtime:iosSimulatorArm64Test
./gradlew :app:installDebug # manual interaction test
./gradlew :chart-realtime:assembleRelease
./gradlew :chart-realtime:publishToMavenLocal --dry-run
```
---
## v2 deltas (intake-confirmed 2026-05-21)
Source: kmp-manager intake. Applied directly by manager (mechanical scope additions, no architectural change).
### Decisions locked
- **Breaking API**: HARD BREAK on T2 (`LodMode` removed) + T5 (old `ChartConfig` ctor + `targetFps` removed). No `@Deprecated` bridges. ABI baseline regen.
- **Versioning**: stay `0.5.0-SNAPSHOT` at close. No tag. No publish.
- **Datetime**: drop `kotlinx-datetime` entirely. Use `kotlin.time.Clock.System.now().toEpochMilliseconds()` (stable since Kotlin 2.3.0, no `@OptIn`).
- **Toolchain**: full alignment Kotlin 2.3.21, Compose-MP 1.11.0, AGP 9.2.0, coroutines + kotlin-test aligned, BCV verify-latest.
### New tasks
#### D1 — Toolchain bump (P0, blocks all)
- Files:
- `gradle/libs.versions.toml`
- `gradle/wrapper/gradle-wrapper.properties` (if Gradle bump needed for AGP 9.2.0)
- `chart-realtime/build.gradle.kts` (drop `kotlinx-datetime` dep)
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt:16,90` (swap `kotlinx.datetime.Clock` `kotlin.time.Clock`)
- Goal: Kotlin 2.1.02.3.21, Compose-MP 1.8.01.11.0, AGP 8.7.39.2.0, coroutines 1.9.0latest, kotlin-test2.3.21, BCVverify-latest (fallback 0.16.3). Drop kotlinx-datetime. Single call site swap.
- Agent: gradle-expert
- Verify:
- `./gradlew :chart-realtime:compileKotlinAndroid` green
- `./gradlew :chart-realtime:iosSimulatorArm64Test` 107 tests pass (regression gate)
- `./gradlew :chart-realtime:apiCheck` green (no ABI delta from toolchain alone)
- `./gradlew :chart-realtime:assembleRelease` green
#### D3 — LTTB upper-bound semantic fix (P5)
- Files: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/LodDecimator.kt` (or post-T2 `lod/LttbLod.kt`)
- Goal: align LTTB upper-bound with snapshot half-open `[lo, hi)` semantic. Currently inclusive off-by-one at window boundary.
- Agent: data-impl
- Verify: existing 33 LodDecimator tests pass + 1 new boundary test
#### D4 — NumberFormat Long overflow guard (P1)
- Files: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/NumberFormat.kt`
- Goal: `formatFixed` Long path overflows @ |v|≥1e19. Add guard route to `formatScientific` before overflow.
- Agent: data-impl
- Verify: new test `formatFixed_handlesLongOverflow` + existing 38 NumberFormat tests pass
### Task semantic changes
#### T2 (was: deprecated bridges) → HARD BREAK
- Remove `LodMode` enum entirely (was: `ChartConfig.lodMode: LodMode` `lodStrategy: LodStrategy`).
- No `LodMode.MIN_MAX_LTTB` shorthand. Pure interface.
- ABI baseline regen: `./gradlew :chart-realtime:apiDump` + commit.
#### T5 (was: deprecated old ctor) → HARD BREAK
- Remove `ChartConfig` original ctor entirely. Replace with new ctor taking `DataConfig + AxisConfig + RenderConfig`.
- Remove `targetFps: Int?` field. Replaced by `RenderConfig.frameRate: FrameRate`.
- No `@Deprecated` bridges.
- ABI baseline regen.
#### T6 — verify SignalConfig stability covered
- Add `@Immutable` to `SignalConfig` (closes deferred v0.3.0 issue: Compose compiler unstable warning).
### Updated P-groups
| Group | Tasks |
|-------|-------|
| P0 | D1 (toolchain, blocks all) |
| P1 | T1, T6, T10, D4 |
| P2 | T2, T3, T4 |
| P3 | T5 |
| P4 | T7 |
| P5 | T8, T9, D3 |
| P6 | ABI regen + apiCheck + unify |
### Acceptance additions
- A10: Kotlin 2.3.21 + Compose-MP 1.11.0 + AGP 9.2.0 green on all targets; 107 tests pass on iosSimulatorArm64.
- A11: ABI baseline regenerated post-T2/T5; `chart-realtime.api` + `chart-realtime.klib.api` committed.
- A12: `LodMode` symbol absent, `targetFps` symbol absent, `kotlinx-datetime` dep absent. No `@Deprecated` bridges in commonMain public API.