package com.radiola.ui.components import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.unit.dp import com.radiola.domain.model.StatPoint import com.radiola.ui.theme.RadiolaTheme import java.text.SimpleDateFormat import java.util.Date import java.util.Locale /** * Компонент-спарклайн: сглаженный линейный график с градиентной заливкой. * Используется для отображения популярности трека (проигрывания / лайки). * Не показывает оси — только форму данных. */ @Composable fun PopularityChart( points: List, modifier: Modifier = Modifier, lineColor: Color = RadiolaTheme.colors.accent ) { val colors = RadiolaTheme.colors val dateFmt = remember { SimpleDateFormat("d MMM", Locale("ru")) } Box(modifier = modifier) { if (points.size >= 2) { val minVal = points.minOf { it.value }.toFloat() val maxVal = points.maxOf { it.value }.toFloat() val range = (maxVal - minVal).coerceAtLeast(1f) Canvas(modifier = Modifier.fillMaxSize()) { val w = size.width val h = size.height val topPad = 4.dp.toPx() val botPad = 4.dp.toPx() val drawH = h - topPad - botPad // Вычисляем координаты точек fun xAt(i: Int) = i * w / (points.size - 1) fun yAt(v: Float) = topPad + drawH * (1f - (v - minVal) / range) // Сглаженный path через cubic bezier val linePath = Path() linePath.moveTo(xAt(0), yAt(points[0].value.toFloat())) for (i in 1 until points.size) { val x0 = xAt(i - 1) val y0 = yAt(points[i - 1].value.toFloat()) val x1 = xAt(i) val y1 = yAt(points[i].value.toFloat()) val cx = (x0 + x1) / 2f linePath.cubicTo(cx, y0, cx, y1, x1, y1) } // Заливка под графиком val fillPath = Path().apply { addPath(linePath) lineTo(xAt(points.size - 1), h) lineTo(xAt(0), h) close() } drawPath( path = fillPath, brush = Brush.verticalGradient( colors = listOf(lineColor.copy(alpha = 0.28f), Color.Transparent), startY = topPad, endY = h ) ) // Линия графика drawPath( path = linePath, color = lineColor, style = Stroke( width = 2.dp.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round ) ) // Точка первого и последнего значения drawCircle( color = lineColor, radius = 3.dp.toPx(), center = Offset(xAt(0), yAt(points.first().value.toFloat())) ) drawCircle( color = lineColor, radius = 3.dp.toPx(), center = Offset(xAt(points.size - 1), yAt(points.last().value.toFloat())) ) } // Подписи дат по краям val firstDate = dateFmt.format(Date(points.first().date)) val lastDate = dateFmt.format(Date(points.last().date)) Text( text = firstDate, style = MaterialTheme.typography.labelSmall, color = colors.textMuted, modifier = Modifier .align(Alignment.BottomStart) .padding(start = 4.dp, bottom = 2.dp) ) Text( text = lastDate, style = MaterialTheme.typography.labelSmall, color = colors.textMuted, modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 4.dp, bottom = 2.dp) ) } // При 0 или 1 точке — ничего не рисуем (корректная пустая обработка) } }