vico icon indicating copy to clipboard operation
vico copied to clipboard

LineCartesianLayer.PointProvider bug on multiple line series with custom Shape

Open diegoup2 opened this issue 6 months ago • 2 comments

How to reproduce

  1. Create custom LineCartesianLayer.PointProvider
  2. Create custom Shape
  3. Create a CartesianChartHost
  4. Add 2 Line series
  5. Add custom point providers
  6. See result
val Diamond: Shape =
    object : Shape {
        override fun draw(
            context: DrawContext,
            paint: Paint,
            path: Path,
            left: Float,
            top: Float,
            right: Float,
            bottom: Float,
        ) {
            val matrix = Matrix()
            val bounds = RectF()
            path.moveTo(left, top)
            path.lineTo(right, top)
            path.lineTo(right, bottom)
            path.lineTo(left, bottom)
            path.computeBounds(bounds, true)
            matrix.postRotate(45f, bounds.centerX(), bounds.centerY())
            path.transform(matrix)
            path.close()
            context.canvas.drawPath(path, paint)
        }

        @Deprecated(
            "Use `draw`.",
            replaceWith = ReplaceWith("draw(context, paint, path, left, top, right, bottom)"),
        )
        override fun drawShape(
            context: DrawContext,
            paint: Paint,
            path: Path,
            left: Float,
            top: Float,
            right: Float,
            bottom: Float,
        ) {
            draw(context, paint, path, left, top, right, bottom)
        }
    }
val LevelSuperHighAlert = Color(0xFF7C0818)
val LevelVeryHighOrLowAlert = Color(0xFFAA182C)
val LevelHighAlert = Color(0xFFEBAB21)
val LevelElevatedAlert = Color(0xFFF0D800)
val LevelNormalAlert = Color(0xFFF0D800)
val SecondaryBlueGray = Color(0xFF6A88AF)

class CustomPointProvider(private val userDB: UserDB?, val isSystolic: Boolean = false) : LineCartesianLayer.PointProvider {

    private var point: Point? = null
    override fun getPoint(
        entry: LineCartesianLayerModel.Entry,
        seriesIndex: Int,
        extraStore: ExtraStore
    ): LineCartesianLayer.Point? {
        point = Point(getGraphShape(entry.y), sizeDp = 10f)
        return point
    }

    override fun getLargestPoint(extraStore: ExtraStore): Point? {
        return point
    }

    private fun getGraphShape(value: Double): ShapeComponent {
        val useCorrectShape = if (isSystolic) Shape.Pill else Diamond
        val useCorrectColor = getColorForShape(value)
        return ShapeComponent(
            shape = useCorrectShape,
            color = useCorrectColor.toArgb(),
            strokeColor = OchsnerDarkBlue.toArgb(),
            strokeThicknessDp = 0.5f
        )
    }

