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> |
||
|---|---|---|
| .. | ||
| api | ||
| src | ||
| build.gradle.kts | ||
| README.md | ||
chart-realtime
KMP real-time line chart for Android. Renders multiple sensor signals at 200Hz+ without frame drops.
Features
- Multiple signals on one chart, added/removed dynamically
- FIFO circular buffer — lock-free writes at 200Hz+, safe render reads at 60fps
- Level-of-Detail decimation — renders O(pixels) regardless of buffer size (1h+ of data)
- Dirty-flag render: skips GPU draw when no new data since last frame
- Configurable render rate: default 30 fps, override via
targetFps(passnullfor max display Hz) - X axis: scrolling window, seconds from configurable t0
- Y axis: auto-fit or fixed range
- Material3 default theme, fully customizable via
ChartTheme
Installation
// settings.gradle.kts
include(":chart-realtime")
Or Maven local:
./gradlew :chart-realtime:publishToMavenLocal
// build.gradle.kts
implementation("dev.dtrentin:chart-realtime:0.5.0-SNAPSHOT")
Quick start (3 lines)
val state = remember { RealtimeChartState(ChartConfig()) }
LaunchedEffect(state) {
state.addSignal("ecg", SignalConfig(color = Color.Green))
sensorFlow.collect { v -> state.push("ecg", System.currentTimeMillis(), v) }
}
RealtimeChart(state = state, modifier = Modifier.fillMaxSize())
Configuration
ChartConfig(
xWindowSeconds = 10f, // visible window width
yRange = YRange.Auto(), // or YRange.Fixed(-1f, 1f)
t0 = T0.FirstSample, // or T0.Fixed(epochMs)
targetFps = 30, // default 30 fps; null = max display refresh rate
theme = ChartTheme( // optional visual override
backgroundColor = Color(0xFF1A1A2E),
gridColor = Color(0x22FFFFFF),
axisColor = Color(0xFFBBBBBB),
),
)
Level of Detail
ChartConfig(lodMode = LodMode.MIN_MAX) // default — zero-alloc, preserves peaks
ChartConfig(lodMode = LodMode.LTTB) // visually smooth, slight alloc per frame
Axis Labels
ChartConfig(
xLabelMode = AxisLabelMode.INSIDE, // default — labels over chart area
yLabelMode = AxisLabelMode.BESIDE, // reserved margin for Y labels
)
// AxisLabelMode.HIDDEN — hides labels on that axis
Material3 Theme
val theme = rememberMaterialChartTheme()
RealtimeChart(state = state, theme = theme)
Flow API
// From Flow<Float> (auto-timestamp)
state.collectFrom("accel_x", accelerometerFlow, viewModelScope)
// From Flow<Pair<Long, Float>> (explicit timestamp)
state.collectFrom("ecg", ecgFlow, viewModelScope) // ecgFlow emits (timestampMs, value)
Performance notes
- Data thread pushes at any rate; render thread reads at
targetFps(default 30) or display refresh if null - TieredBuffer keeps ~1h history per signal (3-tier ring, no caller config)
- LoD decimation reduces >pixel-count data to min/max per pixel column
- Pre-allocated arrays — zero GC on hot render path (except LoD scratch, see TODO in
LodDecimator.kt) - Dirty-flag skips canvas draw when buffer unchanged since last frame
Requirements
- Android minSdk 26
- Jetpack Compose / Compose Multiplatform 1.8+
- Kotlin 2.1+
License
MIT License — see LICENSE.