From 8f0b6c5a7fc76bb463dd33b2bfd2dedcb5ee9289 Mon Sep 17 00:00:00 2001 From: Patrick Michalik <120058021+patrickmichalik@users.noreply.github.com> Date: Mon, 2 Oct 2023 08:41:01 +0200 Subject: [PATCH] Fix `ChartValuesManager`-related concurrency issues Co-authored-by: Patryk Goworowski --- .../vico/compose/chart/Charts.kt | 19 ++++-- .../chart/entry/ChartEntryModelExtensions.kt | 18 +++-- .../layout/MeasureContextExtensions.kt | 9 +-- .../compose/state/ChartEntryModelState.kt | 36 ---------- .../compose/state/ChartEntryModelWrapper.kt | 65 +++++++++++++++++++ .../DefaultHorizontalAxisItemPlacer.kt | 6 +- .../core/axis/horizontal/HorizontalAxis.kt | 8 +-- .../vertical/DefaultVerticalAxisItemPlacer.kt | 4 +- .../vico/core/axis/vertical/VerticalAxis.kt | 10 +-- .../vico/core/chart/BaseChart.kt | 2 +- .../vico/core/chart/column/ColumnChart.kt | 4 +- .../core/chart/decoration/ThresholdLine.kt | 4 +- .../vico/core/chart/draw/ChartDrawContext.kt | 5 +- .../chart/draw/ChartDrawContextExtensions.kt | 3 +- .../vico/core/chart/line/LineChart.kt | 6 +- .../core/chart/values/ChartValuesManager.kt | 33 +++++----- .../core/chart/values/ChartValuesProvider.kt | 14 ++-- .../core/chart/values/MutableChartValues.kt | 13 ++++ .../vico/core/context/MeasureContext.kt | 9 ++- .../core/context/MutableMeasureContext.kt | 4 +- .../vico/core/draw/DrawContextExtensions.kt | 3 +- .../vico/core/entry/ChartEntryModel.kt | 5 ++ .../vico/core/util/ValueWrapper.kt | 6 ++ .../vico/views/chart/BaseChartView.kt | 7 +- 24 files changed, 178 insertions(+), 115 deletions(-) delete mode 100644 vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelState.kt create mode 100644 vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt index a4a66ae90..eb26b2aa8 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableFloatState import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -49,6 +50,9 @@ import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState import com.patrykandpatrick.vico.compose.extension.chartTouchEvent import com.patrykandpatrick.vico.compose.gesture.OnZoom import com.patrykandpatrick.vico.compose.layout.getMeasureContext +import com.patrykandpatrick.vico.compose.state.component1 +import com.patrykandpatrick.vico.compose.state.component2 +import com.patrykandpatrick.vico.compose.state.component3 import com.patrykandpatrick.vico.compose.style.currentChartStyle import com.patrykandpatrick.vico.core.DEF_MAX_ZOOM import com.patrykandpatrick.vico.core.DEF_MIN_ZOOM @@ -65,6 +69,8 @@ import com.patrykandpatrick.vico.core.chart.edges.FadingEdges import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.chart.values.toChartValuesProvider import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartModelProducer import com.patrykandpatrick.vico.core.extension.set @@ -130,12 +136,11 @@ public fun Chart( getXStep: ((Model) -> Float)? = null, ) { val chartValuesManager = remember(chart) { ChartValuesManager() } - val (model, oldModel) = chartModelProducer + val chartEntryModelWrapper by chartModelProducer .collectAsState(chart, chartModelProducer, diffAnimationSpec, runInitialAnimation, chartValuesManager, getXStep) - .value ChartBox(modifier = modifier) { - if (model != null) { + chartEntryModelWrapper?.also { (model, oldModel, chartValuesProvider) -> ChartImpl( chart = chart, model = model, @@ -153,7 +158,7 @@ public fun Chart( autoScaleUp = autoScaleUp, chartScrollState = chartScrollState, horizontalLayout = horizontalLayout, - chartValuesManager = chartValuesManager, + chartValuesProvider = chartValuesProvider, ) } } @@ -233,7 +238,7 @@ public fun Chart( autoScaleUp = autoScaleUp, chartScrollState = chartScrollState, horizontalLayout = horizontalLayout, - chartValuesManager = chartValuesManager, + chartValuesProvider = chartValuesManager.toChartValuesProvider(), ) } } @@ -257,7 +262,7 @@ internal fun ChartImpl( autoScaleUp: AutoScaleUp, chartScrollState: ChartScrollState = rememberChartScrollState(), horizontalLayout: HorizontalLayout, - chartValuesManager: ChartValuesManager, + chartValuesProvider: ChartValuesProvider, ) { val axisManager = remember { AxisManager() } val bounds = remember { RectF() } @@ -269,7 +274,7 @@ internal fun ChartImpl( bounds, horizontalLayout, with(LocalContext.current) { ::spToPx }, - chartValuesManager, + chartValuesProvider, ) val scrollListener = rememberScrollListener(markerTouchPoint) val lastMarkerEntryModels = remember { mutableStateOf(emptyList()) } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/entry/ChartEntryModelExtensions.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/entry/ChartEntryModelExtensions.kt index d6b2dba8a..46a9e5f44 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/entry/ChartEntryModelExtensions.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/entry/ChartEntryModelExtensions.kt @@ -25,10 +25,12 @@ import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalInspectionMode -import com.patrykandpatrick.vico.compose.state.ChartEntryModelState +import com.patrykandpatrick.vico.compose.state.ChartEntryModelWrapper +import com.patrykandpatrick.vico.compose.state.ChartEntryModelWrapperState import com.patrykandpatrick.vico.core.Animation import com.patrykandpatrick.vico.core.chart.Chart import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.toChartValuesProvider import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.entry.ChartModelProducer import com.patrykandpatrick.vico.core.entry.diff.MutableDrawingModelStore @@ -60,8 +62,8 @@ public fun ChartModelProducer.collectAsState( chartValuesManager: ChartValuesManager, getXStep: ((Model) -> Float)?, dispatcher: CoroutineDispatcher = Dispatchers.Default, -): State> { - val chartEntryModelState = remember(chart, producerKey) { ChartEntryModelState() } +): State?> { + val chartEntryModelWrapperState = remember(chart, producerKey) { ChartEntryModelWrapperState() } val modelTransformerProvider = remember(chart) { chart.modelTransformerProvider } val drawingModelStore = remember(chart) { MutableDrawingModelStore() } @@ -76,7 +78,7 @@ public fun ChartModelProducer.collectAsState( DisposableEffect(chart, producerKey, runInitialAnimation, isInPreview) { val afterUpdate: (progressModel: suspend (chartKey: Any, progress: Float) -> Unit) -> Unit = { progressModel -> if (animationSpec != null && !isInPreview && - (chartEntryModelState.value.first != null || runInitialAnimation) + (chartEntryModelWrapperState.value != null || runInitialAnimation) ) { isAnimationRunning = true mainAnimationJob = scope.launch(dispatcher) { @@ -120,7 +122,7 @@ public fun ChartModelProducer.collectAsState( isAnimationRunning = false }, startAnimation = afterUpdate, - getOldModel = { chartEntryModelState.value.first }, + getOldModel = { chartEntryModelWrapperState.value?.chartEntryModel }, modelTransformerProvider = modelTransformerProvider, drawingModelStore = drawingModelStore, updateChartValues = { model -> @@ -128,10 +130,12 @@ public fun ChartModelProducer.collectAsState( chart.updateChartValues(chartValuesManager, model, getXStep?.invoke(model)) chartValuesManager }, - onModelCreated = chartEntryModelState::set, + onModelCreated = { model -> + chartEntryModelWrapperState.set(model, chartValuesManager.toChartValuesProvider()) + }, ) } onDispose { unregisterFromUpdates(chart) } } - return chartEntryModelState + return chartEntryModelWrapperState } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/layout/MeasureContextExtensions.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/layout/MeasureContextExtensions.kt index 9b8d60140..38495b353 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/layout/MeasureContextExtensions.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/layout/MeasureContextExtensions.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.values.ChartValues -import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.context.MutableMeasureContext @@ -35,7 +35,7 @@ import com.patrykandpatrick.vico.core.context.MutableMeasureContext * @param canvasBounds the bounds of the canvas that will be used to draw the chart and its components. * @param horizontalLayout defines how the chart’s content is positioned horizontally. * @param spToPx converts dimensions from sp to px. - * @param chartValuesManager manages the chart’s [ChartValues]. + * @param chartValuesProvider provides the chart’s [ChartValues] instances. */ @Composable public fun getMeasureContext( @@ -43,7 +43,7 @@ public fun getMeasureContext( canvasBounds: RectF, horizontalLayout: HorizontalLayout, spToPx: (Float) -> Float, - chartValuesManager: ChartValuesManager, + chartValuesProvider: ChartValuesProvider, ): MutableMeasureContext = remember { MutableMeasureContext( canvasBounds = canvasBounds, @@ -52,10 +52,11 @@ public fun getMeasureContext( isHorizontalScrollEnabled = isHorizontalScrollEnabled, horizontalLayout = horizontalLayout, spToPx = spToPx, - chartValuesManager = chartValuesManager, + chartValuesProvider = chartValuesProvider, ) }.apply { this.density = LocalDensity.current.density this.isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr this.isHorizontalScrollEnabled = isHorizontalScrollEnabled + this.chartValuesProvider = chartValuesProvider } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelState.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelState.kt deleted file mode 100644 index d173624b1..000000000 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelState.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2023 by Patryk Goworowski and Patrick Michalik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.patrykandpatrick.vico.compose.state - -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.patrykandpatrick.vico.core.entry.ChartEntryModel - -internal class ChartEntryModelState : State> { - private var previousValue: T? = null - - override var value by mutableStateOf>(null to null) - private set - - fun set(value: T) { - val currentValue = this.value.first - if (value.id != currentValue?.id) previousValue = currentValue - this.value = value to previousValue - } -} diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt new file mode 100644 index 000000000..2597a2b5d --- /dev/null +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.patrykandpatrick.vico.compose.state + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.entry.ChartEntryModel + +/** + * Holds a chart’s current [ChartEntryModel] ([chartEntryModel]), previous [ChartEntryModel] + * ([previousChartEntryModel]), and [ChartValuesProvider] ([chartValuesProvider]). + */ +@Immutable +public class ChartEntryModelWrapper( + public val chartEntryModel: T, + public val previousChartEntryModel: T?, + public val chartValuesProvider: ChartValuesProvider, +) + +/** + * Returns [ChartEntryModelWrapper.chartEntryModel]. + */ +public operator fun ChartEntryModelWrapper.component1(): T = chartEntryModel + +/** + * Returns [ChartEntryModelWrapper.previousChartEntryModel]. + */ +public operator fun ChartEntryModelWrapper.component2(): T? = previousChartEntryModel + +/** + * Returns [ChartEntryModelWrapper.chartValuesProvider]. + */ +public operator fun ChartEntryModelWrapper.component3(): ChartValuesProvider = + chartValuesProvider + +internal class ChartEntryModelWrapperState : State?> { + private var previousChartEntryModel: T? = null + + override var value by mutableStateOf?>(null) + private set + + fun set(chartEntryModel: T, chartValuesProvider: ChartValuesProvider) { + val currentChartEntryModel = value?.chartEntryModel + if (chartEntryModel.id != currentChartEntryModel?.id) previousChartEntryModel = currentChartEntryModel + value = ChartEntryModelWrapper(chartEntryModel, previousChartEntryModel, chartValuesProvider) + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/DefaultHorizontalAxisItemPlacer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/DefaultHorizontalAxisItemPlacer.kt index a769ec736..8e59767ad 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/DefaultHorizontalAxisItemPlacer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/DefaultHorizontalAxisItemPlacer.kt @@ -38,7 +38,7 @@ internal class DefaultHorizontalAxisItemPlacer( visibleXRange: ClosedFloatingPointRange, fullXRange: ClosedFloatingPointRange, ): List { - val chartValues = context.chartValuesManager.getChartValues() + val chartValues = context.chartValuesProvider.getChartValues() val remainder = ((visibleXRange.start - chartValues.minX) / chartValues.xStep - offset) % spacing val firstValue = visibleXRange.start + (spacing - remainder) % spacing * chartValues.xStep val minXOffset = chartValues.minX % chartValues.xStep @@ -61,7 +61,7 @@ internal class DefaultHorizontalAxisItemPlacer( horizontalDimensions: HorizontalDimensions, fullXRange: ClosedFloatingPointRange, ): List { - val chartValues = context.chartValuesManager.getChartValues() + val chartValues = context.chartValuesProvider.getChartValues() return listOf(chartValues.minX, (chartValues.minX + chartValues.maxX).half, chartValues.maxX) } @@ -71,7 +71,7 @@ internal class DefaultHorizontalAxisItemPlacer( visibleXRange: ClosedFloatingPointRange, fullXRange: ClosedFloatingPointRange, ): List? { - val chartValues = context.chartValuesManager.getChartValues() + val chartValues = context.chartValuesProvider.getChartValues() return when (context.horizontalLayout) { is HorizontalLayout.Segmented -> { val remainder = (visibleXRange.start - fullXRange.start) % chartValues.xStep diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/HorizontalAxis.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/HorizontalAxis.kt index 321b863b4..bdec45786 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/HorizontalAxis.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/horizontal/HorizontalAxis.kt @@ -89,7 +89,7 @@ public class HorizontalAxis( val clipRestoreCount = canvas.save() val tickMarkTop = if (position.isBottom) bounds.top else bounds.bottom - axisThickness - tickLength val tickMarkBottom = tickMarkTop + axisThickness + tickLength - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() canvas.clipRect( bounds.left - itemPlacer.getStartHorizontalAxisInset(this, horizontalDimensions, tickThickness), @@ -186,7 +186,7 @@ public class HorizontalAxis( val clipRestoreCount = canvas.save() canvas.clipRect(chartBounds) - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() if (lineValues == null) { labelValues.forEach { x -> @@ -238,7 +238,7 @@ public class HorizontalAxis( private fun MeasureContext.getFullXRange( horizontalDimensions: HorizontalDimensions, ): ClosedFloatingPointRange = with(horizontalDimensions) { - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() val start = chartValues.minX - startPadding / xSpacing * chartValues.xStep val end = chartValues.maxX + endPadding / xSpacing * chartValues.xStep start..end @@ -248,7 +248,7 @@ public class HorizontalAxis( context: MeasureContext, horizontalDimensions: HorizontalDimensions, ): Float = with(context) { - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() val fullXRange = getFullXRange(horizontalDimensions) when (val constraint = sizeConstraint) { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/DefaultVerticalAxisItemPlacer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/DefaultVerticalAxisItemPlacer.kt index 76618f059..d03bfa746 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/DefaultVerticalAxisItemPlacer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/DefaultVerticalAxisItemPlacer.kt @@ -49,7 +49,7 @@ internal class DefaultVerticalAxisItemPlacer( position: AxisPosition.Vertical, ): List { if (maxItemCount == 0) return emptyList() - val chartValues = context.chartValuesManager.getChartValues(position) + val chartValues = context.chartValuesProvider.getChartValues(position) return if (chartValues.minY * chartValues.maxY >= 0) { getSimpleLabelValues(axisHeight, maxLabelHeight, chartValues) } else { @@ -61,7 +61,7 @@ internal class DefaultVerticalAxisItemPlacer( context: MeasureContext, position: AxisPosition.Vertical, ): List { - val chartValues = context.chartValuesManager.getChartValues(position) + val chartValues = context.chartValuesProvider.getChartValues(position) return listOf(chartValues.minY, (chartValues.minY + chartValues.maxY).half, chartValues.maxY) } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/VerticalAxis.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/VerticalAxis.kt index 89d6826ca..82a3c1f8f 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/VerticalAxis.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/vertical/VerticalAxis.kt @@ -103,7 +103,7 @@ public class VerticalAxis( context: ChartDrawContext, ): Unit = with(context) { var centerY: Float - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) val maxLabelHeight = getMaxLabelHeight() val lineValues = itemPlacer.getLineValues(this, bounds.height(), maxLabelHeight, position) ?: itemPlacer.getLabelValues(this, bounds.height(), maxLabelHeight, position) @@ -146,7 +146,7 @@ public class VerticalAxis( val tickRightX = tickLeftX + axisThickness + tickLength val labelX = if (areLabelsOutsideAtStartOrInsideAtEnd == isLtr) tickLeftX else tickRightX var tickCenterY: Float - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) labelValues.forEach { labelValue -> tickCenterY = bounds.bottom - bounds.height() * (labelValue - chartValues.minY) / chartValues.lengthY + @@ -289,21 +289,21 @@ public class VerticalAxis( } private fun MeasureContext.getMaxLabelHeight() = label?.let { label -> - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) itemPlacer .getHeightMeasurementLabelValues(this, position) .maxOfOrNull { value -> label.getHeight(this, valueFormatter.formatValue(value, chartValues)) } }.orZero private fun MeasureContext.getMaxLabelWidth(axisHeight: Float) = label?.let { label -> - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) itemPlacer .getWidthMeasurementLabelValues(this, axisHeight, getMaxLabelHeight(), position) .maxOfOrNull { value -> label.getWidth(this, valueFormatter.formatValue(value, chartValues)) } }.orZero private fun ChartDrawContext.getLineCanvasYCorrection(thickness: Float, y: Float): Float { - val chartValues = chartValuesManager.getChartValues(position) + val chartValues = chartValuesProvider.getChartValues(position) return if (y == chartValues.maxY && itemPlacer.getShiftTopLines(this)) -thickness.half else thickness.half } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt index bc1e9e4bd..a115306c2 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt @@ -110,7 +110,7 @@ public abstract class BaseChart : Chart, Boun context = context, bounds = bounds, markedEntries = markerModel, - chartValuesProvider = chartValuesManager, + chartValuesProvider = chartValuesProvider, ) } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt index c343535f4..f10eb041d 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt @@ -120,7 +120,7 @@ public open class ColumnChart( ): Unit = with(context) { entryLocationMap.clear() drawChartInternal( - chartValues = chartValuesManager.getChartValues(axisPosition = targetVerticalAxisPosition), + chartValues = chartValuesProvider.getChartValues(axisPosition = targetVerticalAxisPosition), model = model, drawingModel = model.drawingModelStore.getOrNull(drawingModelKey), ) @@ -276,7 +276,7 @@ public open class ColumnChart( } val text = dataLabelValueFormatter.formatValue( value = dataLabelValue, - chartValues = chartValuesManager.getChartValues(axisPosition = targetVerticalAxisPosition), + chartValues = chartValuesProvider.getChartValues(axisPosition = targetVerticalAxisPosition), ) val dataLabelWidth = textComponent.getWidth( context = this, diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/ThresholdLine.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/ThresholdLine.kt index 8a0375ab5..67cbd88ce 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/ThresholdLine.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/ThresholdLine.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 by Patryk Goworowski and Patrick Michalik. + * Copyright 2023 by Patryk Goworowski and Patrick Michalik. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,7 +99,7 @@ public data class ThresholdLine( context: ChartDrawContext, bounds: RectF, ): Unit = with(context) { - val chartValues = chartValuesManager.getChartValues() + val chartValues = chartValuesProvider.getChartValues() val valueRange = chartValues.maxY - chartValues.minY diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContext.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContext.kt index 9181c863a..0e169e7a7 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContext.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContext.kt @@ -66,7 +66,7 @@ public fun MeasureContext.getMaxScrollDistance( ): Float { val contentWidth = horizontalDimensions .run { if (zoom != null) scaled(zoom) else this } - .getContentWidth(chartValuesManager.getChartValues().getMaxMajorEntryCount()) + .getContentWidth(chartValuesProvider.getChartValues().getMaxMajorEntryCount()) return (layoutDirectionMultiplier * (contentWidth - chartWidth)).run { if (isLtr) coerceAtLeast(minimumValue = 0f) else coerceAtMost(maximumValue = 0f) @@ -90,7 +90,8 @@ public fun MeasureContext.getAutoZoom( chartBounds: RectF, autoScaleUp: AutoScaleUp, ): Float { - val contentWidth = horizontalDimensions.getContentWidth(chartValuesManager.getChartValues().getMaxMajorEntryCount()) + val contentWidth = + horizontalDimensions.getContentWidth(chartValuesProvider.getChartValues().getMaxMajorEntryCount()) return when { contentWidth < chartBounds.width() -> if (autoScaleUp == AutoScaleUp.Full) (chartBounds.width() / contentWidth).coerceAtMost(DEF_MAX_ZOOM) else 1f diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContextExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContextExtensions.kt index 175f72e73..8d2e9cd2a 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContextExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/draw/ChartDrawContextExtensions.kt @@ -93,12 +93,11 @@ public fun ChartDrawContext.drawMarker( markerTouchPoint ?.let(chart.entryLocationMap::getClosestMarkerEntryModel) ?.let { markerEntryModels -> - chartValuesManager.getChartValues() marker.draw( context = this, bounds = chart.bounds, markedEntries = markerEntryModels, - chartValuesProvider = chartValuesManager, + chartValuesProvider = chartValuesProvider, ) if (wasMarkerVisible.not()) { markerVisibilityChangeListener?.onMarkerShown( diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt index d96f0eefd..beab813eb 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt @@ -378,7 +378,7 @@ public open class LineChart( pointInfoMap: Map?, ) { if (lineSpec.point == null && lineSpec.dataLabel == null) return - val chartValues = chartValuesManager.getChartValues(targetVerticalAxisPosition) + val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) forEachPointWithinBoundsIndexed( entries = entries, @@ -440,7 +440,7 @@ public open class LineChart( previousX: Float?, nextX: Float?, ): Int { - val chartValues = chartValuesManager.getChartValues(targetVerticalAxisPosition) + val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) return when { previousX != null && nextX != null -> min(x - previousX, nextX - x) @@ -487,7 +487,7 @@ public open class LineChart( pointInfoMap: Map?, action: (index: Int, entry: ChartEntry, x: Float, y: Float, previousX: Float?, nextX: Float?) -> Unit, ) { - val chartValues = chartValuesManager.getChartValues(targetVerticalAxisPosition) + val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) val minX = chartValues.minX val maxX = chartValues.maxX diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt index ec8659e4d..4e43f247e 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt @@ -38,27 +38,13 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModel */ public class ChartValuesManager : ChartValuesProvider { - private val chartValues: MutableMap = mutableMapOf() + internal val chartValues: MutableMap = mutableMapOf() - /** - * Returns the [ChartValues] associated with the given [axisPosition]. - * @param axisPosition if this is null, the main [ChartValues] instance is returned. Otherwise, the [ChartValues] - * instance associated with the given [AxisPosition.Vertical] is returned. - */ - public fun getChartValues(axisPosition: AxisPosition.Vertical? = null): MutableChartValues = + override fun getChartValues(axisPosition: AxisPosition.Vertical?): ChartValues = chartValues[axisPosition] ?.takeIf { it.hasValuesSet } ?: chartValues.getOrPut(null) { MutableChartValues() } - override fun getChartValues(): ChartValues = getChartValues(null) - - override fun getChartValuesForAxisPosition(axisPosition: AxisPosition.Vertical): ChartValues? = - if (chartValues.containsKey(axisPosition)) { - getChartValues(axisPosition).takeIf { it.hasValuesSet } - } else { - null - } - /** * Attempts to update the stored values to the provided values. * [MutableChartValues.minX] and [MutableChartValues.minY] can be updated to a lower value. @@ -105,3 +91,18 @@ public class ChartValuesManager : ChartValuesProvider { chartValues.values.forEach { it.reset() } } } + +/** + * Creates and returns a [ChartValuesProvider] implementation with this [ChartValuesManager]’s [ChartValues] + * instances. + */ +public fun ChartValuesManager.toChartValuesProvider(): ChartValuesProvider = + object : ChartValuesProvider { + val chartValues = this@toChartValuesProvider + .chartValues + .map { (axisPosition, chartValues) -> axisPosition to chartValues.toImmutable() } + .toMap() + + override fun getChartValues(axisPosition: AxisPosition.Vertical?): ChartValues = + chartValues[axisPosition] ?: chartValues.getValue(null) + } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt index 6cc40077a..55ebfb17a 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt @@ -17,21 +17,15 @@ package com.patrykandpatrick.vico.core.chart.values import com.patrykandpatrick.vico.core.axis.AxisPosition -import com.patrykandpatrick.vico.core.chart.Chart /** - * Provides a [Chart]’s [ChartValues]. + * Provides a chart’s [ChartValues] instances. */ public interface ChartValuesProvider { /** - * Returns the [Chart]’s main [ChartValues]. + * Returns the [ChartValues] instance associated with the specified [AxisPosition.Vertical] subclass. If + * [axisPosition] is `null`, the chart’s main [ChartValues] instance is returned. */ - public fun getChartValues(): ChartValues - - /** - * Returns the [ChartValues] associated with the specified [AxisPosition.Vertical] subclass, or `null` if there is - * no such association. - */ - public fun getChartValuesForAxisPosition(axisPosition: AxisPosition.Vertical): ChartValues? + public fun getChartValues(axisPosition: AxisPosition.Vertical? = null): ChartValues } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/MutableChartValues.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/MutableChartValues.kt index c85b85841..071de4109 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/MutableChartValues.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/MutableChartValues.kt @@ -106,3 +106,16 @@ public class MutableChartValues : ChartValues { } } } + +/** + * Creates and returns an immutable copy of this [MutableChartValues] instance. + */ +public fun MutableChartValues.toImmutable(): ChartValues = + object : ChartValues { + override val minX: Float = this@toImmutable.minX + override val maxX: Float = this@toImmutable.maxX + override val xStep: Float = this@toImmutable.xStep + override val minY: Float = this@toImmutable.minY + override val maxY: Float = this@toImmutable.maxY + override val chartEntryModel: ChartEntryModel = this@toImmutable.chartEntryModel.toImmutable() + } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MeasureContext.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MeasureContext.kt index 095215493..e25d9438a 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MeasureContext.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MeasureContext.kt @@ -17,10 +17,9 @@ package com.patrykandpatrick.vico.core.context import android.graphics.RectF -import com.patrykandpatrick.vico.core.chart.Chart import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.values.ChartValues -import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider /** * [MeasureContext] holds data used by various chart components during the measuring and drawing phases. @@ -33,11 +32,11 @@ public interface MeasureContext : Extras { public val canvasBounds: RectF /** - * Manages the associated [Chart]’s [ChartValues]. + * Provides the chart’s [ChartValues] instances. * - * @see [ChartValuesManager] + * @see [ChartValuesProvider] */ - public val chartValuesManager: ChartValuesManager + public val chartValuesProvider: ChartValuesProvider /** * The pixel density. diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MutableMeasureContext.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MutableMeasureContext.kt index efbb81a39..2544eaade 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MutableMeasureContext.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/context/MutableMeasureContext.kt @@ -18,7 +18,7 @@ package com.patrykandpatrick.vico.core.context import android.graphics.RectF import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout -import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider /** * A [MeasureContext] implementation that facilitates the mutation of some of its properties. @@ -30,7 +30,7 @@ public data class MutableMeasureContext( override var isHorizontalScrollEnabled: Boolean = false, override var horizontalLayout: HorizontalLayout = HorizontalLayout.Segmented, private var spToPx: (Float) -> Float, - override val chartValuesManager: ChartValuesManager, + override var chartValuesProvider: ChartValuesProvider, ) : MeasureContext, Extras by DefaultExtras() { override fun reset() { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/draw/DrawContextExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/draw/DrawContextExtensions.kt index e816d37f8..641476470 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/draw/DrawContextExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/draw/DrawContextExtensions.kt @@ -21,6 +21,7 @@ import android.graphics.RectF import com.patrykandpatrick.vico.core.DefaultColors import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.context.DefaultExtras import com.patrykandpatrick.vico.core.context.DrawContext import com.patrykandpatrick.vico.core.context.Extras @@ -54,7 +55,7 @@ public fun drawContext( override val density: Float = density override val isLtr: Boolean = isLtr override val isHorizontalScrollEnabled: Boolean = false - override val chartValuesManager: ChartValuesManager = ChartValuesManager() + override val chartValuesProvider: ChartValuesProvider = ChartValuesManager() override val horizontalLayout: HorizontalLayout = HorizontalLayout.Segmented override fun withOtherCanvas(canvas: Canvas, block: (DrawContext) -> Unit) { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt index 7049e7ad8..07636c0ff 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt @@ -96,4 +96,9 @@ public interface ChartEntryModel { */ public val drawingModelStore: DrawingModelStore get() = DrawingModelStore.empty + + /** + * Returns an immutable copy of this [ChartEntryModel]. + */ + public fun toImmutable(): ChartEntryModel = this } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/ValueWrapper.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/ValueWrapper.kt index 8d4122e5a..776ea0483 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/ValueWrapper.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/ValueWrapper.kt @@ -29,3 +29,9 @@ public operator fun ValueWrapper.getValue(thisObj: Any?, property: KPrope public operator fun ValueWrapper.setValue(thisObj: Any?, property: KProperty<*>, value: T) { this.value = value } + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public operator fun ValueWrapper.component1(): T = value + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public operator fun ValueWrapper.component2(): (T) -> Unit = { value = it } diff --git a/vico/views/src/main/java/com/patrykandpatrick/vico/views/chart/BaseChartView.kt b/vico/views/src/main/java/com/patrykandpatrick/vico/views/chart/BaseChartView.kt index 8d7053bd9..5261bb91b 100644 --- a/vico/views/src/main/java/com/patrykandpatrick/vico/views/chart/BaseChartView.kt +++ b/vico/views/src/main/java/com/patrykandpatrick/vico/views/chart/BaseChartView.kt @@ -44,6 +44,8 @@ import com.patrykandpatrick.vico.core.chart.edges.FadingEdges import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp import com.patrykandpatrick.vico.core.chart.values.ChartValuesManager +import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.chart.values.toChartValuesProvider import com.patrykandpatrick.vico.core.component.shape.ShapeComponent import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.context.MutableMeasureContext @@ -121,7 +123,7 @@ public abstract class BaseChartView internal constructo isLtr = context.isLtr, isHorizontalScrollEnabled = false, spToPx = context::spToPx, - chartValuesManager = chartValuesManager, + chartValuesProvider = chartValuesManager, ) private val scaleGestureListener: ScaleGestureDetector.OnScaleGestureListener = @@ -167,6 +169,8 @@ public abstract class BaseChartView internal constructo private var wasZoomOverridden = false + private var chartValuesProvider: ChartValuesProvider? = null + internal val themeHandler: ThemeHandler = ThemeHandler(context, attrs, chartType) /** @@ -289,6 +293,7 @@ public abstract class BaseChartView internal constructo ) { model -> post { setModel(model = model, updateChartValues = false) + measureContext.chartValuesProvider = chartValuesManager.toChartValuesProvider() postInvalidateOnAnimation() } }