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

13 KiB

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.ktlod/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: LodModelodStrategy: 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, Rust ref impl

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:50Stroke cached per signal
    • chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:54-131TextStyle 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, Modifier.drawWithCache - Android Devs

T9 — Replace Pair<Float,Float> return + Map iteration alloc

  • Files:
    • chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:144-152resolveYRange 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

./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.Clockkotlin.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: LodModelodStrategy: 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.