KMPCharts/chart-realtime/README.md
Trentin Davide 07197ed8df
Some checks are pending
CI / build (push) Waiting to run
chore(portfolio): v1.0.0 prep — docs, CI, demos
Autonomous portfolio prep batch. All gates green: 207 tests pass on
iosSimulatorArm64Test, assembleRelease green, apiCheck green, app builds.

Docs:
- CHANGELOG.md (Keep-a-Changelog, retroactive v0.1-v0.5)
- CONTRIBUTING.md (build + style + PR workflow + perf rules)
- chart-realtime/README.md rewrite for v0.5.0 API surface
- docs/PERFORMANCE.md (LoD perf table, alloc budget, memory budget,
  stability report, threading model)

CI (.github/):
- workflows/ci.yml — macos-latest, JDK 21, konan cache, full gate
- workflows/release.yml — tag-triggered, GitHub release with XCFramework
  + AAR; Maven Central as commented TODO (needs OSSRH + GPG secrets)
- dependabot.yml — weekly gradle + actions updates
- pull_request_template.md + 2 issue templates (bug, feature)

Android demo polish:
- DemoScreen + ControlPanel (Material3 surface)
- Sample rate slider (10..200Hz), 1-6 signal count, per-signal waveform
  picker (sine/square/triangle/noise), window seconds, LoD picker, Y
  label mode, Y formatter, interaction toggle, theme switch, status chips
- Single producer coroutine (cleaner crosshair readout, easier rate
  accounting)
- SignalGenerator + WaveformType + DemoConfig + PushCounter

iOS demo scaffold (samples/ios/ChartRealtimeDemo/):
- SPM Package.swift with local binaryTarget pointing to
  chart-realtime/build/XCFrameworks/debug/ChartRealtime.xcframework
- DemoApp.swift + DemoView.swift (smoke test confirming Kotlin types
  accessible from Swift)
- README documents the ComposeUIViewController shim needed in
  chart-realtime/src/iosMain/ for full SwiftUI hosting (deferred to
  v1.0.0 T1)
- xcodebuild against iOS Simulator builds green

Debt:
- gradle.properties: removed kotlin.internal.klibs.non-packed=false
  (Compose-MP 1.11.0 final no longer needs the bypass; apiCheck +
  native compile + 207 tests still green)
- 8 remaining deprecation warnings tied to com.android.library + KMP
  plugin combo; clearing them requires migration to
  com.android.kotlin.multiplatform.library 9.2.0 which forces ABI
  baseline regen + Maven publish restructure + XCFramework hierarchy
  expansion. Deferred as dedicated ticket.

Skipped (need user):
- CODE_OF_CONDUCT.md (content classifier blocks Contributor Covenant
  verbatim)
- Maven Central T1 (Sonatype + GPG)
- Benchmark vs competitors T4 (methodology + Maven coords)
- Logo/banner/GIF T7 (design assets)
- Tag + GitHub release T10 (rename pending)

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

221 lines
7.3 KiB
Markdown
Raw Permalink 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.

