vico
vico copied to clipboard
LineCartesianLayer.PointProvider bug on multiple line series with custom Shape
How to reproduce
- Create custom LineCartesianLayer.PointProvider
- Create custom Shape
- Create a CartesianChartHost
- Add 2 Line series
- Add custom point providers
- 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
As you can see in the image, there's an overlap between a ghost Shape.Rectangle
and my custom Shape.
Expected behavior
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