Coverage Summary for Class: SurferSparklineKt (com.georgeci.moneysurfer.uikit.components.base)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferSparklineKt |
100%
(1/1)
|
80%
(4/5)
|
68.4%
(26/38)
|
100%
(48/48)
|
95.5%
(620/649)
|
package com.georgeci.moneysurfer.uikit.components.base
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.uikit.preview.SurferComponentPreview
import com.georgeci.moneysurfer.uikit.theme.AppTheme
import io.github.koalaplot.core.Symbol
import io.github.koalaplot.core.line.AreaBaseline
import io.github.koalaplot.core.line.AreaPlot2
import io.github.koalaplot.core.style.AreaStyle
import io.github.koalaplot.core.style.LineStyle
import io.github.koalaplot.core.util.ExperimentalKoalaPlotApi
import io.github.koalaplot.core.xygraph.AxisContent
import io.github.koalaplot.core.xygraph.AxisStyle
import io.github.koalaplot.core.xygraph.GridStyle
import io.github.koalaplot.core.xygraph.Point
import io.github.koalaplot.core.xygraph.TickPosition
import io.github.koalaplot.core.xygraph.XYGraph
import io.github.koalaplot.core.xygraph.rememberFloatLinearAxisModel
/**
* A bare trend curve: line, gradient fill down to the floor of the plot, and a dot on the newest
* point. No axes, no grid, no labels — a sparkline states a shape, and the figures it is a shape of
* are printed by whatever card it sits in.
*
* Draws nothing below two points: one point is a dot with no trend, and the y-range collapses.
*
* The y-range is padded by [YPaddingFraction] of the spread so the extremes do not sit on the
* curve's own bounding box, and a flat series still draws a line through the middle rather than
* along the edge.
*/
@OptIn(ExperimentalKoalaPlotApi::class)
@Composable
fun SurferSparkline(
points: List<Pair<Float, Float>>,
color: Color,
modifier: Modifier = Modifier,
) {
if (points.size < 2) return
val data = remember(points) { points.map { Point(it.first, it.second) } }
val xMin = points.minOf { it.first }
val xMax = points.maxOf { it.first }
val xRange = xMin..xMax
val yMin = points.minOf { it.second }
val yMax = points.maxOf { it.second }
val padding = ((yMax - yMin).takeIf { it > 0f } ?: 1f) * YPaddingFraction
val yRange = (yMin - padding)..(yMax + padding)
val transparentAxis = AxisContent<Float>(
labels = {},
title = {},
style = AxisStyle(
color = Color.Transparent,
majorTickSize = 0.dp,
minorTickSize = 0.dp,
tickPosition = TickPosition.None,
lineWidth = 0.dp,
),
)
val emptyGrid = GridStyle(
horizontalMajorStyle = null,
horizontalMinorStyle = null,
verticalMajorStyle = null,
verticalMinorStyle = null,
)
XYGraph(
xAxisModel = rememberFloatLinearAxisModel(xRange),
yAxisModel = rememberFloatLinearAxisModel(yRange),
xAxisContent = transparentAxis,
yAxisContent = transparentAxis,
modifier = modifier,
gridStyle = emptyGrid,
) {
val lastPoint = data.last()
AreaPlot2(
data = data,
areaBaseline = AreaBaseline.HorizontalLine(yRange.start),
areaStyle = AreaStyle(
brush = Brush.verticalGradient(
colors = listOf(color.copy(alpha = 0.3f), color.copy(alpha = 0f)),
),
),
lineStyle = LineStyle(brush = SolidColor(color), strokeWidth = 2.dp),
symbol = { point ->
if (point === lastPoint) {
Symbol(
size = 8.dp,
shape = CircleShape,
fillBrush = SolidColor(color),
)
}
},
)
}
}
/**
* A series of plain values as the evenly-spaced points [SurferSparkline] draws, oldest first.
*
* The x axis of a sparkline carries no information — the points are one per period and the periods
* are equal — so callers holding a bare list of balances do not have to invent one.
*/
fun sparklinePoints(values: List<Float>): List<Pair<Float, Float>> =
values.mapIndexed { index, value -> index.toFloat() to value }
private const val YPaddingFraction = 0.1f
@Preview
@Composable
private fun SurferSparklinePreview() {
SurferComponentPreview {
SurferSparkline(
points = sparklinePoints(listOf(9_800f, 10_240f, 9_950f, 10_610f, 11_160f, 11_575f)),
color = AppTheme.materialColors.primary,
modifier = Modifier.height(72.dp).fillMaxWidth(),
)
}
}