|
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> |
||
|---|---|---|
| .. | ||
| api | ||
| src | ||
| build.gradle.kts | ||
| README.md | ||
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 in1.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 implementLodStrategy - Pluggable rendering: implement
SignalRendererfor scatter / area / bar (line default ships) - Pluggable axis formatters:
TimeAxisFormatter/DecimalAxisFormatter/DateTimeAxisFormatter/UnitAxisFormatter("mV"), or implementAxisFormatter - 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.
./gradlew :chart-realtime:publishToMavenLocal
// 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
@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:
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
state.addSignal("noise", SignalConfig(
color = Color.Red,
strokeWidth = 1f,
renderer = MyCustomRenderer, // implement SignalRenderer
))
Interaction
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
Followingmode
State exposed: mode: ViewportMode { Following | Frozen | History(anchorMs) },
crosshair: CrosshairState?, xWindowSecondsOverride: Float,
viewportOffsetMs: Long.
Flow ingest API
// 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 MinMaxLttbLodStrategymeasured at 1.80× speedup vs pure LTTB on Kotlin/Native scalar code (paper claims 10× under Rust + SIMD)- Render hot path: 0
Pair/Map/Iteratorallocations 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.mdfor full numbers
Limitations (honest)
- Line rendering only in default impl. Scatter / area / bar require custom
SignalRendererimpl - 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— all releases since0.1.0CONTRIBUTING.md— build instructions + PR workflow + style guidedocs/PERFORMANCE.md— measured frame time / memory / allocation numbers.paul/ROADMAP.md— milestone history + backlog
License
MIT. See LICENSE.