    private fun getColorForShape(value: Double): Color {
        val correctColor: Color = when (userDB?.getProgramsActive()) {
            1 -> {
                if (isSystolic) {
                    when (value) {
                        in 0.0..139.9 -> LevelNormalAlert
                        in 140.0..159.9 -> LevelElevatedAlert
                        in 160.0..999.9 -> LevelSuperHighAlert
                        else -> SecondaryBlueGray
                    }
                } else {
                    when (value) {
                        in 0.0..89.0 -> LevelNormalAlert
                        in 90.0..104.9 -> LevelElevatedAlert
                        in 105.0..999.9 -> LevelSuperHighAlert
                        else -> SecondaryBlueGray
                    }
                }
            }

            else -> {
                if (userDB?.bloodPressureGoal == "140/90") {
                    if (isSystolic) {
                        when (value) {
                            in 0.0..89.9 -> LevelVeryHighOrLowAlert
                            in 90.0..129.9 -> LevelNormalAlert
                            in 130.0..139.9 -> LevelElevatedAlert
                            in 140.0..179.9 -> LevelHighAlert
                            in 180.0..999.9 -> LevelVeryHighOrLowAlert
                            else -> SecondaryBlueGray
                        }
                    } else {
                        when (value) {
                            in 0.0..39.9 -> LevelVeryHighOrLowAlert
                            in 40.0..79.9 -> LevelNormalAlert
                            in 80.0..89.9 -> LevelElevatedAlert
                            in 90.0..119.9 -> LevelHighAlert
                            in 120.0..999.9 -> LevelVeryHighOrLowAlert
                            else -> SecondaryBlueGray
                        }
                    }
                } else {
                    if (isSystolic) {
                        when (value) {
                            in 0.0..89.9 -> LevelVeryHighOrLowAlert
                            in 90.0..119.9 -> LevelNormalAlert
                            in 120.0..129.9 -> LevelElevatedAlert
                            in 130.0..139.9 -> LevelHighAlert
                            in 140.0..179.9 -> LevelVeryHighOrLowAlert
                            in 180.0..999.9 -> LevelSuperHighAlert
                            else -> SecondaryBlueGray
                        }
                    } else {
                        when (value) {
                            in 0.0..39.0 -> LevelVeryHighOrLowAlert
                            in 40.0..79.9 -> LevelNormalAlert
                            in 80.0..89.9 -> LevelHighAlert
                            in 90.0..119.9 -> LevelVeryHighOrLowAlert
                            in 120.0..999.9 -> LevelSuperHighAlert
                            else -> SecondaryBlueGray
                        }
                    }
                }
            }
        }
        return correctColor
    }
}
 val emptyFormatter = remember {
        CartesianValueFormatter { _, _, _ -> "" }
    }

    val axisValueOverrider = remember {
        AxisValueOverrider.fixed(
            minY = 50.0,
            maxY = 200.0,
        )
    }

    val verticalBox =
        remember(chartState.xValuesTransformed) {
            VerticalBox(
                totalDaysBetweenDates = chartState.totalDaysBetweenDates,
                daysSinceStartDateToTarget = chartState.daysSinceStartDateToTarget,
                box = ShapeComponent(
                    color = BlueGray20.toArgb().copyColor(0.36f),
                    shape = Shape.Rectangle
                )
            )
        }

    val systolicPointProvider = remember(chartState.xValuesTransformed) {
        CustomPointProvider(userDB = chartState.currentUser, isSystolic = true)
    }
    val diastolicPointProvider = remember(chartState.xValuesTransformed) {
        CustomPointProvider(userDB = chartState.currentUser, isSystolic = false)
    }

    LaunchedEffect(chartState.xValuesTransformed) {
        withContext(Dispatchers.Default) {
            if (sysList.isEmpty()) return@withContext
            if (chartState.systolicGoal == "" || chartState.diastolicGoal == "") return@withContext
            if (chartState.systolicGoal.toDoubleOrNull() == null || chartState.diastolicGoal.toDoubleOrNull() == null) return@withContext
            if (chartState.xValuesTransformed.isEmpty()) return@withContext
            modelProducer.runTransaction {
                extras { extraStore ->
                    extraStore[sysLabelTop] = chartState.systolicGoal
                    extraStore[sysLimitKey] = chartState.systolicGoal.toDoubleOrNull() ?: 140.0
                    extraStore[diaLabelTop] = chartState.diastolicGoal
                    extraStore[diaLimitKey] = chartState.diastolicGoal.toDoubleOrNull() ?: 90.0
                    extraStore[xToDateMapKey] = chartState.xValuesTransformed
                }
                lineSeries {
                    series(sysList)
                    series(diaList)
                }
            }
        }
    }

    CartesianChartHost(
        chart = rememberCartesianChart(
            rememberLineCartesianLayer(
                axisValueOverrider = axisValueOverrider,
                lineProvider = LineCartesianLayer.LineProvider.series(
                    rememberLine(
                        fill = LineCartesianLayer.LineFill.single(fill(BlueGray20)),
                        pointProvider = systolicPointProvider
                    ),
                    rememberLine(
                        fill = LineCartesianLayer.LineFill.single(fill(BlueGray20)),
                        pointProvider = diastolicPointProvider
                    )
                )
            ),
            endAxis = rememberEndAxis(
                valueFormatter = emptyFormatter
            ),
            topAxis = rememberTopAxis(
                valueFormatter = emptyFormatter
            ),
            startAxis = rememberStartAxis(
                label = rememberTextComponent(
                    color = OchsnerDarkBlue,
                    typeface = AppTheme.GraphAxisLabel.toGraphicsTypeFace(),
                )
            ),
            bottomAxis = rememberBottomAxis(
                valueFormatter = chartValueFormatter,
                label = rememberTextComponent(
                    color = OchsnerDarkBlue,
                    typeface = AppTheme.GraphAxisLabel.toGraphicsTypeFace(),
                    lineCount = 3,
                    textSize = 10.sp
                ),
                itemPlacer =
                remember {
                    HorizontalAxis.ItemPlacer.default(
                        spacing = 1,
                        shiftExtremeTicks = false,
                        addExtremeLabelPadding = false
                    )
                }
            ),
            getXStep = { 1.0 },
            decorations = listOf(
                rememberHorizontalLine(
                    y = { it.getOrNull(sysLimitKey) ?: 0.0 },
                    line = rememberLineComponent(
                        color = OchLightBlue,
                        thickness = 1.5.dp,
                    ),
                    labelComponent = rememberTextComponent(
                        color = OchLightBlue,
                        margins = Dimensions.of(start = 4.dp),
                        padding = Dimensions.of(8.dp, 2.dp),
                    ),
                    label = { it[sysLabelTop] },
                    verticalLabelPosition = VerticalPosition.Top,
                    horizontalLabelPosition = HorizontalPosition.End,
                ),
                rememberHorizontalLine(
                    y = { it[diaLimitKey] },
                    line = rememberLineComponent(
                        color = OchLightBlue,
                        thickness = 1.5.dp,
                    ),
                    labelComponent = rememberTextComponent(
                        color = OchLightBlue,
                        margins = Dimensions.of(start = 4.dp),
                        padding = Dimensions.of(8.dp, 2.dp),
                    ),
                    label = { it[diaLabelTop] },
                    verticalLabelPosition = VerticalPosition.Top,
                    horizontalLabelPosition = HorizontalPosition.End,
                ),
                verticalBox
            ),
        ),
        modelProducer,
        modifier = Modifier
            .fillMaxWidth()
            .padding(bottom = 16.dp),
        zoomState = rememberVicoZoomState(zoomEnabled = false),
    )

Observed behavior

Screenshot 2024-08-06 at 4 18 13 PM

As you can see in the image, there's an overlap between a ghost Shape.Rectangle and my custom Shape.

Expected behavior

Screenshot 2024-08-06 at 4 19 38 PM

This resolves with changing from custom Diamond Shape to Shape.Rectangle on the LineCartesianLayer.PointProvider

Vico version(s)

2.0.0-alpha.27

Android version(s)

API 34

Additional information

No response

diegoup2 avatar Aug 06 '24 21:08 diegoup2