# 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 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.1→2.3 + Compose 1.8→1.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.0→2.3.21, Compose-MP 1.8.0→1.11.0, AGP 8.7.3→9.2.0, coroutines 1.9.0→latest, kotlin-test→2.3.21, BCV→verify-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.