Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LineCartesianLayer.PointProvider bug on multiple line series with custom Shape #832

Open
diegoup2 opened this issue Aug 6, 2024 · 2 comments

Comments

@diegoup2
Copy link

diegoup2 commented Aug 6, 2024

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 diegoup2 added the bug label Aug 6, 2024
@Gowsky
Copy link
Member

Gowsky commented Aug 7, 2024

Hi @diegoup2, thank you for the report. We've identified the problem with ShapeComponent. It occurs when Matrix transformations are used. We will fix it in the next release.

In the meantime you can work around this easily in your code by calling Path.rewind at the beginning of the draw function.

Also, your custom Shape will stick out of its bounds since the diagonal of a square is longer than its side. This may lead to clipping. You could update the drawing logic to resize the shape correctly, but there’s a built-in way of creating a diamond with correct sizing. It may be quicker to use that.

Shape.cut(allPercent = 50)

Note that you may have to increase the Point size.

This will not be affected by the ShapeComponent bug, as CorneredShape doesn’t use Matrix transformations.

@Gowsky
Copy link
Member

Gowsky commented Aug 12, 2024

Hi @diegoup2, Vico 2.0.0-alpha.28 fixes this bug. Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants