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, ) } } } }