KMPCharts/app/src/main/kotlin/dev/dtrentin/chart/demo/CustomRendererScreen.kt
Trentin Davide 512ca35d8b
Some checks failed
CI / build (push) Has been cancelled
feat(app): portfolio showcase app — 7 scenes demoing all capabilities
Multi-scene Compose app demonstrating chart-realtime v0.5.0 features
end-to-end. Single Activity, sealed Screen enum + mutableStateOf for
nav (no nav library dep).

Scenes:
- LiveSensor — synthetic waveform sandbox with full control panel
  (relocated from DemoScreen.kt; behavior unchanged)
- Ecg — single-signal 250Hz ECG with P/QRS/T complex synthesis, dark
  theme, UnitAxisFormatter("mV"), MinMaxLttbLodStrategy
- StressTest — 8 signals @ 200Hz (1600 samples/sec total), MinMaxLod
  for peak preservation, live push-rate readout
- HistoryScrubber — preloads 300k samples in coroutine, then pauses
  production; demonstrates tiered buffer (Tier0 60s full-rate +
  Tier1/Tier2 LoD-decimated history) and interaction History mode
- CustomRenderer — ships AreaSignalRenderer + ScatterSignalRenderer
  in app/demo/renderers/ to demo SignalRenderer extensibility (~30
  LoC each, no library code change)
- Formatters — 4 stacked mini-charts, same data, 4 different
  AxisFormatter impls (Time / Decimal / DateTime / Unit("Hz"))
- ThemeGallery — 4 ChartTheme presets side-by-side (Light, Dark,
  Cyberpunk, Newspaper)

Material3 NavigationBar + TopAppBar. Unicode emoji as nav icons
(skips material-icons-extended dep). enableEdgeToEdge() with Scaffold
inset handling. Dark mode toggle in TopAppBar actions.

Files:
- app/demo/ShowcaseApp.kt — root composable, Screen enum, nav
- app/demo/{LiveSensor,Ecg,StressTest,HistoryScrubber,
  CustomRenderer,FormattersShowcase,ThemeGallery}Screen.kt
- app/demo/renderers/{AreaSignalRenderer,ScatterSignalRenderer}.kt
- app/demo/generators/EcgGenerator.kt
- app/demo/MainActivity.kt — hosts ShowcaseApp + dark-mode state
- app/demo/DemoScreen.kt — deleted (content moved to
  LiveSensorScreen.kt)

Preserved (used by LiveSensorScreen):
- ControlPanel.kt, DemoConfig.kt, SignalGenerator.kt,
  WaveformType.kt, PushCounter.kt

Build: assembleDebug green, APK 11.1 MB. No new Maven deps. No
library code changes. iosSimulatorArm64Test still 207/207 green.

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

128 lines
4.9 KiB
Kotlin

package dev.dtrentin.chart.demo
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.dtrentin.chart.RealtimeChart
import dev.dtrentin.chart.RealtimeChartState
import dev.dtrentin.chart.demo.renderers.AreaSignalRenderer
import dev.dtrentin.chart.demo.renderers.ScatterSignalRenderer
import dev.dtrentin.chart.interaction.InteractionConfig
import dev.dtrentin.chart.interaction.rememberChartInteractionState
import dev.dtrentin.chart.lod.MinMaxLttbLodStrategy
import dev.dtrentin.chart.model.AxisConfig
import dev.dtrentin.chart.model.AxisLabelMode
import dev.dtrentin.chart.model.ChartConfig
import dev.dtrentin.chart.model.DataConfig
import dev.dtrentin.chart.model.RenderConfig
import dev.dtrentin.chart.model.SignalConfig
import dev.dtrentin.chart.model.YRange
import dev.dtrentin.chart.model.rememberMaterialChartTheme
import dev.dtrentin.chart.render.DecimalAxisFormatter
import dev.dtrentin.chart.render.TimeAxisFormatter
import kotlin.math.PI
import kotlin.math.sin
import kotlin.random.Random
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
/**
* Demonstrates the [dev.dtrentin.chart.render.SignalRenderer] extensibility surface.
* Two signals on the same chart:
* - "area": filled curve via [AreaSignalRenderer]
* - "scatter": dot scatter via [ScatterSignalRenderer]
*
* Both renderers are ~30 LoC each (see `demo/renderers/`).
*/
private const val CUSTOM_SAMPLE_HZ = 100
@Composable
fun CustomRendererScreen() {
val chartTheme = rememberMaterialChartTheme()
val primary = MaterialTheme.colorScheme.primary
val secondary = MaterialTheme.colorScheme.tertiary
val state = remember(chartTheme) {
RealtimeChartState(
config = ChartConfig(
data = DataConfig(
xWindowSeconds = 10f,
yRange = YRange.Auto(),
),
axis = AxisConfig(
xLabelMode = AxisLabelMode.BESIDE,
yLabelMode = AxisLabelMode.BESIDE,
yLabelDecimals = 2,
showGrid = true,
xFormatter = TimeAxisFormatter,
yFormatter = DecimalAxisFormatter(decimals = 2),
),
render = RenderConfig(
theme = chartTheme,
lodStrategy = MinMaxLttbLodStrategy(),
),
),
)
}
val interaction = rememberChartInteractionState(InteractionConfig())
LaunchedEffect(state, primary, secondary) {
state.addSignal(
"area",
SignalConfig(color = primary, strokeWidth = 2f, renderer = AreaSignalRenderer),
)
state.addSignal(
"scatter",
SignalConfig(color = secondary, strokeWidth = 2f, renderer = ScatterSignalRenderer),
)
val tickIntervalMs = (1_000L / CUSTOM_SAMPLE_HZ).coerceAtLeast(1L)
val startMs = System.currentTimeMillis()
val twoPi = 2f * PI.toFloat()
var n = 0L
while (isActive) {
val ts = startMs + (n * 1000L) / CUSTOM_SAMPLE_HZ
val t = n.toFloat() / CUSTOM_SAMPLE_HZ
val areaV = sin(twoPi * 0.3f * t)
val scatterV = (Random.nextFloat() * 2f - 1f) * 0.7f
state.push("area", ts, areaV)
state.push("scatter", ts, scatterV)
n++
delay(tickIntervalMs)
}
}
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 6.dp)) {
Text(
"Custom SignalRenderer impls",
style = MaterialTheme.typography.titleSmall,
)
Text(
"Same SignalRenderer interface as LineSignalRenderer. ~30 LoC each.",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
RealtimeChart(
state = state,
modifier = Modifier.fillMaxSize(),
xWindowSeconds = 10f,
theme = chartTheme,
interaction = interaction,
)
}
}
}
}