>(ChartEntryModelWrapper())
+ 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/compose/src/main/java/com/patrykandpatrick/vico/compose/state/MutableSharedState.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/MutableSharedState.kt
deleted file mode 100644
index 118cdc83f..000000000
--- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/MutableSharedState.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2022 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.MutableState
-import androidx.compose.runtime.mutableStateOf
-
-/**
- * A [MutableState] implementation that stores both its current and previous values.
- */
-public class MutableSharedState(
- value: T,
- previousValue: P,
-) : MutableState {
-
- private val backingState = mutableStateOf(value)
-
- private var mutablePreviousValue: P = previousValue
-
- /**
- * The previous value.
- */
- public val previousValue: P
- get() = mutablePreviousValue
-
- override var value: T
- get() = backingState.value
- set(value) {
- if (value !== this.value) {
- mutablePreviousValue = backingState.value
- }
- backingState.value = value
- }
-
- override fun component1(): T = backingState.component1()
-
- override fun component2(): (T) -> Unit = backingState.component2()
-}
-
-/**
- * Creates a [MutableSharedState] instance with the provided current value and a previous value of `null`.
- */
-public fun mutableSharedStateOf(value: T): MutableSharedState =
- MutableSharedState(
- value = value,
- previousValue = null,
- )
-
-/**
- * Creates a [MutableSharedState] with the provided current and previous values.
- */
-public fun mutableSharedStateOf(
- value: T,
- previousValue: P,
-): MutableSharedState
=
- MutableSharedState(
- value = value,
- previousValue = previousValue,
- )
diff --git a/vico/core/build.gradle b/vico/core/build.gradle
index d44f20257..8d01976c9 100644
--- a/vico/core/build.gradle
+++ b/vico/core/build.gradle
@@ -60,6 +60,8 @@ afterEvaluate {
dependencies {
+ implementation libs.androidXAnnotation
+ implementation libs.coroutinesCore
implementation libs.kotlinStdLib
testImplementation libs.JUnit
testImplementation libs.JUnitExt
diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisItemPlacer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisItemPlacer.kt
index 2aa9a50f7..b636ec48e 100644
--- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisItemPlacer.kt
+++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisItemPlacer.kt
@@ -45,6 +45,20 @@ public interface AxisItemPlacer {
context: ChartDrawContext,
): Boolean = true
+ /**
+ * Returns a boolean indicating whether the [HorizontalAxis] should reserve room for a label for
+ * [ChartValues.minX]. If `true` is returned, indicating that this behavior is desired, then [getLabelValues]
+ * should request a label for [ChartValues.minX].
+ */
+ public fun getAddFirstLabelPadding(context: MeasureContext): Boolean
+
+ /**
+ * Returns a boolean indicating whether the [HorizontalAxis] should reserve room for a label for
+ * [ChartValues.maxX]. If `true` is returned, indicating that this behavior is desired, then [getLabelValues]
+ * should request a label for [ChartValues.maxX].
+ */
+ public fun getAddLastLabelPadding(context: MeasureContext): Boolean
+
/**
* Returns, as a list, the _x_ values for which labels are to be displayed, restricted to [visibleXRange] and
* with two extra values on either side (if applicable).
@@ -113,9 +127,15 @@ public interface AxisItemPlacer {
* [HorizontalLayout.FullWidth], their corresponding ticks and guidelines) to skip from the start.
* [shiftExtremeTicks] defines whether ticks whose _x_ values are bounds of the _x_-axis value range should
* be shifted to the edges of the axis bounds, to be aligned with the vertical axes.
+ * [addExtremeLabelPadding] specifies whether, for [HorizontalLayout.FullWidth], padding should be added for
+ * the first and last labels, ensuring their visibility.
*/
- public fun default(spacing: Int = 1, offset: Int = 0, shiftExtremeTicks: Boolean = true): Horizontal =
- DefaultHorizontalAxisItemPlacer(spacing, offset, shiftExtremeTicks)
+ public fun default(
+ spacing: Int = 1,
+ offset: Int = 0,
+ shiftExtremeTicks: Boolean = true,
+ addExtremeLabelPadding: Boolean = false,
+ ): Horizontal = DefaultHorizontalAxisItemPlacer(spacing, offset, shiftExtremeTicks, addExtremeLabelPadding)
}
}
diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisRenderer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisRenderer.kt
index 6935ab3c9..5a5d19e10 100644
--- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisRenderer.kt
+++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisRenderer.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.
@@ -18,8 +18,10 @@ package com.patrykandpatrick.vico.core.axis
import android.graphics.RectF
import com.patrykandpatrick.vico.core.chart.Chart
+import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions
import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext
import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter
+import com.patrykandpatrick.vico.core.context.MeasureContext
import com.patrykandpatrick.vico.core.dimensions.BoundsAware
/**
@@ -53,4 +55,9 @@ public interface AxisRenderer : BoundsAware, ChartInset
* The bounds ([RectF]) passed here define the area where the [AxisRenderer] shouldn’t draw anything.
*/
public fun setRestrictedBounds(vararg bounds: RectF?)
+
+ /**
+ * Updates the chart’s [MutableHorizontalDimensions] instance.
+ */
+ public fun updateHorizontalDimensions(context: MeasureContext, horizontalDimensions: MutableHorizontalDimensions)
}
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..842e4b631 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
@@ -28,17 +28,27 @@ internal class DefaultHorizontalAxisItemPlacer(
private val spacing: Int,
private val offset: Int,
private val shiftExtremeTicks: Boolean,
+ private val addExtremeLabelPadding: Boolean,
) : AxisItemPlacer.Horizontal {
override fun getShiftExtremeTicks(context: ChartDrawContext): Boolean = shiftExtremeTicks
+ override fun getAddFirstLabelPadding(context: MeasureContext) =
+ context.horizontalLayout is HorizontalLayout.FullWidth && addExtremeLabelPadding && offset == 0
+
+ override fun getAddLastLabelPadding(context: MeasureContext): Boolean {
+ val chartValues = context.chartValuesProvider.getChartValues()
+ return context.horizontalLayout is HorizontalLayout.FullWidth && addExtremeLabelPadding &&
+ (chartValues.maxX - chartValues.minX - chartValues.xStep * offset) % (chartValues.xStep * spacing) == 0f
+ }
+
@Suppress("LoopWithTooManyJumpStatements")
override fun getLabelValues(
context: ChartDrawContext,
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 +71,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 +81,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..80d298364 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
@@ -22,11 +22,13 @@ import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.AxisRenderer
import com.patrykandpatrick.vico.core.axis.setTo
import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions
+import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions
import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext
import com.patrykandpatrick.vico.core.chart.insets.Insets
import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout
import com.patrykandpatrick.vico.core.component.text.VerticalPosition
import com.patrykandpatrick.vico.core.context.MeasureContext
+import com.patrykandpatrick.vico.core.extension.ceil
import com.patrykandpatrick.vico.core.extension.doubled
import com.patrykandpatrick.vico.core.extension.getStart
import com.patrykandpatrick.vico.core.extension.half
@@ -89,7 +91,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),
@@ -114,7 +116,8 @@ public class HorizontalAxis(
layoutDirectionMultiplier
val previousX = labelValues.getOrNull(index - 1) ?: (fullXRange.start.doubled - x)
val nextX = labelValues.getOrNull(index + 1) ?: (fullXRange.endInclusive.doubled - x)
- val maxWidth = (min(x - previousX, nextX - x) / chartValues.xStep * horizontalDimensions.xSpacing).toInt()
+ val maxWidth =
+ (min(x - previousX, nextX - x) / chartValues.xStep * horizontalDimensions.xSpacing).ceil.toInt()
label?.drawText(
context = context,
@@ -186,7 +189,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 ->
@@ -224,6 +227,33 @@ public class HorizontalAxis(
override fun drawAboveChart(context: ChartDrawContext): Unit = Unit
+ override fun updateHorizontalDimensions(
+ context: MeasureContext,
+ horizontalDimensions: MutableHorizontalDimensions,
+ ) {
+ val chartValues = context.chartValuesProvider.getChartValues()
+ horizontalDimensions.ensureValuesAtLeast(
+ unscalableStartPadding = label
+ .takeIf { itemPlacer.getAddFirstLabelPadding(context) }
+ ?.getWidth(
+ context = context,
+ text = valueFormatter.formatValue(chartValues.minX, chartValues),
+ pad = true,
+ )
+ ?.half
+ .orZero,
+ unscalableEndPadding = label
+ .takeIf { itemPlacer.getAddLastLabelPadding(context) }
+ ?.getWidth(
+ context = context,
+ text = valueFormatter.formatValue(chartValues.maxX, chartValues),
+ pad = true,
+ )
+ ?.half
+ .orZero,
+ )
+ }
+
override fun getInsets(
context: MeasureContext,
outInsets: Insets,
@@ -238,7 +268,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 +278,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..06d723685 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)
}
@@ -69,24 +69,30 @@ internal class DefaultVerticalAxisItemPlacer(
verticalLabelPosition: VerticalAxis.VerticalLabelPosition,
maxLabelHeight: Float,
maxLineThickness: Float,
- ) = when (verticalLabelPosition) {
- VerticalAxis.VerticalLabelPosition.Top ->
+ ) = when {
+ maxItemCount == 0 -> 0f
+
+ verticalLabelPosition == VerticalAxis.VerticalLabelPosition.Top ->
maxLabelHeight + (if (shiftTopLines) maxLineThickness else -maxLineThickness).half
- VerticalAxis.VerticalLabelPosition.Center ->
+ verticalLabelPosition == VerticalAxis.VerticalLabelPosition.Center ->
(max(maxLabelHeight, maxLineThickness) + if (shiftTopLines) maxLineThickness else -maxLineThickness).half
- VerticalAxis.VerticalLabelPosition.Bottom -> if (shiftTopLines) maxLineThickness else 0f
+ else -> if (shiftTopLines) maxLineThickness else 0f
}
override fun getBottomVerticalAxisInset(
verticalLabelPosition: VerticalAxis.VerticalLabelPosition,
maxLabelHeight: Float,
maxLineThickness: Float,
- ): Float = when (verticalLabelPosition) {
- VerticalAxis.VerticalLabelPosition.Top -> maxLineThickness
- VerticalAxis.VerticalLabelPosition.Center -> (maxOf(maxLabelHeight, maxLineThickness) + maxLineThickness).half
- VerticalAxis.VerticalLabelPosition.Bottom -> maxLabelHeight + maxLineThickness.half
+ ): Float = when {
+ maxItemCount == 0 -> 0f
+ verticalLabelPosition == VerticalAxis.VerticalLabelPosition.Top -> maxLineThickness
+
+ verticalLabelPosition == VerticalAxis.VerticalLabelPosition.Center ->
+ (maxOf(maxLabelHeight, maxLineThickness) + maxLineThickness).half
+
+ else -> maxLabelHeight + maxLineThickness.half
}
private fun getSimpleLabelValues(axisHeight: Float, maxLabelHeight: Float, chartValues: ChartValues): List {
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..30337bcf9 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
@@ -27,6 +27,7 @@ import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis.HorizontalLabel
import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis.HorizontalLabelPosition.Outside
import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis.VerticalLabelPosition.Center
import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions
+import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions
import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext
import com.patrykandpatrick.vico.core.chart.insets.HorizontalInsets
import com.patrykandpatrick.vico.core.chart.insets.Insets
@@ -103,7 +104,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 +147,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 +
@@ -182,6 +183,11 @@ public class VerticalAxis(
}
}
+ override fun updateHorizontalDimensions(
+ context: MeasureContext,
+ horizontalDimensions: MutableHorizontalDimensions,
+ ): Unit = Unit
+
private fun ChartDrawContext.drawLabel(
label: TextComponent,
labelText: CharSequence,
@@ -289,21 +295,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/Chart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt
index e206dc58f..c41514734 100644
--- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt
+++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt
@@ -19,16 +19,20 @@ package com.patrykandpatrick.vico.core.chart
import com.patrykandpatrick.vico.core.chart.column.ColumnChart
import com.patrykandpatrick.vico.core.chart.composed.ComposedChart
import com.patrykandpatrick.vico.core.chart.decoration.Decoration
-import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions
+import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions
import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext
import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter
import com.patrykandpatrick.vico.core.chart.line.LineChart
import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider
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.dimensions.BoundsAware
import com.patrykandpatrick.vico.core.entry.ChartEntryModel
+import com.patrykandpatrick.vico.core.entry.diff.DrawingModel
+import com.patrykandpatrick.vico.core.entry.diff.ExtraStore
+import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore
import com.patrykandpatrick.vico.core.marker.Marker
internal const val AXIS_VALUES_DEPRECATION_MESSAGE: String = "Axis values should be overridden via " +
@@ -178,13 +182,13 @@ public interface Chart : BoundsAware, ChartInsetter {
public fun removePersistentMarker(x: Float)
/**
- * Called to get the [HorizontalDimensions] of this chart. The [HorizontalDimensions] influence the look of various
- * parts of the chart.
- *
- * @param context holds data used for component measurements.
- * @param model holds data about the [Chart]’s entries.
+ * Updates the chart’s [MutableHorizontalDimensions] instance.
*/
- public fun getHorizontalDimensions(context: MeasureContext, model: Model): HorizontalDimensions
+ public fun updateHorizontalDimensions(
+ context: MeasureContext,
+ horizontalDimensions: MutableHorizontalDimensions,
+ model: Model,
+ )
/**
* Updates the [ChartValues] stored in the provided [ChartValuesManager] instance to this [Chart]’s [ChartValues].
@@ -194,6 +198,46 @@ public interface Chart : BoundsAware, ChartInsetter {
* @param xStep the overridden _x_ step (or `null` if no override has occurred).
*/
public fun updateChartValues(chartValuesManager: ChartValuesManager, model: Model, xStep: Float?)
+
+ /**
+ * Provides the [Chart]’s [ModelTransformer].
+ */
+ public val modelTransformerProvider: ModelTransformerProvider
+
+ /**
+ * Provides a [Chart]’s [ModelTransformer].
+ */
+ public interface ModelTransformerProvider {
+ /**
+ * Returns the [ModelTransformer].
+ */
+ public fun getModelTransformer(): ModelTransformer
+ }
+
+ /**
+ * Transforms [Model]s into [DrawingModel]s.
+ */
+ public abstract class ModelTransformer {
+ /**
+ * Used for writing to and reading from the host’s [ExtraStore].
+ */
+ protected abstract val key: ExtraStore.Key<*>
+
+ /**
+ * Prepares the [Chart] for a difference animation.
+ */
+ public abstract fun prepareForTransformation(
+ oldModel: Model?,
+ newModel: Model?,
+ extraStore: MutableExtraStore,
+ chartValuesProvider: ChartValuesProvider,
+ )
+
+ /**
+ * Carries out the pending difference animation. [fraction] is the animation progress.
+ */
+ public abstract suspend fun transform(extraStore: MutableExtraStore, fraction: Float)
+ }
}
/**
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 5fefc0283..65ab4ba2c 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
@@ -20,8 +20,8 @@ import com.patrykandpatrick.vico.core.DefaultDimens
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.AxisRenderer
import com.patrykandpatrick.vico.core.chart.BaseChart
+import com.patrykandpatrick.vico.core.chart.Chart
import com.patrykandpatrick.vico.core.chart.composed.ComposedChart
-import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions
import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions
import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext
import com.patrykandpatrick.vico.core.chart.forEachInAbsolutelyIndexed
@@ -29,6 +29,7 @@ import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout
import com.patrykandpatrick.vico.core.chart.put
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.component.shape.LineComponent
import com.patrykandpatrick.vico.core.component.text.TextComponent
import com.patrykandpatrick.vico.core.component.text.VerticalPosition
@@ -36,6 +37,10 @@ import com.patrykandpatrick.vico.core.component.text.inBounds
import com.patrykandpatrick.vico.core.context.MeasureContext
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.ChartEntryModel
+import com.patrykandpatrick.vico.core.entry.diff.DefaultDrawingModelInterpolator
+import com.patrykandpatrick.vico.core.entry.diff.DrawingModelInterpolator
+import com.patrykandpatrick.vico.core.entry.diff.ExtraStore
+import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore
import com.patrykandpatrick.vico.core.extension.doubled
import com.patrykandpatrick.vico.core.extension.getRepeating
import com.patrykandpatrick.vico.core.extension.getStart
@@ -60,6 +65,7 @@ import kotlin.math.abs
* respective columns.
* @param dataLabelValueFormatter the [ValueFormatter] to use for data labels.
* @param dataLabelRotationDegrees the rotation of data labels (in degrees).
+ * @param drawingModelInterpolator interpolates the [ColumnChart]’s [ColumnChartDrawingModel]s.
*/
public open class ColumnChart(
public var columns: List,
@@ -71,6 +77,10 @@ public open class ColumnChart(
public var dataLabelVerticalPosition: VerticalPosition = VerticalPosition.Top,
public var dataLabelValueFormatter: ValueFormatter = DecimalFormatValueFormatter(),
public var dataLabelRotationDegrees: Float = 0f,
+ public var drawingModelInterpolator: DrawingModelInterpolator<
+ ColumnChartDrawingModel.ColumnInfo,
+ ColumnChartDrawingModel,
+ > = DefaultDrawingModelInterpolator(),
) : BaseChart() {
/**
@@ -105,6 +115,8 @@ public open class ColumnChart(
*/
protected val horizontalDimensions: MutableHorizontalDimensions = MutableHorizontalDimensions()
+ protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key()
+
override val entryLocationMap: HashMap> = HashMap()
override fun drawChart(
@@ -113,8 +125,9 @@ public open class ColumnChart(
): Unit = with(context) {
entryLocationMap.clear()
drawChartInternal(
- chartValues = chartValuesManager.getChartValues(axisPosition = targetVerticalAxisPosition),
+ chartValues = chartValuesProvider.getChartValues(axisPosition = targetVerticalAxisPosition),
model = model,
+ drawingModel = model.extraStore.getOrNull(drawingModelKey),
)
heightMap.clear()
}
@@ -122,6 +135,7 @@ public open class ColumnChart(
protected open fun ChartDrawContext.drawChartInternal(
chartValues: ChartValues,
model: ChartEntryModel,
+ drawingModel: ColumnChartDrawingModel?,
) {
val yRange = (chartValues.maxY - chartValues.minY).takeIf { it != 0f } ?: return
val heightMultiplier = bounds.height() / yRange
@@ -141,7 +155,8 @@ public open class ColumnChart(
entryCollection.forEachInAbsolutelyIndexed(chartValues.minX..chartValues.maxX) { entryIndex, entry ->
- height = abs(entry.y) * heightMultiplier
+ val columnInfo = drawingModel?.getOrNull(index)?.get(entry.x)
+ height = (columnInfo?.height ?: (abs(entry.y) / chartValues.lengthY)) * bounds.height()
val xSpacingMultiplier = (entry.x - chartValues.minX) / chartValues.xStep
check(xSpacingMultiplier % 1f == 0f) { "Each entry’s x value must be a multiple of the x step." }
columnCenterX = drawingStart +
@@ -150,20 +165,20 @@ public open class ColumnChart(
when (mergeMode) {
MergeMode.Stack -> {
- val (stackedNegY, stackedPosY) = heightMap.getOrElse(entry.x) { 0f to 0f }
+ val (stackedNegHeight, stackedPosHeight) = heightMap.getOrElse(entry.x) { 0f to 0f }
columnBottom = zeroLinePosition +
if (entry.y < 0f) {
- height + abs(stackedNegY) * heightMultiplier
+ height + stackedNegHeight
} else {
- -stackedPosY * heightMultiplier
+ -stackedPosHeight
}
columnTop = (columnBottom - height).coerceAtMost(columnBottom)
heightMap[entry.x] =
if (entry.y < 0f) {
- stackedNegY + entry.y to stackedPosY
+ stackedNegHeight + height to stackedPosHeight
} else {
- stackedNegY to stackedPosY + entry.y
+ stackedNegHeight to stackedPosHeight + height
}
}
@@ -185,7 +200,7 @@ public open class ColumnChart(
)
) {
updateMarkerLocationMap(entry, columnSignificantY, columnCenterX, column, entryIndex)
- column.drawVertical(this, columnTop, columnBottom, columnCenterX, zoom)
+ column.drawVertical(this, columnTop, columnBottom, columnCenterX, zoom, drawingModel?.opacity ?: 1f)
}
if (mergeMode == MergeMode.Grouped) {
@@ -266,7 +281,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,
@@ -332,26 +347,44 @@ public open class ColumnChart(
)
}
- override fun getHorizontalDimensions(
+ override fun updateHorizontalDimensions(
context: MeasureContext,
+ horizontalDimensions: MutableHorizontalDimensions,
model: ChartEntryModel,
- ): HorizontalDimensions = with(context) {
- val columnCollectionWidth = getColumnCollectionWidth(if (model.entries.isNotEmpty()) model.entries.size else 1)
- horizontalDimensions.apply {
- xSpacing = columnCollectionWidth + spacingDp.pixels
+ ) {
+ with(context) {
+ val columnCollectionWidth =
+ getColumnCollectionWidth(if (model.entries.isNotEmpty()) model.entries.size else 1)
+ val xSpacing = columnCollectionWidth + spacingDp.pixels
when (val horizontalLayout = horizontalLayout) {
is HorizontalLayout.Segmented -> {
- scalableStartPadding = xSpacing.half
- scalableEndPadding = scalableStartPadding
+ horizontalDimensions.ensureValuesAtLeast(
+ xSpacing = xSpacing,
+ scalableStartPadding = xSpacing.half,
+ scalableEndPadding = xSpacing.half,
+ )
}
is HorizontalLayout.FullWidth -> {
- scalableStartPadding = columnCollectionWidth.half + horizontalLayout.startPaddingDp.pixels
- scalableEndPadding = columnCollectionWidth.half + horizontalLayout.endPaddingDp.pixels
+ horizontalDimensions.ensureValuesAtLeast(
+ xSpacing = xSpacing,
+ scalableStartPadding = columnCollectionWidth.half +
+ horizontalLayout.scalableStartPaddingDp.pixels,
+ scalableEndPadding = columnCollectionWidth.half + horizontalLayout.scalableEndPaddingDp.pixels,
+ unscalableStartPadding = horizontalLayout.unscalableStartPaddingDp.pixels,
+ unscalableEndPadding = horizontalLayout.unscalableEndPaddingDp.pixels,
+ )
}
}
}
}
+ override val modelTransformerProvider: Chart.ModelTransformerProvider = object : Chart.ModelTransformerProvider {
+ private val modelTransformer =
+ ColumnChartModelTransformer(drawingModelKey, { targetVerticalAxisPosition }, { drawingModelInterpolator })
+
+ override fun getModelTransformer(): Chart.ModelTransformer = modelTransformer
+ }
+
protected open fun MeasureContext.getColumnCollectionWidth(
entryCollectionSize: Int,
): Float = when (mergeMode) {
@@ -416,4 +449,41 @@ public open class ColumnChart(
Stack -> model.stackedPositiveY
}
}
+
+ protected class ColumnChartModelTransformer(
+ override val key: ExtraStore.Key,
+ private val getTargetVerticalAxisPosition: () -> AxisPosition.Vertical?,
+ private val getDrawingModelInterpolator: () -> DrawingModelInterpolator<
+ ColumnChartDrawingModel.ColumnInfo,
+ ColumnChartDrawingModel,
+ >,
+ ) : Chart.ModelTransformer() {
+
+ override fun prepareForTransformation(
+ oldModel: ChartEntryModel?,
+ newModel: ChartEntryModel?,
+ extraStore: MutableExtraStore,
+ chartValuesProvider: ChartValuesProvider,
+ ) {
+ getDrawingModelInterpolator().setModels(
+ extraStore.getOrNull(key),
+ newModel?.toDrawingModel(chartValuesProvider.getChartValues(getTargetVerticalAxisPosition())),
+ )
+ }
+
+ override suspend fun transform(extraStore: MutableExtraStore, fraction: Float) {
+ getDrawingModelInterpolator()
+ .transform(fraction)
+ ?.let { extraStore[key] = it }
+ ?: extraStore.remove(key)
+ }
+
+ private fun ChartEntryModel.toDrawingModel(chartValues: ChartValues): ColumnChartDrawingModel = entries
+ .map { series ->
+ series.associate { entry ->
+ entry.x to ColumnChartDrawingModel.ColumnInfo(abs(entry.y) / chartValues.lengthY)
+ }
+ }
+ .let(::ColumnChartDrawingModel)
+ }
}
diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChartDrawingModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChartDrawingModel.kt
new file mode 100644
index 000000000..3c6c26b66
--- /dev/null
+++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChartDrawingModel.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.core.chart.column
+
+import com.patrykandpatrick.vico.core.entry.diff.DrawingModel
+import com.patrykandpatrick.vico.core.extension.orZero
+
+/**
+ * Houses drawing information for a [ColumnChart]. [opacity] is the columns’ opacity.
+ */
+public class ColumnChartDrawingModel(entries: List