KMPCharts/chart-realtime
Trentin Davide af6814e2b8 feat(chart): v0.5.0 architecture + interaction
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>
2026-05-22 00:05:43 +02:00
..
api feat(chart): v0.5.0 architecture + interaction 2026-05-22 00:05:43 +02:00
src feat(chart): v0.5.0 architecture + interaction 2026-05-22 00:05:43 +02:00
build.gradle.kts feat(chart): v0.5.0 architecture + interaction 2026-05-22 00:05:43 +02:00
README.md feat(chart): v0.4.0 portfolio hardening 2026-05-21 16:27:15 +02:00

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 (pass null for 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.