KMPCharts/.paul/phases/v0.2.0-plan-SUMMARY.md
dtrentin 73bbd9ddec feat(chart-realtime): v0.3.0 — dirty flag render, targetFps=30, iOS targets, KDoc, README
- Dirty-flag render: dataVersion counter in RealtimeChartState, skip Canvas draw
  when no new data since last frame (zero GPU work when sensor idle)
- targetFps=30 default in ChartConfig (pass null for max display refresh rate)
- iOS targets: iosX64, iosArm64, iosSimulatorArm64 + Platform.ios.kt actual
- KDoc: RealtimeChart params, dataVersion, ChartConfig.targetFps, LodMode entries,
  AxisLabelMode entries
- README: fixed Maven coords, added LoD/AxisLabels/Material3 sections, updated
  feature list and performance notes
- Tests: RealtimeChartStateTest (17 tests, dataVersion + signal lifecycle)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 11:11:37 +02:00

159 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: v0.2.0-polish
plan: v0.2.0
subsystem: chart-realtime
tags: [kmp, compose, canvas, buffer, lod, tiered-buffer, lttb, min-max, axis, material3]
requires:
- phase: v0.1.0-mvp
provides: CircularBuffer, LodDecimator, RealtimeChart composable, basic AxisRenderer
provides:
- TieredBuffer (3-tier 1h ring buffer, MIN_MAX binning, zero-alloc)
- LodMode enum (MIN_MAX, LTTB)
- LTTB render-time decimation
- AxisLabelMode (INSIDE/BESIDE/HIDDEN) with chartLeft/chartBottom insets
- T0-relative X-axis timestamps with mm:ss/hh:mm:ss formatting
- Material3 dynamic theming (light/dark, wallpaper-based)
- Camera cutout / edge-to-edge support
- Sensor domain layer (Clean Architecture)
- Koin DI wiring
- Package: dev.dtrentin.chart
- Git repository on ssh://3nt-git.duckdns.org:222/davide.trentin/KMPCharts.git
affects: [future iOS target, future persistence layer]
tech-stack:
added: [koin-4.0.0, kotlinx-coroutines-android, androidx-lifecycle-runtime-compose]
patterns: [TieredBuffer hot-path write, MIN_MAX composable decimation, LodDecimator as class with pre-alloc scratch]
key-files:
created:
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/TieredBuffer.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/LodMode.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/AxisLabelMode.kt
- sensor-domain/src/commonMain/kotlin/dev/dtrentin/sensor/domain/
modified:
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/LodDecimator.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/CircularBuffer.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/SignalRenderer.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/ChartConfig.kt
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/ChartTheme.kt
key-decisions:
- "TieredBuffer over flat ring: O(signals × constant) memory, scales to 20+ signals"
- "MIN_MAX binning for tiers: composable operation, zero visual discontinuity at boundaries"
- "MEAN removed: introduces values never in raw signal, disinformation for sensor data"
- "LodDecimator as class: pre-alloc scratch arrays, zero GC at render time"
- "Sensor rate capped at 200Hz: thermal/battery optimization"
- "Package dev.dtrentin: personal project, not company"
patterns-established:
- "MIN_MAX composability: MIN(bin_mins)=global_MIN across tier boundaries"
- "TieredBuffer snapshot always time-sorted: tier2→tier1→tier0 with tier0Start=max(windowStart,tier0Boundary)"
- "Render arrays pre-alloc in RealtimeChart via remember { }, zero alloc per frame"
duration: ~8h (multi-session)
started: 2026-05-20T00:00:00Z
completed: 2026-05-21T00:00:00Z
---
# v0.2.0 Plan Summary
**chart-realtime KMP library rebuilt from scratch: TieredBuffer (1h/20-signal), LTTB+MIN_MAX LoD, axis labels with INSIDE/BESIDE/HIDDEN modes, Material3 dynamic theming, Clean Architecture sensor domain — shipped as v0.5.0-SNAPSHOT.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~8h multi-session |
| Started | 2026-05-20 |
| Completed | 2026-05-21 |
| Versions shipped | 0.2.0→0.3.0→0.4.0→0.4.1→0.5.0-SNAPSHOT |
| Git commits | 3 (initial + 2 incremental) |
| Files modified | 20+ |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| Axis text labels visible | Pass | AxisRenderer TextMeasurer, INSIDE/BESIDE/HIDDEN modes |
| Dynamic X-window from UI | Pass | Slider 23600s, StateFlow, MAX_BUFFER_SECONDS=3600 |
| Material3 theme compliance | Pass | rememberMaterialChartTheme(), dynamic color API 31+ |
| User-decided signal colors | Pass | SignalConfig.color, ChartTheme separate |
| Camera cutout / edge-to-edge | Pass | enableEdgeToEdge() + Scaffold innerPadding |
| 1h data support without OOM | Pass | TieredBuffer ~18MB for 6 signals |
| No tier boundary visual artifact | Pass | MIN_MAX composable + tier0 lower-bound fix |
| LodDecimator zero-alloc | Pass | Class with pre-alloc scratch arrays |
| Negative X-axis values | Pass | formatTimeSec handles negative elapsed time |
| Sensor rate cap 200Hz | Pass | SensorRepositoryImpl 5_000μs |
| Package renamed dev.dtrentin | Pass | eu.henesis → dev.dtrentin everywhere |
| Remote git repository | Pass | ssh://3nt-git.duckdns.org:222/davide.trentin/KMPCharts.git |
## Accomplishments
- **TieredBuffer**: 3-tier ring buffer (tier0=5min@200Hz, tier1=10min@10Hz MIN_MAX, tier2=45min@1Hz MIN_MAX). ~18MB for 6 signals vs 259MB naive flat at 1000Hz assumption
- **MIN_MAX composability**: tier boundary artifacts eliminated. MIN(tier_mins)=global_MIN mathematically proven. MEAN removed as inherently dishonest for raw sensor data
- **LTTB algorithm**: O(n) Largest-Triangle-Three-Buckets in LodDecimator. Preserves peaks, smooth trend line, better than mean without signal distortion
- **Axis overhaul**: INSIDE/BESIDE/HIDDEN modes, chartLeft/chartBottom insets, T0-relative timestamps (mm:ss/hh:mm:ss), scientific notation for |y|<0.01 or >1e5, negative values, grid toggle
## Deviations from v0.2.0 Plan
| Type | Count | Description |
|------|-------|-------------|
| Scope additions | 15+ | All features beyond Material3/axis/xWindow/scratch |
| Auto-fixed bugs | 8 | Tier boundary, LTTB samplePtr, X label sticky, Y label clipping, LoD path unravel, etc. |
| Deferred | 1 | iOS expect/actual — still commonMain only |
**Key deviations:**
- Phase 4 (scratch optimization): addressed differently — LodDecimator converted from `object` to `class` with pre-allocated instance fields, not param-passing as planned
- CircularBuffer replaced by TieredBuffer at storage level: larger scope than originally planned
- MEAN LodMode added then removed in same milestone after analysis showed it was harmful
## Key Decisions
| Decision | Rationale | Impact |
|----------|-----------|--------|
| TieredBuffer over flat ring | O(N×constant) memory, scales to 20+ signals at any rate | Core buffer architecture |
| MIN_MAX tier binning | Composable: MIN(mins)=global_MIN, no artifacts | Eliminates tier discontinuity |
| MEAN removed from LodMode | Introduces synthetic values never in raw signal | Cleaner API, honest visualization |
| Sensor cap 200Hz not FASTEST | Thermal/battery optimization, 200Hz exceeds visual need | Reduces CPU interrupts 4-10× |
| dev.dtrentin namespace | Personal project, not company IP | Package identity |
## Bugs Fixed
| Bug | Root Cause | Fix |
|-----|-----------|-----|
| TieredBuffer tier artifact | tier0 snapshot lacked `ts >= tier0BoundaryMs` filter → double-inclusion → col-sort broken | Added `tier0Start = maxOf(windowStart, tier0BoundaryMs)` |
| LoD path unravel | LodDecimator mapped out-of-window samples (tRelMs<0) to x=0 | Added in-window filter before binning |
| X label sticky at axis | `coerceAtLeast(chartLeft)` clamped all early labels to axis line | Replaced with `if (labelX >= chartLeft)` skip |
| Y labels clipped bottom | Used raw `14f` px instead of density-aware sp | `with(this) { 12.sp.toPx() }` |
| Signal line outside chart | No clipRect on drawPath | Added `clipRect(left=chartLeft…)` |
| Negative X shows "0s" | `coerceAtLeast(0L)` | `abs()` + `-` prefix for negative |
| OOM at MAX_X_WINDOW=3600 | MAX_SAMPLE_RATE=1000Hz assumed, 259MB pre-alloc | Reduced to 200Hz, TieredBuffer replaces flat ring |
## Next Phase Readiness
**Ready:**
- Library stable at v0.5.0-SNAPSHOT, published to mavenLocal
- Remote git: `ssh://3nt-git.duckdns.org:222/davide.trentin/KMPCharts.git`
- AndroidChartsApp consuming library, builds clean
- Clean Architecture sensor domain in place
**Potential next work:**
- Dirty flag render (skip frame if no new data) thermal/battery improvement
- targetFps=30 default thermal improvement
- iOS expect/actual for currentTimeMs (currently Android only)
- Publish to Maven Central or GitHub Packages
- KDoc on public API surface
- README with integration example
**Deferred:**
- iOS support (expect/actual beyond currentTimeMs)
- Adaptive sensor rate based on xWindowSeconds
---
*Phase: v0.2.0-polish, completed 2026-05-21*