Some checks are pending
CI / build (push) Waiting to run
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>
221 lines
7.3 KiB
Markdown
221 lines
7.3 KiB
Markdown
# 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).
|