# chart-realtime
Real-time line chart for **Compose Multiplatform** (Android + iOS). Optimized for
sensor data: **200Hz+ multi-signal**, bounded memory, 1h+ rolling window, zero-alloc render path.
> Status: `0.5.0-SNAPSHOT` — pre-`1.0.0`. API is stable on the public surface; Maven Central publishing lands in `1.0.0`.
## Why it exists
Existing KMP chart libraries (Vico, KoalaPlot, AAY-chart) target static datasets
and snapshot-style updates. None target sustained high-frequency streaming sensor
data with bounded memory. `chart-realtime` fills that gap.
## Features
- **Multi-signal** rendering on a single Canvas — add / remove signals at runtime
- **Tiered ring buffer** (5min full-rate + 10min @ 10Hz + 45min @ 1Hz) — 1h of data per
signal at ~1.14 MB
- **Pluggable LoD**: `MinMaxLttbLodStrategy` (SOTA, default) / `MinMaxLodStrategy` /
`LttbLodStrategy`, or implement `LodStrategy`
- **Pluggable rendering**: implement `SignalRenderer` for scatter / area / bar (line
default ships)
- **Pluggable axis formatters**: `TimeAxisFormatter` / `DecimalAxisFormatter` /
`DateTimeAxisFormatter` / `UnitAxisFormatter("mV")`, or implement `AxisFormatter`
- **Interaction layer**: pinch zoom, drag pan, tap crosshair with per-signal values.
Swipe to right edge resumes auto-scroll
- **Data-driven recomposition** via Compose snapshots — idle = 0 recompositions
- **Material3 dynamic theming**, edge-to-edge / camera-cutout safe
- **Thread-safe push** from any background thread (`Snapshot.withMutableSnapshot`
+ retry helper)
- **ABI lockdown** via Binary Compatibility Validator
- **Zero-alloc render** at steady state (cached Stroke / TextStyle / signalsArray /
per-signal scratch arrays)
## Supported targets
- Android (compileSdk 35, minSdk 26)
- iOS arm64 (real devices) + iosSimulatorArm64 (Apple Silicon simulators)
- Intel Mac simulators (`iosX64`) **not supported** since v0.5.0 (Compose
Multiplatform 1.11.0 dropped the variant; Apple deprecated Intel iOS sims since
Xcode 15+)
## Install
Pre-`1.0.0`: consume from Maven Local.
```bash
./gradlew :chart-realtime:publishToMavenLocal
```
```kotlin
// settings.gradle.kts
dependencyResolutionManagement {
repositories { mavenLocal(); google(); mavenCentral() }
}
// build.gradle.kts
implementation("dev.dtrentin:chart-realtime:0.5.0-SNAPSHOT")
```
Post-`1.0.0`: Maven Central (planned).
## Quick start
```kotlin
@Composable
fun MyChart(sensorFlow: Flow<Float>) {
val state = remember { RealtimeChartState(ChartConfig()) }
val interaction = rememberChartInteractionState()
LaunchedEffect(state) {
state.addSignal("ecg", SignalConfig(color = Color.Green))
sensorFlow.collect { v ->
state.push("ecg", System.currentTimeMillis(), v)
}
}
RealtimeChart(
state = state,
modifier = Modifier.fillMaxSize(),
interaction = interaction,
)
}
```
## Configuration
`ChartConfig` is split into three orthogonal sub-configs:
```kotlin
ChartConfig(
data = DataConfig(
xWindowSeconds = 10f,
yRange = YRange.Auto(),
t0 = T0.FirstSample,
),
axis = AxisConfig(
xLabelMode = AxisLabelMode.INSIDE,
yLabelMode = AxisLabelMode.BESIDE,
yLabelDecimals = 2,
showGrid = true,
xFormatter = TimeAxisFormatter,
yFormatter = UnitAxisFormatter("mV", decimals = 2),
),
render = RenderConfig(
theme = ChartTheme(),
frameRate = FrameRate.Display,
lodStrategy = MinMaxLttbLodStrategy(),
),
)
```
### Per-signal renderer override
```kotlin
state.addSignal("noise", SignalConfig(
color = Color.Red,
strokeWidth = 1f,
renderer = MyCustomRenderer, // implement SignalRenderer
))
```
## Interaction
```kotlin
val interaction = rememberChartInteractionState(
config = InteractionConfig(
zoomEnabled = true,
panEnabled = true,
tapCrosshairEnabled = true,
minXWindowSeconds = 0.5f,
maxXWindowSeconds = 3600f,
resumeFollowingThresholdMs = 500L,
)
)
RealtimeChart(state, modifier, interaction = interaction)
```
Gestures:
- **Pinch** — zoom (clamps to `minXWindowSeconds` / `maxXWindowSeconds`)
- **Drag** — pan; mode switches to `History(anchorMs)`
- **Tap** — toggle crosshair; shows per-signal value at the tapped timestamp
- **Pan back to right edge** — auto resumes `Following` mode
State exposed: `mode: ViewportMode { Following | Frozen | History(anchorMs) }`,
`crosshair: CrosshairState?`, `xWindowSecondsOverride: Float`,
`viewportOffsetMs: Long`.
## Flow ingest API
```kotlin
// Pre-timestamped samples
state.collectFrom("ecg", ecgFlow, scope) // Flow<Pair<Long, Float>>
// Auto-timestamp via kotlin.time.Clock
state.collectFrom("accel_x", accelFlow, scope) // Flow<Float>
```
## Performance
- 207 tests on `iosSimulatorArm64Test`, all green at every release
- `MinMaxLttbLodStrategy` measured at **1.80×** speedup vs pure LTTB on
Kotlin/Native scalar code (paper claims 10× under Rust + SIMD)
- Render hot path: 0 `Pair` / `Map` / `Iterator` allocations per frame at steady state
- Memory per signal: ~1.14 MB for 1h of data @ 200Hz
- Bisect-based `TieredBuffer.snapshotWindow` — 30× fewer per-frame copies on
10s windows in 5min Tier0 buffers
- See [`docs/PERFORMANCE.md`](../docs/PERFORMANCE.md) for full numbers
## Limitations (honest)
- **Line rendering only** in default impl. Scatter / area / bar require custom
`SignalRenderer` impl
- **Single Y axis** per chart. No multi-Y, no log scale (planned post-`1.0.0`)
- **No annotations / threshold lines** yet (backlog)
- **No headless export** (PNG / CSV) — render is Compose-only
- **No SwiftUI demo app** yet (planned for `1.0.0`); iOS targets compile, link,
test, ship XCFramework
- **No Maven Central** yet — Maven Local only pre-`1.0.0`
## Architecture
```
RealtimeChart (composable)
├─ Canvas draw lambda
│ ├─ reads state.dataVersion (Compose-observable)
│ ├─ for each signal:
│ │ ├─ TieredBuffer.snapshot → SignalEntry.scratch (1× per frame)
│ │ ├─ LodStrategy.decimate → lodX / lodY (1× per frame)
│ │ └─ SignalRenderer.drawSignal (lodX, lodY, count, ...)
│ └─ AxisRenderer.drawXAxis / drawYAxis
├─ pointer-input chain (when interaction != null)
│ ├─ detectTransformGestures → applyZoom
│ ├─ detectDragGestures → applyPan
│ └─ detectTapGestures → toggleCrosshair
└─ overlay: crosshair guide + per-signal dots + readout (when crosshair set)
RealtimeChartState
├─ _signals: Map<String, SignalEntry> (Volatile, copy-on-write)
├─ signalsArray: cached Array<SignalEntry> (invalidated on add/remove)
├─ _dataVersion: mutableLongStateOf (Compose-observable counter)
└─ resolvedT0Ms: Long? (lazy from T0.FirstSample)
```
## Requirements
- Android `minSdk 26`, `compileSdk 35`
- Kotlin `2.3.21+`
- Compose Multiplatform `1.11.0+` (or AndroidX Compose equivalent)
- JDK 21 toolchain for builds
## Documentation
- [`CHANGELOG.md`](../CHANGELOG.md) — all releases since `0.1.0`
- [`CONTRIBUTING.md`](../CONTRIBUTING.md) — build instructions + PR workflow + style guide
- [`docs/PERFORMANCE.md`](../docs/PERFORMANCE.md) — measured frame time / memory / allocation numbers
- [`.paul/ROADMAP.md`](../.paul/ROADMAP.md) — milestone history + backlog
## License
MIT. See [`LICENSE`](../LICENSE).