From 4e74816714e157faf838b4ca2858787cd21bf0cb Mon Sep 17 00:00:00 2001 From: Patrick Michalik <120058021+patrickmichalik@users.noreply.github.com> Date: Sun, 26 Nov 2023 10:08:28 +0100 Subject: [PATCH] Rework core APIs Co-authored-by: Patryk Goworowski --- settings.gradle.kts | 2 +- .../{Charts.kt => CartesianChartHost.kt} | 113 +++--- .../chart/cartesian/CartesianChart.kt} | 22 +- ...ColumnChart.kt => ColumnCartesianLayer.kt} | 39 +- .../chart/entry/ChartEntryModelExtensions.kt | 51 ++- .../{LineChart.kt => LineCartesianLayer.kt} | 49 +-- .../compose/chart/scroll/ChartScrollSpec.kt | 18 +- .../compose/chart/scroll/ChartScrollState.kt | 4 +- .../layout/MeasureContextExtensions.kt | 9 +- .../state/CartesianChartModelWrapper.kt | 64 ++++ .../compose/state/ChartEntryModelWrapper.kt | 65 ---- .../vico/compose/style/ChartStyle.kt | 4 +- .../vico/core/axis/AxisItemPlacer.kt | 15 +- .../vico/core/axis/AxisManager.kt | 12 +- .../vico/core/axis/AxisPosition.kt | 30 +- .../vico/core/axis/AxisRenderer.kt | 11 +- .../DefaultHorizontalAxisItemPlacer.kt | 48 ++- .../core/axis/horizontal/HorizontalAxis.kt | 15 +- .../vertical/DefaultVerticalAxisItemPlacer.kt | 34 +- .../vico/core/axis/vertical/VerticalAxis.kt | 26 +- .../vico/core/chart/BaseCartesianLayer.kt | 53 +++ .../vico/core/chart/BaseChart.kt | 139 ------- .../vico/core/chart/CartesianLayer.kt | 74 ++++ .../patrykandpatrick/vico/core/chart/Chart.kt | 213 ----------- .../vico/core/chart/ChartExtensions.kt | 4 +- .../vico/core/chart/DefaultPointConnector.kt | 8 +- .../vico/core/chart/EntryModelExtensions.kt | 56 +-- .../vico/core/chart/LineSpecExtensions.kt | 12 +- ...ColumnChart.kt => ColumnCartesianLayer.kt} | 156 ++++---- ...kt => ColumnCartesianLayerDrawingModel.kt} | 14 +- .../chart/column/ColumnCartesianLayerModel.kt | 133 +++++++ .../core/chart/composed/CartesianChart.kt | 166 +++++++++ .../chart/composed/CartesianChartModel.kt | 70 ++++ .../vico/core/chart/composed/ComposedChart.kt | 169 --------- .../chart/composed/ComposedChartEntryModel.kt | 30 -- .../chart/composed/ComposedChartExtensions.kt | 44 --- .../vico/core/chart/decoration/Decoration.kt | 18 +- .../core/chart/decoration/ThresholdLine.kt | 10 +- .../chart/dimensions/HorizontalDimensions.kt | 11 +- .../vico/core/chart/draw/ChartDrawContext.kt | 12 +- .../chart/draw/ChartDrawContextExtensions.kt | 15 +- .../vico/core/chart/insets/ChartInsetter.kt | 14 +- .../core/chart/insets/HorizontalInsets.kt | 6 +- .../core/chart/layout/HorizontalLayout.kt | 18 +- .../{LineChart.kt => LineCartesianLayer.kt} | 207 +++++------ ...l.kt => LineCartesianLayerDrawingModel.kt} | 14 +- ...ons.kt => LineCartesianLayerExtensions.kt} | 4 +- .../chart/line/LineCartesianLayerModel.kt | 106 ++++++ .../core/chart/values/AxisValuesOverrider.kt | 27 +- .../vico/core/chart/values/ChartValues.kt | 78 ++-- .../core/chart/values/ChartValuesManager.kt | 107 ------ .../core/chart/values/ChartValuesProvider.kt | 39 -- .../core/chart/values/MutableChartValues.kt | 92 +++-- .../core/component/marker/MarkerComponent.kt | 5 +- .../vico/core/context/MeasureContext.kt | 7 +- .../core/context/MutableMeasureContext.kt | 4 +- .../vico/core/draw/DrawContextExtensions.kt | 4 +- .../vico/core/entry/CartesianLayerModel.kt | 86 +++++ .../vico/core/entry/ChartEntry.kt | 51 --- .../vico/core/entry/ChartEntryExtensions.kt | 82 +---- .../vico/core/entry/ChartEntryModel.kt | 104 ------ .../core/entry/ChartEntryModelProducer.kt | 233 ------------ .../vico/core/entry/ChartModelProducer.kt | 77 ---- .../vico/core/entry/EntryListExtensions.kt | 48 --- .../composed/CartesianChartModelProducer.kt | 274 ++++++++++++++ .../ComposedChartEntryModelProducer.kt | 343 ------------------ .../composed/ComposedEntryListExtensions.kt | 35 -- .../vico/core/entry/diff/DrawingModel.kt | 6 +- .../vico/core/extension/MapExtensions.kt | 7 +- .../formatter/DecimalFormatValueFormatter.kt | 4 +- .../core/formatter/DefaultValueFormatter.kt | 4 +- .../PercentageFormatValueFormatter.kt | 6 +- .../vico/core/formatter/ValueFormatter.kt | 4 +- .../vico/core/layout/VirtualLayout.kt | 8 +- .../marker/DefaultMarkerLabelFormatter.kt | 10 + .../vico/core/marker/Marker.kt | 18 +- .../vico/core/scroll/AutoScrollCondition.kt | 25 +- .../vico/core/util/RandomEntriesGenerator.kt | 74 ---- 78 files changed, 1671 insertions(+), 2598 deletions(-) rename vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/{Charts.kt => CartesianChartHost.kt} (84%) rename vico/{core/src/main/java/com/patrykandpatrick/vico/core/entry/FloatEntry.kt => compose/src/main/java/com/patrykandpatrick/vico/compose/chart/cartesian/CartesianChart.kt} (55%) rename vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/{ColumnChart.kt => ColumnCartesianLayer.kt} (72%) rename vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/{LineChart.kt => LineCartesianLayer.kt} (75%) create mode 100644 vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/CartesianChartModelWrapper.kt delete mode 100644 vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseCartesianLayer.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/CartesianLayer.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt rename vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/{ColumnChart.kt => ColumnCartesianLayer.kt} (77%) rename vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/{ColumnChartDrawingModel.kt => ColumnCartesianLayerDrawingModel.kt} (66%) create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnCartesianLayerModel.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/CartesianChart.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/CartesianChartModel.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChart.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChartEntryModel.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChartExtensions.kt rename vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/{LineChart.kt => LineCartesianLayer.kt} (74%) rename vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/{LineChartDrawingModel.kt => LineCartesianLayerDrawingModel.kt} (64%) rename vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/{LineChartExtensions.kt => LineCartesianLayerExtensions.kt} (89%) create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerModel.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/CartesianLayerModel.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntry.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModelProducer.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartModelProducer.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/EntryListExtensions.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/CartesianChartModelProducer.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedChartEntryModelProducer.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedEntryListExtensions.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/util/RandomEntriesGenerator.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 821eeb011..8506366be 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,4 +34,4 @@ dependencyResolutionManagement { rootProject.name = "Vico" -include("sample", "vico", "vico:compose", "vico:compose-m2", "vico:compose-m3", "vico:core", "vico:views") +include("vico", "vico:compose", "vico:compose-m2", "vico:compose-m3", "vico:core") 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/CartesianChartHost.kt similarity index 84% rename from vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/Charts.kt rename to vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/CartesianChartHost.kt index 1e5b796a5..c96c1f149 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/CartesianChartHost.kt @@ -39,11 +39,9 @@ import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.patrykandpatrick.vico.compose.chart.column.columnChart import com.patrykandpatrick.vico.compose.chart.entry.collectAsState import com.patrykandpatrick.vico.compose.chart.entry.defaultDiffAnimationSpec import com.patrykandpatrick.vico.compose.chart.layout.segmented -import com.patrykandpatrick.vico.compose.chart.line.lineChart import com.patrykandpatrick.vico.compose.chart.scroll.ChartScrollSpec import com.patrykandpatrick.vico.compose.chart.scroll.ChartScrollState import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec @@ -61,7 +59,8 @@ import com.patrykandpatrick.vico.core.DefaultDimens import com.patrykandpatrick.vico.core.axis.AxisManager import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisRenderer -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChartModel import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.chartDrawContext import com.patrykandpatrick.vico.core.chart.draw.drawMarker @@ -70,11 +69,10 @@ import com.patrykandpatrick.vico.core.chart.draw.getMaxScrollDistance 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.chart.values.ChartValues +import com.patrykandpatrick.vico.core.chart.values.MutableChartValues +import com.patrykandpatrick.vico.core.chart.values.toImmutable +import com.patrykandpatrick.vico.core.entry.composed.CartesianChartModelProducer import com.patrykandpatrick.vico.core.extension.set import com.patrykandpatrick.vico.core.extension.spToPx import com.patrykandpatrick.vico.core.layout.VirtualLayout @@ -89,11 +87,10 @@ import com.patrykandpatrick.vico.core.util.setValue import kotlinx.coroutines.launch /** - * Displays a chart. + * Displays a [CartesianChart]. * - * @param chart the chart itself (excluding axes, markers, etc.). You can use [lineChart] or [columnChart], or provide a - * custom [Chart] implementation. - * @param chartModelProducer creates and updates the [ChartEntryModel] for the chart. + * @param chart the [CartesianChart]. + * @param chartModelProducer creates and updates the [CartesianChartModel]. * @param modifier the modifier to be applied to the chart. * @param startAxis the axis displayed at the start of the chart. * @param topAxis the axis displayed at the top of the chart. @@ -113,13 +110,13 @@ import kotlinx.coroutines.launch * @param chartScrollState houses information on the chart’s scroll state. Allows for programmatic scrolling. * @param horizontalLayout defines how the chart’s content is positioned horizontally. * @param getXStep overrides the _x_ step (the difference between the _x_ values of neighboring major entries). If this - * is null, the default _x_ step ([ChartEntryModel.xGcd]) is used. - * @param placeholder shown when no [ChartEntryModel] is available. + * is null, the default _x_ step ([CartesianChartModel.xDeltaGcd]) is used. + * @param placeholder shown when no [CartesianChartModel] is available. */ @Composable -public fun Chart( - chart: Chart, - chartModelProducer: ChartModelProducer, +public fun CartesianChartHost( + chart: CartesianChart, + chartModelProducer: CartesianChartModelProducer, modifier: Modifier = Modifier, startAxis: AxisRenderer? = null, topAxis: AxisRenderer? = null, @@ -128,7 +125,7 @@ public fun Chart( marker: Marker? = null, markerVisibilityChangeListener: MarkerVisibilityChangeListener? = null, legend: Legend? = null, - chartScrollSpec: ChartScrollSpec = rememberChartScrollSpec(), + chartScrollSpec: ChartScrollSpec = rememberChartScrollSpec(), isZoomEnabled: Boolean = true, diffAnimationSpec: AnimationSpec? = defaultDiffAnimationSpec, runInitialAnimation: Boolean = true, @@ -136,20 +133,20 @@ public fun Chart( autoScaleUp: AutoScaleUp = AutoScaleUp.Full, chartScrollState: ChartScrollState = rememberChartScrollState(), horizontalLayout: HorizontalLayout = HorizontalLayout.segmented(), - getXStep: ((Model) -> Float)? = null, + getXStep: ((CartesianChartModel) -> Float)? = null, placeholder: @Composable BoxScope.() -> Unit = {}, ) { - val chartValuesManager = remember(chart) { ChartValuesManager() } + val mutableChartValues = remember(chart) { MutableChartValues() } val chartEntryModelWrapper by chartModelProducer - .collectAsState(chart, chartModelProducer, diffAnimationSpec, runInitialAnimation, chartValuesManager, getXStep) - val (chartEntryModel, previousChartEntryModel, chartValuesProvider) = chartEntryModelWrapper + .collectAsState(chart, chartModelProducer, diffAnimationSpec, runInitialAnimation, mutableChartValues, getXStep) + val (model, previousModel, chartValues) = chartEntryModelWrapper - ChartBox(modifier = modifier) { - if (chartEntryModel != null) { - ChartImpl( + CartesianChartHostBox(modifier = modifier) { + if (model != null) { + CartesianChartHostImpl( chart = chart, - model = chartEntryModel, - oldModel = previousChartEntryModel, + model = model, + oldModel = previousModel, startAxis = startAxis, topAxis = topAxis, endAxis = endAxis, @@ -163,7 +160,7 @@ public fun Chart( autoScaleUp = autoScaleUp, chartScrollState = chartScrollState, horizontalLayout = horizontalLayout, - chartValuesProvider = chartValuesProvider, + chartValues = chartValues, ) } else { placeholder() @@ -172,14 +169,11 @@ public fun Chart( } /** - * Displays a chart. + * Displays a [CartesianChart]. This function accepts a [CartesianChartModel]. For dynamic data, use the function + * overload that accepts a [CartesianChartModelProducer] instance. * - * This function accepts a [ChartEntryModel]. For dynamic data, use the function overload that accepts a - * [ChartModelProducer] instance. - * - * @param chart the chart itself (excluding axes, markers, etc.). You can use [lineChart] or [columnChart], or provide a - * custom [Chart] implementation. - * @param model the [ChartEntryModel] for the chart. + * @param chart the [CartesianChart]. + * @param model the [CartesianChartModel]. * @param modifier the modifier to be applied to the chart. * @param startAxis the axis displayed at the start of the chart. * @param topAxis the axis displayed at the top of the chart. @@ -190,7 +184,7 @@ public fun Chart( * @param legend an optional legend for the chart. * @param chartScrollSpec houses scrolling-related settings. * @param isZoomEnabled whether zooming in and out is enabled. - * @param oldModel the chart’s previous [ChartEntryModel]. This is used to determine whether to perform an automatic + * @param oldModel the chart’s previous [CartesianChartModel]. This is used to determine whether to perform an automatic * scroll. * @param fadingEdges applies a horizontal fade to the edges of the chart area for scrollable charts. * @param autoScaleUp defines whether the content of the chart should be scaled up when the dimensions are such that, at @@ -198,13 +192,13 @@ public fun Chart( * @param chartScrollState houses information on the chart’s scroll state. Allows for programmatic scrolling. * @param horizontalLayout defines how the chart’s content is positioned horizontally. * @param getXStep overrides the _x_ step (the difference between the _x_ values of neighboring major entries). If this - * is null, the default _x_ step ([ChartEntryModel.xGcd]) is used. + * is null, the default _x_ step ([CartesianChartModel.xDeltaGcd]) is used. */ @Composable @SuppressLint("RememberReturnType") -public fun Chart( - chart: Chart, - model: Model, +public fun CartesianChartHost( + chart: CartesianChart, + model: CartesianChartModel, modifier: Modifier = Modifier, startAxis: AxisRenderer? = null, topAxis: AxisRenderer? = null, @@ -213,22 +207,22 @@ public fun Chart( marker: Marker? = null, markerVisibilityChangeListener: MarkerVisibilityChangeListener? = null, legend: Legend? = null, - chartScrollSpec: ChartScrollSpec = rememberChartScrollSpec(), + chartScrollSpec: ChartScrollSpec = rememberChartScrollSpec(), isZoomEnabled: Boolean = true, - oldModel: Model? = null, + oldModel: CartesianChartModel? = null, fadingEdges: FadingEdges? = null, autoScaleUp: AutoScaleUp = AutoScaleUp.Full, chartScrollState: ChartScrollState = rememberChartScrollState(), horizontalLayout: HorizontalLayout = HorizontalLayout.segmented(), - getXStep: ((Model) -> Float)? = null, + getXStep: ((CartesianChartModel) -> Float)? = null, ) { - val chartValuesManager = remember(chart) { ChartValuesManager() } - remember(chartValuesManager, model, getXStep) { - chartValuesManager.resetChartValues() - chart.updateChartValues(chartValuesManager, model, getXStep?.invoke(model)) + val chartValues = remember(chart) { MutableChartValues() } + remember(chartValues, model, getXStep) { + chartValues.reset() + chart.updateChartValues(chartValues, model, getXStep?.invoke(model)) } - ChartBox(modifier = modifier) { - ChartImpl( + CartesianChartHostBox(modifier = modifier) { + CartesianChartHostImpl( chart = chart, model = model, startAxis = startAxis, @@ -245,16 +239,16 @@ public fun Chart( autoScaleUp = autoScaleUp, chartScrollState = chartScrollState, horizontalLayout = horizontalLayout, - chartValuesProvider = chartValuesManager.toChartValuesProvider(), + chartValues = chartValues.toImmutable(), ) } } @Suppress("LongMethod") @Composable -internal fun ChartImpl( - chart: Chart, - model: Model, +internal fun CartesianChartHostImpl( + chart: CartesianChart, + model: CartesianChartModel, startAxis: AxisRenderer?, topAxis: AxisRenderer?, endAxis: AxisRenderer?, @@ -262,14 +256,14 @@ internal fun ChartImpl( marker: Marker?, markerVisibilityChangeListener: MarkerVisibilityChangeListener?, legend: Legend?, - chartScrollSpec: ChartScrollSpec, + chartScrollSpec: ChartScrollSpec, isZoomEnabled: Boolean, - oldModel: Model? = null, + oldModel: CartesianChartModel? = null, fadingEdges: FadingEdges?, autoScaleUp: AutoScaleUp, chartScrollState: ChartScrollState = rememberChartScrollState(), horizontalLayout: HorizontalLayout, - chartValuesProvider: ChartValuesProvider, + chartValues: ChartValues, ) { val axisManager = remember { AxisManager() } val bounds = remember { RectF() } @@ -281,7 +275,7 @@ internal fun ChartImpl( bounds, horizontalLayout, with(LocalContext.current) { ::spToPx }, - chartValuesProvider, + chartValues, ) val scrollListener = rememberScrollListener(markerTouchPoint) val lastMarkerEntryModels = remember { mutableStateOf(emptyList()) } @@ -377,7 +371,7 @@ internal fun ChartImpl( val count = if (fadingEdges != null) chartDrawContext.saveLayer() else -1 axisManager.drawBehindChart(chartDrawContext) - chart.drawScrollableContent(chartDrawContext, model) + chart.draw(chartDrawContext, model) fadingEdges?.apply { applyFadingEdges(chartDrawContext, chart.bounds) @@ -385,7 +379,6 @@ internal fun ChartImpl( } axisManager.drawAboveChart(chartDrawContext) - chart.drawNonScrollableContent(chartDrawContext, model) legend?.draw(chartDrawContext) if (marker != null) { @@ -406,7 +399,7 @@ internal fun ChartImpl( } @Composable -internal fun ChartBox( +internal fun CartesianChartHostBox( modifier: Modifier, content: @Composable BoxScope.() -> Unit, ) { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/FloatEntry.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/cartesian/CartesianChart.kt similarity index 55% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/FloatEntry.kt rename to vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/cartesian/CartesianChart.kt index 469fbb097..c317f196e 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/FloatEntry.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/cartesian/CartesianChart.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. @@ -14,18 +14,14 @@ * limitations under the License. */ -package com.patrykandpatrick.vico.core.entry +package com.patrykandpatrick.vico.compose.chart.cartesian + +import androidx.compose.runtime.Composable +import com.patrykandpatrick.vico.core.chart.CartesianLayer +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart /** - * The default implementation of [ChartEntry]. + * Creates and remembers a [CartesianChart]. */ -public data class FloatEntry( - override val x: Float, - override val y: Float, -) : ChartEntry { - - override fun withY(y: Float): ChartEntry = FloatEntry( - x = x, - y = y, - ) -} +@Composable +public fun rememberCartesianChart(vararg layers: CartesianLayer<*>): CartesianChart = CartesianChart(*layers) diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnChart.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnCartesianLayer.kt similarity index 72% rename from vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnChart.kt rename to vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnCartesianLayer.kt index b55bec4b4..aa70be1d4 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnChart.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/column/ColumnCartesianLayer.kt @@ -19,36 +19,30 @@ package com.patrykandpatrick.vico.compose.chart.column import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.unit.Dp -import com.patrykandpatrick.vico.compose.chart.Chart import com.patrykandpatrick.vico.compose.style.currentChartStyle import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisRenderer -import com.patrykandpatrick.vico.core.chart.column.ColumnChart -import com.patrykandpatrick.vico.core.chart.column.ColumnChart.MergeMode -import com.patrykandpatrick.vico.core.chart.column.ColumnChartDrawingModel -import com.patrykandpatrick.vico.core.chart.composed.ComposedChart -import com.patrykandpatrick.vico.core.chart.decoration.Decoration +import com.patrykandpatrick.vico.core.chart.column.ColumnCartesianLayer +import com.patrykandpatrick.vico.core.chart.column.ColumnCartesianLayer.MergeMode +import com.patrykandpatrick.vico.core.chart.column.ColumnCartesianLayerDrawingModel +import com.patrykandpatrick.vico.core.chart.column.ColumnCartesianLayerModel import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.component.shape.LineComponent import com.patrykandpatrick.vico.core.component.text.TextComponent import com.patrykandpatrick.vico.core.component.text.VerticalPosition -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.formatter.ValueFormatter -import com.patrykandpatrick.vico.core.marker.Marker /** - * Creates a [ColumnChart]. + * Creates a [ColumnCartesianLayer]. * * @param columns the [LineComponent] instances to use for columns. This list is iterated through as many times * as necessary for each column collection. If the list contains a single element, all columns have the same appearance. * @param spacing the distance between neighboring column collections. * @param innerSpacing the distance between neighboring grouped columns. * @param mergeMode defines how columns should be drawn in column collections. - * @param decorations the list of [Decoration]s that will be added to the [ColumnChart]. - * @param persistentMarkers maps x-axis values to persistent [Marker]s. * @param dataLabel an optional [TextComponent] to use for data labels. * @param dataLabelVerticalPosition the vertical position of data labels relative to the top of their * respective columns. @@ -56,29 +50,26 @@ import com.patrykandpatrick.vico.core.marker.Marker * @param dataLabelRotationDegrees the rotation of data labels (in degrees). * @param axisValuesOverrider overrides the minimum and maximum x-axis and y-axis values. * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided - * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. - * @param drawingModelInterpolator interpolates the [ColumnChart]’s [ColumnChartDrawingModel]s. - * - * @see Chart - * @see ColumnChart + * value will use the [ChartValues] provided by this chart. + * @param drawingModelInterpolator interpolates the [ColumnCartesianLayer]’s [ColumnCartesianLayerDrawingModel]s. */ @Composable -public fun columnChart( +public fun rememberColumnCartesianLayer( columns: List = currentChartStyle.columnChart.columns, spacing: Dp = currentChartStyle.columnChart.outsideSpacing, innerSpacing: Dp = currentChartStyle.columnChart.innerSpacing, mergeMode: MergeMode = currentChartStyle.columnChart.mergeMode, - decorations: List? = null, - persistentMarkers: Map? = null, targetVerticalAxisPosition: AxisPosition.Vertical? = null, dataLabel: TextComponent? = currentChartStyle.columnChart.dataLabel, dataLabelVerticalPosition: VerticalPosition = currentChartStyle.columnChart.dataLabelVerticalPosition, dataLabelValueFormatter: ValueFormatter = currentChartStyle.columnChart.dataLabelValueFormatter, dataLabelRotationDegrees: Float = currentChartStyle.columnChart.dataLabelRotationDegrees, - axisValuesOverrider: AxisValuesOverrider? = null, - drawingModelInterpolator: DrawingModelInterpolator = - remember { DefaultDrawingModelInterpolator() }, -): ColumnChart = remember { ColumnChart() }.apply { + axisValuesOverrider: AxisValuesOverrider? = null, + drawingModelInterpolator: DrawingModelInterpolator< + ColumnCartesianLayerDrawingModel.ColumnInfo, + ColumnCartesianLayerDrawingModel, + > = remember { DefaultDrawingModelInterpolator() }, +): ColumnCartesianLayer = remember { ColumnCartesianLayer() }.apply { this.columns = columns this.spacingDp = spacing.value this.innerSpacingDp = innerSpacing.value @@ -90,6 +81,4 @@ public fun columnChart( this.axisValuesOverrider = axisValuesOverrider this.targetVerticalAxisPosition = targetVerticalAxisPosition this.drawingModelInterpolator = drawingModelInterpolator - decorations?.also(::setDecorations) - persistentMarkers?.also(::setPersistentMarkers) } 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 0c0b2f859..ac58c7cba 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,15 +25,15 @@ 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.ChartEntryModelWrapper +import com.patrykandpatrick.vico.compose.state.CartesianChartModelWrapper 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.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.chart.composed.CartesianChart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChartModel +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.chart.values.MutableChartValues +import com.patrykandpatrick.vico.core.chart.values.toImmutable +import com.patrykandpatrick.vico.core.entry.composed.CartesianChartModelProducer import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -50,22 +50,19 @@ import kotlinx.coroutines.runBlocking public val defaultDiffAnimationSpec: AnimationSpec = tween(durationMillis = Animation.DIFF_DURATION) /** - * Observes the data provided by this [ChartModelProducer] and launches an animation for each [ChartEntryModel] update. - * - * @see ChartModelProducer + * Observes the data provided by this [CartesianChartModelProducer] and launches an animation for each update. */ @Composable -public fun ChartModelProducer.collectAsState( - chart: Chart, +public fun CartesianChartModelProducer.collectAsState( + chart: CartesianChart, producerKey: Any, animationSpec: AnimationSpec? = defaultDiffAnimationSpec, runInitialAnimation: Boolean = true, - chartValuesManager: ChartValuesManager, - getXStep: ((Model) -> Float)?, + mutableChartValues: MutableChartValues, + getXStep: ((CartesianChartModel) -> Float)?, dispatcher: CoroutineDispatcher = Dispatchers.Default, -): State> { - val chartEntryModelWrapperState = remember(chart, producerKey) { ChartEntryModelWrapperState() } - val modelTransformerProvider = remember(chart) { chart.modelTransformerProvider } +): State { + val chartEntryModelWrapperState = remember(chart, producerKey) { ChartEntryModelWrapperState() } val extraStore = remember(chart) { MutableExtraStore() } val scope = rememberCoroutineScope() val isInPreview = LocalInspectionMode.current @@ -75,10 +72,10 @@ public fun ChartModelProducer.collectAsState( var finalAnimationFrameJob: Job? = null var isAnimationRunning: Boolean var isAnimationFrameGenerationRunning = false - var chartValuesProvider: ChartValuesProvider = ChartValuesProvider.Empty + var chartValues: ChartValues = ChartValues.Empty val startAnimation: (transformModel: suspend (key: Any, fraction: Float) -> Unit) -> Unit = { transformModel -> if (animationSpec != null && !isInPreview && - (chartEntryModelWrapperState.value.chartEntryModel != null || runInitialAnimation) + (chartEntryModelWrapperState.value.model != null || runInitialAnimation) ) { isAnimationRunning = true mainAnimationJob = scope.launch(dispatcher) { @@ -125,20 +122,20 @@ public fun ChartModelProducer.collectAsState( isAnimationFrameGenerationRunning = false }, startAnimation = startAnimation, - getOldModel = { chartEntryModelWrapperState.value.chartEntryModel }, - modelTransformerProvider = modelTransformerProvider, + prepareForTransformation = chart::prepareForTransformation, + transform = chart::transform, extraStore = extraStore, updateChartValues = { model -> - chartValuesManager.resetChartValues() + mutableChartValues.reset() if (model != null) { - chart.updateChartValues(chartValuesManager, model, getXStep?.invoke(model)) - chartValuesManager.toChartValuesProvider() + chart.updateChartValues(mutableChartValues, model, getXStep?.invoke(model)) + mutableChartValues.toImmutable() } else { - ChartValuesProvider.Empty - }.also { provider -> chartValuesProvider = provider } + ChartValues.Empty + }.also { values -> chartValues = values } }, ) { chartEntryModel -> - chartEntryModelWrapperState.set(chartEntryModel, chartValuesProvider) + chartEntryModelWrapperState.set(chartEntryModel, chartValues) } } onDispose { diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineChart.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineCartesianLayer.kt similarity index 75% rename from vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineChart.kt rename to vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineCartesianLayer.kt index 12deda097..4d4894019 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineChart.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/line/LineCartesianLayer.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.patrykandpatrick.vico.compose.chart.Chart import com.patrykandpatrick.vico.compose.component.shape.shader.fromBrush import com.patrykandpatrick.vico.compose.style.currentChartStyle import com.patrykandpatrick.vico.core.DefaultAlpha @@ -33,12 +32,10 @@ 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.DefaultPointConnector -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.line.LineChart -import com.patrykandpatrick.vico.core.chart.line.LineChart.LineSpec -import com.patrykandpatrick.vico.core.chart.line.LineChartDrawingModel +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayer +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayer.LineSpec +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayerDrawingModel +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayerModel import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.component.Component @@ -46,51 +43,42 @@ import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShader import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShaders import com.patrykandpatrick.vico.core.component.text.TextComponent import com.patrykandpatrick.vico.core.component.text.VerticalPosition -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.formatter.DecimalFormatValueFormatter import com.patrykandpatrick.vico.core.formatter.ValueFormatter -import com.patrykandpatrick.vico.core.marker.Marker /** - * Creates a [LineChart]. + * Creates a [LineCartesianLayer]. * - * @param lines the [LineChart.LineSpec]s to use for the lines. This list is iterated through as many times as there - * are lines. + * @param lines the [LineCartesianLayer.LineSpec]s to use for the lines. This list is iterated through as many times as + * there are lines. * @param spacing the distance between neighboring major entries’ points. - * @param decorations the list of [Decoration]s that will be added to the [LineChart]. - * @param persistentMarkers maps x-axis values to persistent [Marker]s. * @param axisValuesOverrider overrides the minimum and maximum x-axis and y-axis values. * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided - * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. - * @param drawingModelInterpolator interpolates the [LineChart]’s [LineChartDrawingModel]s. - * - * @see Chart - * @see ColumnChart + * value will use the [ChartValues] provided by this chart. + * @param drawingModelInterpolator interpolates the [LineCartesianLayer]’s [LineCartesianLayerDrawingModel]s. */ @Composable -public fun lineChart( +public fun rememberLineCartesianLayer( lines: List = currentChartStyle.lineChart.lines, spacing: Dp = currentChartStyle.lineChart.spacing, - decorations: List? = null, - persistentMarkers: Map? = null, - axisValuesOverrider: AxisValuesOverrider? = null, + axisValuesOverrider: AxisValuesOverrider? = null, targetVerticalAxisPosition: AxisPosition.Vertical? = null, - drawingModelInterpolator: DrawingModelInterpolator = - remember { DefaultDrawingModelInterpolator() }, -): LineChart = remember { LineChart() }.apply { + drawingModelInterpolator: DrawingModelInterpolator< + LineCartesianLayerDrawingModel.PointInfo, + LineCartesianLayerDrawingModel, + > = remember { DefaultDrawingModelInterpolator() }, +): LineCartesianLayer = remember { LineCartesianLayer() }.apply { this.lines = lines this.spacingDp = spacing.value this.axisValuesOverrider = axisValuesOverrider this.targetVerticalAxisPosition = targetVerticalAxisPosition this.drawingModelInterpolator = drawingModelInterpolator - decorations?.also(::setDecorations) - persistentMarkers?.also(::setPersistentMarkers) } /** - * Creates a [LineChart.LineSpec] for use in [LineChart]s. + * Creates a [LineCartesianLayer.LineSpec] for use in [LineCartesianLayer]s. * * @param lineColor the color of the line. * @param lineThickness the thickness of the line. @@ -103,9 +91,6 @@ public fun lineChart( * @param dataLabelValueFormatter the [ValueFormatter] to use for data labels. * @param dataLabelRotationDegrees the rotation of data labels in degrees. * @param pointConnector the [LineSpec.PointConnector] for the line. - * - * @see LineChart - * @see LineChart.LineSpec */ public fun lineSpec( lineColor: Color, diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollSpec.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollSpec.kt index 1cc6d6d29..aeac7e080 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollSpec.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollSpec.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. @@ -24,7 +24,7 @@ import androidx.compose.foundation.gestures.stopScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember -import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.chart.composed.CartesianChartModel import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition import com.patrykandpatrick.vico.core.scroll.InitialScroll @@ -37,10 +37,10 @@ import com.patrykandpatrick.vico.core.scroll.InitialScroll * @property autoScrollAnimationSpec the [AnimationSpec] to use for automatic scrolling. */ @Stable -public class ChartScrollSpec( +public class ChartScrollSpec( public val isScrollEnabled: Boolean, public val initialScroll: InitialScroll, - public val autoScrollCondition: AutoScrollCondition, + public val autoScrollCondition: AutoScrollCondition, public val autoScrollAnimationSpec: AnimationSpec, ) { @@ -48,8 +48,8 @@ public class ChartScrollSpec( * Performs an automatic scroll. */ public suspend fun performAutoScroll( - model: Model, - oldModel: Model?, + model: CartesianChartModel, + oldModel: CartesianChartModel?, chartScrollState: ChartScrollState, ) { if (autoScrollCondition.shouldPerformAutoScroll(model, oldModel)) { @@ -72,12 +72,12 @@ public class ChartScrollSpec( * Creates and remembers an instance of [ChartScrollSpec]. */ @Composable -public fun rememberChartScrollSpec( +public fun rememberChartScrollSpec( isScrollEnabled: Boolean = true, initialScroll: InitialScroll = InitialScroll.Start, - autoScrollCondition: AutoScrollCondition = AutoScrollCondition.Never, + autoScrollCondition: AutoScrollCondition = AutoScrollCondition.Never, autoScrollAnimationSpec: AnimationSpec = spring(), -): ChartScrollSpec = remember( +): ChartScrollSpec = remember( isScrollEnabled, initialScroll, autoScrollCondition, diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollState.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollState.kt index 1f74a6009..81160e232 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollState.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/chart/scroll/ChartScrollState.kt @@ -22,7 +22,7 @@ import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember -import com.patrykandpatrick.vico.compose.chart.Chart +import com.patrykandpatrick.vico.compose.chart.CartesianChartHost import com.patrykandpatrick.vico.core.extension.rangeWith import com.patrykandpatrick.vico.core.scroll.InitialScroll import com.patrykandpatrick.vico.core.scroll.ScrollListener @@ -30,7 +30,7 @@ import com.patrykandpatrick.vico.core.scroll.ScrollListenerHost import kotlin.math.abs /** - * Houses information on a [Chart]’s scroll state. Allows for programmatic scrolling. + * Houses information on a [CartesianChartHost]’s scroll state. Allows for programmatic scrolling. */ public class ChartScrollState : ScrollableState, ScrollListenerHost { 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 b20687319..ef3317e73 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,6 @@ 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.ChartValuesProvider import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.context.MutableMeasureContext @@ -35,7 +34,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 chartValuesProvider provides the chart’s [ChartValues] instances. + * @param chartValues the chart’s [ChartValues]. */ @Composable public fun getMeasureContext( @@ -43,14 +42,14 @@ public fun getMeasureContext( canvasBounds: RectF, horizontalLayout: HorizontalLayout, spToPx: (Float) -> Float, - chartValuesProvider: ChartValuesProvider, + chartValues: ChartValues, ): MutableMeasureContext = remember { MutableMeasureContext( canvasBounds = canvasBounds, density = 0f, isLtr = true, spToPx = spToPx, - chartValuesProvider = chartValuesProvider, + chartValues = chartValues, ) }.apply { this.density = LocalDensity.current.density @@ -58,5 +57,5 @@ public fun getMeasureContext( this.isHorizontalScrollEnabled = isHorizontalScrollEnabled this.horizontalLayout = horizontalLayout this.spToPx = spToPx - this.chartValuesProvider = chartValuesProvider + this.chartValues = chartValues } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/CartesianChartModelWrapper.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/CartesianChartModelWrapper.kt new file mode 100644 index 000000000..68ea29b3c --- /dev/null +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/CartesianChartModelWrapper.kt @@ -0,0 +1,64 @@ +/* + * 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.composed.CartesianChartModel +import com.patrykandpatrick.vico.core.chart.values.ChartValues + +/** + * Holds a chart’s current [CartesianChartModel] ([model]), previous [CartesianChartModel] ([previousModel]), and + * [ChartValues] ([chartValues]). + */ +@Immutable +public class CartesianChartModelWrapper( + public val model: CartesianChartModel? = null, + public val previousModel: CartesianChartModel? = null, + public val chartValues: ChartValues = ChartValues.Empty, +) + +/** + * Returns [CartesianChartModelWrapper.model]. + */ +public operator fun CartesianChartModelWrapper.component1(): CartesianChartModel? = model + +/** + * Returns [CartesianChartModelWrapper.previousModel]. + */ +public operator fun CartesianChartModelWrapper.component2(): CartesianChartModel? = previousModel + +/** + * Returns [CartesianChartModelWrapper.chartValues]. + */ +public operator fun CartesianChartModelWrapper.component3(): ChartValues = chartValues + +internal class ChartEntryModelWrapperState : State { + private var previousModel: CartesianChartModel? = null + + override var value by mutableStateOf(CartesianChartModelWrapper()) + private set + + fun set(model: CartesianChartModel?, chartValues: ChartValues) { + val currentChartEntryModel = value.model + if (model?.id != currentChartEntryModel?.id) previousModel = currentChartEntryModel + value = CartesianChartModelWrapper(model, previousModel, chartValues) + } +} 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 deleted file mode 100644 index dc5580210..000000000 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/state/ChartEntryModelWrapper.kt +++ /dev/null @@ -1,65 +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.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? = null, - public val previousChartEntryModel: T? = null, - public val chartValuesProvider: ChartValuesProvider = ChartValuesProvider.Empty, -) - -/** - * 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>(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/style/ChartStyle.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/style/ChartStyle.kt index ee18b7ecd..5400fa5ae 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/style/ChartStyle.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/style/ChartStyle.kt @@ -40,8 +40,8 @@ import com.patrykandpatrick.vico.core.DefaultDimens import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter import com.patrykandpatrick.vico.core.axis.formatter.DecimalFormatAxisValueFormatter -import com.patrykandpatrick.vico.core.chart.column.ColumnChart.MergeMode -import com.patrykandpatrick.vico.core.chart.line.LineChart.LineSpec +import com.patrykandpatrick.vico.core.chart.column.ColumnCartesianLayer.MergeMode +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayer.LineSpec import com.patrykandpatrick.vico.core.component.shape.LineComponent import com.patrykandpatrick.vico.core.component.shape.Shape import com.patrykandpatrick.vico.core.component.shape.ShapeComponent 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 8a6813e13..39d02ef72 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 @@ -21,7 +21,7 @@ import com.patrykandpatrick.vico.core.axis.horizontal.DefaultHorizontalAxisItemP import com.patrykandpatrick.vico.core.axis.horizontal.HorizontalAxis import com.patrykandpatrick.vico.core.axis.vertical.DefaultVerticalAxisItemPlacer import com.patrykandpatrick.vico.core.axis.vertical.VerticalAxis -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout @@ -132,9 +132,10 @@ public interface AxisItemPlacer { */ public interface Vertical { /** - * Returns a boolean indicating whether to shift the lines whose _y_ values are equal to [ChartValues.maxY], if - * such lines are present, such that they’re immediately above the [Chart]’s bounds. If the chart has a top - * axis, the shifted tick will then be aligned with this axis, and the shifted guideline will be hidden. + * Returns a boolean indicating whether to shift the lines whose _y_ values are equal to + * [ChartValues.YRange.maxY], if such lines are present, such that they’re immediately above the + * [CartesianChart]’s bounds. If the chart has a top axis, the shifted tick will then be aligned with this axis, + * and the shifted guideline will be hidden. */ public fun getShiftTopLines(chartDrawContext: ChartDrawContext): Boolean = true @@ -202,9 +203,9 @@ public interface AxisItemPlacer { * Creates a base [AxisItemPlacer.Vertical] implementation. [maxItemCount] is the maximum number of labels * (and their corresponding line pairs) to be displayed. The actual item count is the greatest number * smaller than or equal to [maxItemCount] for which no overlaps occur. [shiftTopLines] defines whether - * to shift the lines whose _y_ values are equal to [ChartValues.maxY], if such lines are present, such that - * they’re immediately above the [Chart]’s bounds. If the chart has a top axis, the shifted tick will then - * be aligned with this axis, and the shifted guideline will be hidden. + * to shift the lines whose _y_ values are equal to [ChartValues.YRange.maxY], if such lines are present, + * such that they’re immediately above the [CartesianChart]’s bounds. If the chart has a top axis, the + * shifted tick will then be aligned with this axis, and the shifted guideline will be hidden. */ public fun default(maxItemCount: Int = DEF_LABEL_COUNT, shiftTopLines: Boolean = true): Vertical = DefaultVerticalAxisItemPlacer(maxItemCount, shiftTopLines) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisManager.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisManager.kt index 9e0c690db..3a66365ad 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisManager.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisManager.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. @@ -17,7 +17,7 @@ package com.patrykandpatrick.vico.core.axis import android.graphics.RectF -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter import com.patrykandpatrick.vico.core.chart.insets.Insets @@ -195,8 +195,8 @@ public open class AxisManager { } /** - * Called before the associated [Chart] is drawn. This forwards a call to all [Axis] subclasses that causes them to - * be drawn behind the chart. + * Called before the associated [CartesianChart] is drawn. This forwards a call to all [Axis] subclasses that causes + * them to be drawn behind the chart. * * @param context holds the information necessary to draw the axes. * @@ -209,8 +209,8 @@ public open class AxisManager { } /** - * Called after the associated [Chart] is drawn. This forwards a call to all [Axis] subclasses that causes them to - * be drawn above the chart. + * Called after the associated [CartesianChart] is drawn. This forwards a call to all [Axis] subclasses that causes + * them to be drawn above the chart. * * @param context holds the information necessary to draw the axes. * diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisPosition.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisPosition.kt index c4c57191e..9c0d9e032 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisPosition.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/axis/AxisPosition.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. @@ -16,77 +16,77 @@ package com.patrykandpatrick.vico.core.axis -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart /** - * Defines the position of an axis relative to its [Chart]. + * Defines the position of an axis relative to its [CartesianChart]. */ public sealed class AxisPosition { /** - * Whether the axis is at the top of its [Chart]. + * Whether the axis is at the top of its [CartesianChart]. */ public val isTop: Boolean get() = this is Horizontal.Top /** - * Whether the axis is at the bottom of its [Chart]. + * Whether the axis is at the bottom of its [CartesianChart]. */ public val isBottom: Boolean get() = this is Horizontal.Bottom /** - * Whether the axis is at the start of its [Chart]. + * Whether the axis is at the start of its [CartesianChart]. */ public val isStart: Boolean get() = this is Vertical.Start /** - * Whether the axis is at the end of its [Chart]. + * Whether the axis is at the end of its [CartesianChart]. */ public val isEnd: Boolean get() = this is Vertical.End /** - * Whether the axis is on the left of its [Chart]. The layout direction is considered here. + * Whether the axis is on the left of its [CartesianChart]. The layout direction is considered here. */ public fun isLeft(isLtr: Boolean): Boolean = this is Vertical.Start && isLtr || this is Vertical.End && isLtr.not() /** - * Whether the axis is on the right of its [Chart]. The layout direction is considered here. + * Whether the axis is on the right of its [CartesianChart]. The layout direction is considered here. */ public fun isRight(isLtr: Boolean): Boolean = this is Vertical.End && isLtr || this is Vertical.Start && isLtr.not() /** - * Defines the position of a horizontal axis relative to its [Chart]. + * Defines the position of a horizontal axis relative to its [CartesianChart]. */ public sealed class Horizontal : AxisPosition() { /** - * The horizontal axis will be placed at the top of its [Chart]. + * The horizontal axis will be placed at the top of its [CartesianChart]. */ public object Top : Horizontal() /** - * The horizontal axis will be placed at the bottom of its [Chart]. + * The horizontal axis will be placed at the bottom of its [CartesianChart]. */ public object Bottom : Horizontal() } /** - * Defines the position of a vertical axis relative to its [Chart]. + * Defines the position of a vertical axis relative to its [CartesianChart]. */ public sealed class Vertical : AxisPosition() { /** - * The vertical axis will be placed at the start of its [Chart]. + * The vertical axis will be placed at the start of its [CartesianChart]. */ public object Start : Vertical() /** - * The vertical axis will be placed at the end of its [Chart]. + * The vertical axis will be placed at the end of its [CartesianChart]. */ public object End : Vertical() } 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 5a5d19e10..dabb4fad8 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 @@ -17,7 +17,7 @@ package com.patrykandpatrick.vico.core.axis import android.graphics.RectF -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter @@ -30,13 +30,13 @@ import com.patrykandpatrick.vico.core.dimensions.BoundsAware public interface AxisRenderer : BoundsAware, ChartInsetter { /** - * Defines the position of the axis relative to the [Chart]. + * Defines the position of the axis relative to the [CartesianChart]. */ public val position: Position /** - * Called before the [Chart] is drawn. Implementations should rely on this function to draw themselves, unless they - * need to draw something above the [Chart]. + * Called before the [CartesianChart] is drawn. Implementations should rely on this function to draw themselves, + * unless they need to draw something above the [CartesianChart]. * * @param context holds the information needed to draw the axis. * @@ -45,7 +45,8 @@ public interface AxisRenderer : BoundsAware, ChartInset public fun drawBehindChart(context: ChartDrawContext) /** - * Called after the [Chart] is drawn. Implementations can use this function to draw content above the [Chart]. + * Called after the [CartesianChart] is drawn. Implementations can use this function to draw content above the + * [CartesianChart]. * * @param context holds the information needed to draw the axis. */ 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 842e4b631..fb6fbd9c1 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 @@ -36,9 +36,8 @@ internal class DefaultHorizontalAxisItemPlacer( 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 && + override fun getAddLastLabelPadding(context: MeasureContext): Boolean = with(context) { + context.horizontalLayout is HorizontalLayout.FullWidth && addExtremeLabelPadding && (chartValues.maxX - chartValues.minX - chartValues.xStep * offset) % (chartValues.xStep * spacing) == 0f } @@ -48,41 +47,40 @@ internal class DefaultHorizontalAxisItemPlacer( visibleXRange: ClosedFloatingPointRange, fullXRange: ClosedFloatingPointRange, ): List { - 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 - val values = mutableListOf() - var multiplier = -LABEL_OVERFLOW_SIZE - var hasEndOverflow = false - while (true) { - var potentialValue = firstValue + multiplier++ * spacing * chartValues.xStep - potentialValue = chartValues.xStep * ((potentialValue - minXOffset) / chartValues.xStep).round + minXOffset - if (potentialValue < chartValues.minX || potentialValue == fullXRange.start) continue - if (potentialValue > chartValues.maxX || potentialValue == fullXRange.endInclusive) break - values += potentialValue - if (potentialValue > visibleXRange.endInclusive && hasEndOverflow.also { hasEndOverflow = true }) break + with(context) { + 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 + val values = mutableListOf() + var multiplier = -LABEL_OVERFLOW_SIZE + var hasEndOverflow = false + while (true) { + var potentialValue = firstValue + multiplier++ * spacing * chartValues.xStep + potentialValue = chartValues.xStep * ((potentialValue - minXOffset) / chartValues.xStep).round + + minXOffset + if (potentialValue < chartValues.minX || potentialValue == fullXRange.start) continue + if (potentialValue > chartValues.maxX || potentialValue == fullXRange.endInclusive) break + values += potentialValue + if (potentialValue > visibleXRange.endInclusive && hasEndOverflow.also { hasEndOverflow = true }) break + } + return values } - return values } override fun getMeasuredLabelValues( context: MeasureContext, horizontalDimensions: HorizontalDimensions, fullXRange: ClosedFloatingPointRange, - ): List { - val chartValues = context.chartValuesProvider.getChartValues() - return listOf(chartValues.minX, (chartValues.minX + chartValues.maxX).half, chartValues.maxX) - } + ): List = + with(context) { listOf(chartValues.minX, (chartValues.minX + chartValues.maxX).half, chartValues.maxX) } @Suppress("LoopWithTooManyJumpStatements") override fun getLineValues( context: ChartDrawContext, visibleXRange: ClosedFloatingPointRange, fullXRange: ClosedFloatingPointRange, - ): List? { - val chartValues = context.chartValuesProvider.getChartValues() - return when (context.horizontalLayout) { + ): List? = with(context) { + when (context.horizontalLayout) { is HorizontalLayout.Segmented -> { val remainder = (visibleXRange.start - fullXRange.start) % chartValues.xStep val firstValue = visibleXRange.start + (chartValues.xStep - remainder) % 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 e9cdf0582..96553f68e 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 @@ -58,7 +58,6 @@ 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 = chartValuesProvider.getChartValues() canvas.clipRect( bounds.left - itemPlacer.getStartHorizontalAxisInset(this, horizontalDimensions, tickThickness), @@ -88,7 +87,7 @@ public class HorizontalAxis( label?.drawText( context = context, - text = valueFormatter.formatValue(x, chartValues), + text = valueFormatter.formatValue(x, chartValues, null), textX = canvasX, textY = textY, verticalPosition = position.textVerticalPosition, @@ -156,8 +155,6 @@ public class HorizontalAxis( val clipRestoreCount = canvas.save() canvas.clipRect(chartBounds) - val chartValues = chartValuesProvider.getChartValues() - if (lineValues == null) { labelValues.forEach { x -> val canvasX = baseCanvasX + (x - chartValues.minX) / chartValues.xStep * horizontalDimensions.xSpacing * @@ -198,13 +195,13 @@ public class HorizontalAxis( context: MeasureContext, horizontalDimensions: MutableHorizontalDimensions, ) { - val chartValues = context.chartValuesProvider.getChartValues() + val chartValues = context.chartValues horizontalDimensions.ensureValuesAtLeast( unscalableStartPadding = label .takeIf { itemPlacer.getAddFirstLabelPadding(context) } ?.getWidth( context = context, - text = valueFormatter.formatValue(chartValues.minX, chartValues), + text = valueFormatter.formatValue(chartValues.minX, chartValues, null), pad = true, ) ?.half @@ -213,7 +210,7 @@ public class HorizontalAxis( .takeIf { itemPlacer.getAddLastLabelPadding(context) } ?.getWidth( context = context, - text = valueFormatter.formatValue(chartValues.maxX, chartValues), + text = valueFormatter.formatValue(chartValues.maxX, chartValues, null), pad = true, ) ?.half @@ -235,7 +232,6 @@ public class HorizontalAxis( private fun MeasureContext.getFullXRange( horizontalDimensions: HorizontalDimensions, ): ClosedFloatingPointRange = with(horizontalDimensions) { - val chartValues = chartValuesProvider.getChartValues() val start = chartValues.minX - startPadding / xSpacing * chartValues.xStep val end = chartValues.maxX + endPadding / xSpacing * chartValues.xStep start..end @@ -245,7 +241,6 @@ public class HorizontalAxis( context: MeasureContext, horizontalDimensions: HorizontalDimensions, ): Float = with(context) { - val chartValues = chartValuesProvider.getChartValues() val fullXRange = getFullXRange(horizontalDimensions) when (val constraint = sizeConstraint) { @@ -253,7 +248,7 @@ public class HorizontalAxis( val labelHeight = label?.let { label -> itemPlacer .getMeasuredLabelValues(this, horizontalDimensions, fullXRange) - .map { valueFormatter.formatValue(it, chartValues) } + .map { valueFormatter.formatValue(it, chartValues, null) } .maxOf { labelText -> label.getHeight( context = this, 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 06d723685..7725c7ca9 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,11 +49,11 @@ internal class DefaultVerticalAxisItemPlacer( position: AxisPosition.Vertical, ): List { if (maxItemCount == 0) return emptyList() - val chartValues = context.chartValuesProvider.getChartValues(position) - return if (chartValues.minY * chartValues.maxY >= 0) { - getSimpleLabelValues(axisHeight, maxLabelHeight, chartValues) + val yRange = context.chartValues.getYRange(position) + return if (yRange.minY * yRange.maxY >= 0) { + getSimpleLabelValues(axisHeight, maxLabelHeight, yRange) } else { - getMixedLabelValues(axisHeight, maxLabelHeight, chartValues) + getMixedLabelValues(axisHeight, maxLabelHeight, yRange) } } @@ -61,8 +61,8 @@ internal class DefaultVerticalAxisItemPlacer( context: MeasureContext, position: AxisPosition.Vertical, ): List { - val chartValues = context.chartValuesProvider.getChartValues(position) - return listOf(chartValues.minY, (chartValues.minY + chartValues.maxY).half, chartValues.maxY) + val yRange = context.chartValues.getYRange(position) + return listOf(yRange.minY, (yRange.minY + yRange.maxY).half, yRange.maxY) } override fun getTopVerticalAxisInset( @@ -95,20 +95,24 @@ internal class DefaultVerticalAxisItemPlacer( else -> maxLabelHeight + maxLineThickness.half } - private fun getSimpleLabelValues(axisHeight: Float, maxLabelHeight: Float, chartValues: ChartValues): List { - val values = mutableListOf(chartValues.minY) + private fun getSimpleLabelValues( + axisHeight: Float, + maxLabelHeight: Float, + yRange: ChartValues.YRange, + ): List { + val values = mutableListOf(yRange.minY) if (maxItemCount == 1) return values val extraItemCount = (axisHeight / maxLabelHeight).toInt().coerceAtMost(maxItemCount - 1) - val step = chartValues.lengthY / extraItemCount - repeat(extraItemCount) { values += chartValues.minY + (it + 1) * step } + val step = yRange.length / extraItemCount + repeat(extraItemCount) { values += yRange.minY + (it + 1) * step } return values } - private fun getMixedLabelValues(axisHeight: Float, maxLabelHeight: Float, chartValues: ChartValues): List { + private fun getMixedLabelValues(axisHeight: Float, maxLabelHeight: Float, yRange: ChartValues.YRange): List { val values = mutableListOf(0f) if (maxItemCount == 1) return values - val topHeight = chartValues.maxY / chartValues.lengthY * axisHeight - val bottomHeight = -chartValues.minY / chartValues.lengthY * axisHeight + val topHeight = yRange.maxY / yRange.length * axisHeight + val bottomHeight = -yRange.minY / yRange.length * axisHeight val maxTopItemCount = (maxItemCount - 1) * topHeight / axisHeight val maxBottomItemCount = (maxItemCount - 1) * bottomHeight / axisHeight val topItemCountByHeight = topHeight / maxLabelHeight @@ -127,11 +131,11 @@ internal class DefaultVerticalAxisItemPlacer( } } if (topItemCount != 0) { - val step = chartValues.maxY / topItemCount + val step = yRange.maxY / topItemCount repeat(topItemCount) { values += (it + 1) * step } } if (bottomItemCount != 0) { - val step = chartValues.minY / bottomItemCount + val step = yRange.minY / bottomItemCount repeat(bottomItemCount) { values += (it + 1) * step } } return values 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 9bbc9b544..6d42f3047 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 @@ -78,13 +78,13 @@ public class VerticalAxis( context: ChartDrawContext, ): Unit = with(context) { var centerY: Float - val chartValues = chartValuesProvider.getChartValues(position) + val yRange = chartValues.getYRange(position) val maxLabelHeight = getMaxLabelHeight() val lineValues = itemPlacer.getLineValues(this, bounds.height(), maxLabelHeight, position) ?: itemPlacer.getLabelValues(this, bounds.height(), maxLabelHeight, position) lineValues.forEach { lineValue -> - centerY = bounds.bottom - bounds.height() * (lineValue - chartValues.minY) / chartValues.lengthY + + centerY = bounds.bottom - bounds.height() * (lineValue - yRange.minY) / yRange.length + getLineCanvasYCorrection(guidelineThickness, lineValue) guideline?.takeIf { @@ -121,10 +121,10 @@ public class VerticalAxis( val tickRightX = tickLeftX + axisThickness + tickLength val labelX = if (areLabelsOutsideAtStartOrInsideAtEnd == isLtr) tickLeftX else tickRightX var tickCenterY: Float - val chartValues = chartValuesProvider.getChartValues(position) + val yRange = chartValues.getYRange(position) labelValues.forEach { labelValue -> - tickCenterY = bounds.bottom - bounds.height() * (labelValue - chartValues.minY) / chartValues.lengthY + + tickCenterY = bounds.bottom - bounds.height() * (labelValue - yRange.minY) / yRange.length + getLineCanvasYCorrection(tickThickness, labelValue) tick?.drawHorizontal( @@ -137,7 +137,7 @@ public class VerticalAxis( label ?: return@forEach drawLabel( label = label, - labelText = valueFormatter.formatValue(labelValue, chartValues), + labelText = valueFormatter.formatValue(labelValue, chartValues, position), labelX = labelX, tickCenterY = tickCenterY, ) @@ -269,23 +269,23 @@ public class VerticalAxis( } private fun MeasureContext.getMaxLabelHeight() = label?.let { label -> - val chartValues = chartValuesProvider.getChartValues(position) itemPlacer .getHeightMeasurementLabelValues(this, position) - .maxOfOrNull { value -> label.getHeight(this, valueFormatter.formatValue(value, chartValues)) } + .maxOfOrNull { value -> label.getHeight(this, valueFormatter.formatValue(value, chartValues, position)) } }.orZero private fun MeasureContext.getMaxLabelWidth(axisHeight: Float) = label?.let { label -> - val chartValues = chartValuesProvider.getChartValues(position) itemPlacer .getWidthMeasurementLabelValues(this, axisHeight, getMaxLabelHeight(), position) - .maxOfOrNull { value -> label.getWidth(this, valueFormatter.formatValue(value, chartValues)) } + .maxOfOrNull { value -> label.getWidth(this, valueFormatter.formatValue(value, chartValues, position)) } }.orZero - private fun ChartDrawContext.getLineCanvasYCorrection(thickness: Float, y: Float): Float { - val chartValues = chartValuesProvider.getChartValues(position) - return if (y == chartValues.maxY && itemPlacer.getShiftTopLines(this)) -thickness.half else thickness.half - } + private fun ChartDrawContext.getLineCanvasYCorrection(thickness: Float, y: Float) = + if (y == chartValues.getYRange(position).maxY && itemPlacer.getShiftTopLines(this)) { + -thickness.half + } else { + thickness.half + } /** * Defines the horizontal position of each of a vertical axis’s labels relative to the axis line. diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseCartesianLayer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseCartesianLayer.kt new file mode 100644 index 000000000..dd63f59cc --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseCartesianLayer.kt @@ -0,0 +1,53 @@ +/* + * 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 + +import android.graphics.RectF +import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext +import com.patrykandpatrick.vico.core.chart.insets.Insets +import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider +import com.patrykandpatrick.vico.core.dimensions.BoundsAware +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel +import com.patrykandpatrick.vico.core.extension.inClip + +/** + * A base [CartesianLayer] implementation. + */ +public abstract class BaseCartesianLayer : CartesianLayer, BoundsAware { + private val insets: Insets = Insets() + + override val bounds: RectF = RectF() + + override var axisValuesOverrider: AxisValuesOverrider? = null + + protected abstract fun drawInternal(context: ChartDrawContext, model: Model) + + override fun draw(context: ChartDrawContext, model: Model) { + with(context) { + insets.clear() + getInsets(this, insets, horizontalDimensions) + canvas.inClip( + left = bounds.left - insets.getLeft(isLtr), + top = bounds.top - insets.top, + right = bounds.right + insets.getRight(isLtr), + bottom = bounds.bottom + insets.bottom, + ) { + drawInternal(context, model) + } + } + } +} 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 deleted file mode 100644 index 15390a491..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/BaseChart.kt +++ /dev/null @@ -1,139 +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.core.chart - -import android.graphics.RectF -import com.patrykandpatrick.vico.core.chart.decoration.Decoration -import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext -import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter -import com.patrykandpatrick.vico.core.chart.insets.Insets -import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider -import com.patrykandpatrick.vico.core.dimensions.BoundsAware -import com.patrykandpatrick.vico.core.entry.ChartEntryModel -import com.patrykandpatrick.vico.core.extension.getEntryModel -import com.patrykandpatrick.vico.core.extension.inClip -import com.patrykandpatrick.vico.core.extension.setAll -import com.patrykandpatrick.vico.core.marker.Marker - -/** - * A base implementation of [Chart]. - * - * @see Chart - */ -public abstract class BaseChart : Chart, BoundsAware { - - private val decorations = ArrayList() - - private val insets: Insets = Insets() - - /** - * A [HashMap] that links x-axis values to [Marker]s. - */ - protected val persistentMarkers: HashMap = HashMap() - - override val bounds: RectF = RectF() - - override val chartInsetters: Collection = persistentMarkers.values - - override var axisValuesOverrider: AxisValuesOverrider<@UnsafeVariance Model>? = null - - override fun addDecoration(decoration: Decoration): Boolean = decorations.add(decoration) - - override fun setDecorations(decorations: List) { - this.decorations.setAll(decorations) - } - - override fun removeDecoration(decoration: Decoration): Boolean = decorations.remove(decoration) - - override fun addPersistentMarker(x: Float, marker: Marker) { - persistentMarkers[x] = marker - } - - override fun setPersistentMarkers(markers: Map) { - persistentMarkers.setAll(markers) - } - - override fun removePersistentMarker(x: Float) { - persistentMarkers.remove(x) != null - } - - override fun drawScrollableContent( - context: ChartDrawContext, - model: Model, - ): Unit = with(context) { - insets.clear() - getInsets(this, insets, horizontalDimensions) - drawChartInternal(context, model) - } - - override fun drawNonScrollableContent( - context: ChartDrawContext, - model: Model, - ): Unit = with(context) { - canvas.inClip( - left = bounds.left, - top = 0f, - right = bounds.right, - bottom = context.canvas.height.toFloat(), - ) { - drawDecorationAboveChart(context) - } - persistentMarkers.forEach { (x, marker) -> - entryLocationMap.getEntryModel(x)?.also { markerModel -> - marker.draw( - context = context, - bounds = bounds, - markedEntries = markerModel, - chartValuesProvider = chartValuesProvider, - ) - } - } - } - - /** - * An internal function that draws both [Decoration]s behind the chart and the chart itself in the clip bounds. - */ - protected open fun drawChartInternal( - context: ChartDrawContext, - model: Model, - ): Unit = with(context) { - canvas.inClip( - left = bounds.left - insets.getLeft(isLtr), - top = bounds.top - insets.top, - right = bounds.right + insets.getRight(isLtr), - bottom = bounds.bottom + insets.bottom, - ) { - drawDecorationBehindChart(context) - if (model.entries.isNotEmpty()) { - drawChart(context, model) - } - } - } - - protected abstract fun drawChart( - context: ChartDrawContext, - model: Model, - ) - - protected fun drawDecorationBehindChart(context: ChartDrawContext) { - decorations.forEach { line -> line.onDrawBehindChart(context, bounds) } - } - - protected fun drawDecorationAboveChart(context: ChartDrawContext) { - decorations.forEach { line -> line.onDrawAboveChart(context, bounds) } - } -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/CartesianLayer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/CartesianLayer.kt new file mode 100644 index 000000000..129573c0b --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/CartesianLayer.kt @@ -0,0 +1,74 @@ +/* + * 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 + +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart +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.values.AxisValuesOverrider +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.chart.values.MutableChartValues +import com.patrykandpatrick.vico.core.context.MeasureContext +import com.patrykandpatrick.vico.core.dimensions.BoundsAware +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore +import com.patrykandpatrick.vico.core.marker.Marker + +/** + * Visualizes data on a Cartesian plane. [CartesianLayer]s are combined and drawn by [CartesianChart]s. + */ +public interface CartesianLayer : BoundsAware, ChartInsetter { + /** + * Links _x_ values to [Marker.EntryModel]s. + */ + public val entryLocationMap: Map> + + /** + * Overrides the _x_ and _y_ ranges. + */ + public var axisValuesOverrider: AxisValuesOverrider? + + /** + * Draws the [CartesianLayer]. + */ + public fun draw(context: ChartDrawContext, model: T) + + /** + * Updates [horizontalDimensions] to match this [CartesianLayer]’s dimensions. + */ + public fun updateHorizontalDimensions( + context: MeasureContext, + horizontalDimensions: MutableHorizontalDimensions, + model: T, + ) + + /** + * Updates [chartValues] in accordance with [model]. + */ + public fun updateChartValues(chartValues: MutableChartValues, model: T, xStep: Float?) + + /** + * Prepares the [CartesianLayer] for a difference animation. + */ + public fun prepareForTransformation(model: T?, extraStore: MutableExtraStore, chartValues: ChartValues) + + /** + * Carries out the pending difference animation. + */ + public suspend fun transform(extraStore: MutableExtraStore, fraction: Float) +} 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 deleted file mode 100644 index 69c58d38d..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/Chart.kt +++ /dev/null @@ -1,213 +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.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.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 - -/** - * Defines the minimal set of properties and functions required by other parts of the library to draw a chart. - */ -public interface Chart : BoundsAware, ChartInsetter { - - /** - * Links x-axis values to [Marker.EntryModel]s. A [Marker.EntryModel] holds the data needed to draw a [Marker]. - */ - public val entryLocationMap: Map> - - /** - * A [Collection] of the [ChartInsetter]s that are part of this [Chart]. Each [ChartInsetter] can influence the - * final layout of the chart and its components. - * - * @see ChartInsetter - */ - public val chartInsetters: Collection - - /** - * Overrides the minimum and maximum x-axis and y-axis values. In the case of [ColumnChart]s and [LineChart]s - * contained in [ComposedChart]s, these overrides can be applied to one vertical axis instead of both. Use - * [ColumnChart.targetVerticalAxisPosition] and [LineChart.targetVerticalAxisPosition] for this purpose. - */ - public var axisValuesOverrider: AxisValuesOverrider<@UnsafeVariance Model>? - - /** - * Responsible for drawing the chart itself and any decorations behind it. - * - * @param context holds the data needed to draw the [Chart]. - * @param model holds data about the [Chart]’s entries. - * - * @see ChartDrawContext - */ - public fun drawScrollableContent( - context: ChartDrawContext, - model: Model, - ) - - /** - * Responsible for drawing any decorations placed above the chart, as well as persistent markers. - * - * @param context holds the data needed to draw the [Chart]. - * @param model holds data about the [Chart]’s entries. - * - * @see ChartDrawContext - */ - public fun drawNonScrollableContent( - context: ChartDrawContext, - model: Model, - ) - - /** - * Adds a [Decoration] to this [Chart]. - * - * @return `true` if the decoration was added successfully. - * - * @see Decoration - */ - public fun addDecoration(decoration: Decoration): Boolean - - /** - * Replaces the current list of decorations with the provided [decorations]. - */ - public fun setDecorations(decorations: List) - - /** - * Removes a [Decoration] from this [Chart]. - * - * @return `true` if the decoration was removed successfully. - * - * @see Decoration - */ - public fun removeDecoration(decoration: Decoration): Boolean - - /** - * Removes each [Decoration] from [decorations] from this [Chart]. - * - * @return `true` if all decorations were removed successfully. - * - * @see removeDecoration - * @see Decoration - */ - public fun removeDecorations(decorations: List): Boolean = - decorations.all(::removeDecoration) - - /** - * Adds a persistent [Marker] to this [Chart]. The [Marker] will be anchored to the given [x] value on the x-axis. - * - * @see Marker - */ - public fun addPersistentMarker(x: Float, marker: Marker) - - /** - * Replaces the current map of markers with the provided [markers]. - * - * @see addPersistentMarker - * @see Marker - */ - public fun setPersistentMarkers(markers: Map) - - /** - * Removes a persistent [Marker] from this [Chart]. - * - * @see Marker - */ - public fun removePersistentMarker(x: Float) - - /** - * Updates the chart’s [MutableHorizontalDimensions] instance. - */ - public fun updateHorizontalDimensions( - context: MeasureContext, - horizontalDimensions: MutableHorizontalDimensions, - model: Model, - ) - - /** - * Updates the [ChartValues] stored in the provided [ChartValuesManager] instance to this [Chart]’s [ChartValues]. - * - * @param chartValuesManager the [ChartValuesManager] whose properties will be updated. - * @param model holds data about the [Chart]’s entries. - * @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) - } -} - -/** - * Adds each [Decoration] from [decorations] to this [Chart]. - * - * @return true if all decorations were added successfully. - * - * @see Chart.addDecoration - * @see Decoration - */ -public fun Chart.addDecorations(decorations: List): Boolean = - decorations.all(::addDecoration) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/ChartExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/ChartExtensions.kt index 638ab06dd..8a0045127 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/ChartExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/ChartExtensions.kt @@ -16,7 +16,7 @@ package com.patrykandpatrick.vico.core.chart -import com.patrykandpatrick.vico.core.entry.ChartEntry +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel import com.patrykandpatrick.vico.core.extension.updateList import com.patrykandpatrick.vico.core.marker.Marker import com.patrykandpatrick.vico.core.model.Point @@ -24,7 +24,7 @@ import com.patrykandpatrick.vico.core.model.Point internal fun HashMap>.put( x: Float, y: Float, - entry: ChartEntry, + entry: CartesianLayerModel.Entry, color: Int, index: Int, ) { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/DefaultPointConnector.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/DefaultPointConnector.kt index d7dab7aaf..bd833cd14 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/DefaultPointConnector.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/DefaultPointConnector.kt @@ -20,21 +20,21 @@ import android.graphics.Path import android.graphics.RectF import com.patrykandpatrick.vico.core.DefaultDimens import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions -import com.patrykandpatrick.vico.core.chart.line.LineChart +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayer import com.patrykandpatrick.vico.core.component.shape.extension.horizontalCubicTo import com.patrykandpatrick.vico.core.extension.half import kotlin.math.abs /** - * The default implementation of [LineChart.LineSpec.PointConnector]. This uses cubic bezier curves. + * The default implementation of [LineCartesianLayer.LineSpec.PointConnector]. This uses cubic bezier curves. * * @property cubicStrength the strength of the cubic bezier curve between each point on the line. * - * @see LineChart.LineSpec.PointConnector + * @see LineCartesianLayer.LineSpec.PointConnector */ public class DefaultPointConnector( private val cubicStrength: Float = DefaultDimens.CUBIC_STRENGTH, -) : LineChart.LineSpec.PointConnector { +) : LineCartesianLayer.LineSpec.PointConnector { public override fun connect( path: Path, diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/EntryModelExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/EntryModelExtensions.kt index dbfffde04..603f9b401 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/EntryModelExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/EntryModelExtensions.kt @@ -16,23 +16,16 @@ package com.patrykandpatrick.vico.core.chart -import com.patrykandpatrick.vico.core.entry.ChartEntry +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel /** - * For each [ChartEntry] in the list such that [ChartEntry.x] belongs to the provided range, calls the [action] function - * block with the [ChartEntry] as the block’s argument. - */ -public inline fun List.forEachIn(range: ClosedFloatingPointRange, action: (ChartEntry) -> Unit) { - forEach { if (it.x in range) action(it) } -} - -/** - * For each [ChartEntry] in the list such that [ChartEntry.x] belongs to the provided range, calls the [action] function - * block with the [ChartEntry] and its index in the list as the block’s arguments. + * For each [CartesianLayerModel.Entry] in the list such that [CartesianLayerModel.Entry.x] belongs to the provided + * range, calls the [action] function block with the [CartesianLayerModel.Entry] and its index in the list as the + * block’s arguments. */ -public inline fun List.forEachInAbsolutelyIndexed( +public inline fun List.forEachInAbsolutelyIndexed( range: ClosedFloatingPointRange, - action: (Int, ChartEntry) -> Unit, + action: (Int, T) -> Unit, ) { var index = 0 forEach { entry -> @@ -42,42 +35,15 @@ public inline fun List.forEachInAbsolutelyIndexed( } /** - * For each [ChartEntry] in the list such that [ChartEntry.x] belongs to the provided range, calls the [action] function - * block with the [ChartEntry], its index in the list, and the next [ChartEntry] in the filtered list as the block’s - * arguments. + * For each [CartesianLayerModel.Entry] in the list such that [CartesianLayerModel.Entry.x] belongs to the provided + * range, calls the [action] function block with the [CartesianLayerModel.Entry], its index in the list, and the next + * [CartesianLayerModel.Entry] in the filtered list as the block’s arguments. */ -public inline fun List.forEachInAbsolutelyIndexed( +public inline fun List.forEachInAbsolutelyIndexed( range: ClosedFloatingPointRange, - action: (Int, ChartEntry, ChartEntry?) -> Unit, + action: (Int, T, T?) -> Unit, ) { forEachInAbsolutelyIndexed(range) { index, entry -> action(index, entry, getOrNull(index + 1)?.takeIf { it.x in range }) } } - -/** - * For each [ChartEntry] in the list such that [ChartEntry.x] belongs to the provided range, calls the [action] function - * block with the [ChartEntry] and its index in the filtered list as the block’s arguments. - */ -public inline fun List.forEachInRelativelyIndexed( - range: ClosedFloatingPointRange, - action: (Int, ChartEntry) -> Unit, -) { - var index = 0 - forEachIn(range) { action(index++, it) } -} - -/** - * For each [ChartEntry] in the list such that [ChartEntry.x] belongs to the provided range, calls the [action] function - * block with the [ChartEntry], its index in the filtered list, and the next [ChartEntry] in the filtered list as the - * block’s arguments. - */ -public inline fun List.forEachInRelativelyIndexed( - range: ClosedFloatingPointRange, - action: (Int, ChartEntry, ChartEntry?) -> Unit, -) { - var relativeIndex = 0 - forEachInAbsolutelyIndexed(range) { absoluteIndex, entry -> - action(relativeIndex++, entry, getOrNull(absoluteIndex + 1)?.takeIf { it.x in range }) - } -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/LineSpecExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/LineSpecExtensions.kt index b4c67a3c0..18a4b80b1 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/LineSpecExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/LineSpecExtensions.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. @@ -17,7 +17,7 @@ package com.patrykandpatrick.vico.core.chart import android.graphics.Paint -import com.patrykandpatrick.vico.core.chart.line.LineChart +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayer import com.patrykandpatrick.vico.core.component.Component import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShader import com.patrykandpatrick.vico.core.component.text.TextComponent @@ -25,9 +25,9 @@ import com.patrykandpatrick.vico.core.component.text.VerticalPosition import com.patrykandpatrick.vico.core.formatter.ValueFormatter /** - * Creates a new [LineChart.LineSpec] based on this one, updating select properties. + * Creates a new [LineCartesianLayer.LineSpec] based on this one, updating select properties. */ -public fun LineChart.LineSpec.copy( +public fun LineCartesianLayer.LineSpec.copy( lineColor: Int = this.lineColor, lineThicknessDp: Float = this.lineThicknessDp, lineBackgroundShader: DynamicShader? = this.lineBackgroundShader, @@ -38,8 +38,8 @@ public fun LineChart.LineSpec.copy( dataLabelVerticalPosition: VerticalPosition = this.dataLabelVerticalPosition, dataLabelValueFormatter: ValueFormatter = this.dataLabelValueFormatter, dataLabelRotationDegrees: Float = this.dataLabelRotationDegrees, - pointConnector: LineChart.LineSpec.PointConnector = this.pointConnector, -): LineChart.LineSpec = LineChart.LineSpec( + pointConnector: LineCartesianLayer.LineSpec.PointConnector = this.pointConnector, +): LineCartesianLayer.LineSpec = LineCartesianLayer.LineSpec( lineColor = lineColor, lineThicknessDp = lineThicknessDp, lineBackgroundShader = lineBackgroundShader, 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/ColumnCartesianLayer.kt similarity index 77% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChart.kt rename to vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnCartesianLayer.kt index 8ff083013..ea07303a3 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/ColumnCartesianLayer.kt @@ -19,24 +19,19 @@ package com.patrykandpatrick.vico.core.chart.column 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.BaseCartesianLayer import com.patrykandpatrick.vico.core.chart.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext import com.patrykandpatrick.vico.core.chart.forEachInAbsolutelyIndexed 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.chart.values.MutableChartValues import com.patrykandpatrick.vico.core.component.shape.LineComponent import com.patrykandpatrick.vico.core.component.text.TextComponent import com.patrykandpatrick.vico.core.component.text.VerticalPosition 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 @@ -51,7 +46,7 @@ import com.patrykandpatrick.vico.core.marker.Marker import kotlin.math.abs /** - * [ColumnChart] displays data as vertical bars. It can group and stack columns. + * [ColumnCartesianLayer] displays data as vertical bars. It can group and stack columns. * * @param columns the [LineComponent] instances to use for columns. This list is iterated through as many times * as necessary for each column collection. If the list contains a single element, all columns have the same appearance. @@ -59,15 +54,15 @@ import kotlin.math.abs * @param innerSpacingDp the distance between neighboring grouped columns. * @param mergeMode defines how columns should be drawn in column collections. * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided - * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. + * value will use the [ChartValues] provided by this chart. * @param dataLabel an optional [TextComponent] to use for data labels. * @param dataLabelVerticalPosition the vertical position of data labels relative to the top of their * 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. + * @param drawingModelInterpolator interpolates the [ColumnCartesianLayer]’s [ColumnCartesianLayerDrawingModel]s. */ -public open class ColumnChart( +public open class ColumnCartesianLayer( public var columns: List, public var spacingDp: Float = DefaultDimens.COLUMN_OUTSIDE_SPACING, public var innerSpacingDp: Float = DefaultDimens.COLUMN_INSIDE_SPACING, @@ -78,13 +73,13 @@ public open class ColumnChart( public var dataLabelValueFormatter: ValueFormatter = DecimalFormatValueFormatter(), public var dataLabelRotationDegrees: Float = 0f, public var drawingModelInterpolator: DrawingModelInterpolator< - ColumnChartDrawingModel.ColumnInfo, - ColumnChartDrawingModel, + ColumnCartesianLayerDrawingModel.ColumnInfo, + ColumnCartesianLayerDrawingModel, > = DefaultDrawingModelInterpolator(), -) : BaseChart() { +) : BaseCartesianLayer() { /** - * Creates a [ColumnChart] with a common style for all columns. + * Creates a [ColumnCartesianLayer] with a common style for all columns. * * @param column a [LineComponent] defining the appearance of the columns. * @param spacingDp the distance between neighboring column collections. @@ -98,34 +93,34 @@ public open class ColumnChart( ) : this(columns = listOf(column), spacingDp = spacingDp, targetVerticalAxisPosition = targetVerticalAxisPosition) /** - * Creates a [ColumnChart] instance with [columns] set to an empty list. The list must be populated before the chart - * is drawn. + * Creates a [ColumnCartesianLayer] instance with [columns] set to an empty list. The list must be populated before + * the chart is drawn. */ public constructor() : this(emptyList()) /** * When [mergeMode] is set to [MergeMode.Stack], this maps the x-axis value of every column collection to a pair * containing the bottom coordinate of the collection’s bottommost column and the top coordinate of the collection’s - * topmost column. This hash map is used by [drawChart] and [drawChartInternal]. + * topmost column. This hash map is used by [drawInternal] and [drawChartInternal]. */ protected val heightMap: HashMap> = HashMap() /** - * Holds information on the [ColumnChart]’s horizontal dimensions. + * Holds information on the [ColumnCartesianLayer]’s horizontal dimensions. */ protected val horizontalDimensions: MutableHorizontalDimensions = MutableHorizontalDimensions() - protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() + protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() override val entryLocationMap: HashMap> = HashMap() - override fun drawChart( + override fun drawInternal( context: ChartDrawContext, - model: ChartEntryModel, + model: ColumnCartesianLayerModel, ): Unit = with(context) { entryLocationMap.clear() drawChartInternal( - chartValues = chartValuesProvider.getChartValues(axisPosition = targetVerticalAxisPosition), + chartValues = chartValues, model = model, drawingModel = model.extraStore.getOrNull(drawingModelKey), ) @@ -134,11 +129,11 @@ public open class ColumnChart( protected open fun ChartDrawContext.drawChartInternal( chartValues: ChartValues, - model: ChartEntryModel, - drawingModel: ColumnChartDrawingModel?, + model: ColumnCartesianLayerModel, + drawingModel: ColumnCartesianLayerDrawingModel?, ) { - val yRange = (chartValues.maxY - chartValues.minY).takeIf { it != 0f } ?: return - val heightMultiplier = bounds.height() / yRange + val yRange = chartValues.getYRange(targetVerticalAxisPosition) + val heightMultiplier = bounds.height() / yRange.length var drawingStart: Float var height: Float @@ -146,17 +141,17 @@ public open class ColumnChart( var column: LineComponent var columnTop: Float var columnBottom: Float - val zeroLinePosition = bounds.bottom + chartValues.minY / yRange * bounds.height() + val zeroLinePosition = bounds.bottom + yRange.minY / yRange.length * bounds.height() - model.entries.forEachIndexed { index, entryCollection -> + model.series.forEachIndexed { index, entryCollection -> column = columns.getRepeating(index) - drawingStart = getDrawingStart(index, model.entries.size) - horizontalScroll + drawingStart = getDrawingStart(index, model.series.size) - horizontalScroll entryCollection.forEachInAbsolutelyIndexed(chartValues.minX..chartValues.maxX) { entryIndex, entry -> val columnInfo = drawingModel?.getOrNull(index)?.get(entry.x) - height = (columnInfo?.height ?: (abs(entry.y) / chartValues.lengthY)) * bounds.height() + height = (columnInfo?.height ?: (abs(entry.y) / yRange.length)) * 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 + @@ -205,18 +200,18 @@ public open class ColumnChart( if (mergeMode == MergeMode.Grouped) { drawDataLabel( - modelEntriesSize = model.entries.size, + modelEntriesSize = model.series.size, columnThicknessDp = column.thicknessDp, dataLabelValue = entry.y, x = columnCenterX, y = columnSignificantY, isFirst = index == 0 && entry.x == chartValues.minX, - isLast = index == model.entries.lastIndex && entry.x == chartValues.maxX, + isLast = index == model.series.lastIndex && entry.x == chartValues.maxX, ) - } else if (index == model.entries.lastIndex) { + } else if (index == model.series.lastIndex) { val yValues = heightMap[entry.x] drawStackedDataLabel( - modelEntriesSize = model.entries.size, + modelEntriesSize = model.series.size, columnThicknessDp = column.thicknessDp, negativeY = yValues?.first, positiveY = yValues?.second, @@ -281,7 +276,8 @@ public open class ColumnChart( } val text = dataLabelValueFormatter.formatValue( value = dataLabelValue, - chartValues = chartValuesProvider.getChartValues(axisPosition = targetVerticalAxisPosition), + chartValues = chartValues, + verticalAxisPosition = targetVerticalAxisPosition, ) val dataLabelWidth = textComponent.getWidth( context = this, @@ -317,7 +313,7 @@ public open class ColumnChart( } protected open fun updateMarkerLocationMap( - entry: ChartEntry, + entry: ColumnCartesianLayerModel.Entry, columnTop: Float, columnCenterX: Float, column: LineComponent, @@ -334,26 +330,24 @@ public open class ColumnChart( } } - override fun updateChartValues(chartValuesManager: ChartValuesManager, model: ChartEntryModel, xStep: Float?) { - chartValuesManager.tryUpdate( + override fun updateChartValues(chartValues: MutableChartValues, model: ColumnCartesianLayerModel, xStep: Float?) { + chartValues.tryUpdate( + axisPosition = targetVerticalAxisPosition, minX = axisValuesOverrider?.getMinX(model) ?: model.minX, maxX = axisValuesOverrider?.getMaxX(model) ?: model.maxX, minY = axisValuesOverrider?.getMinY(model) ?: mergeMode.getMinY(model), maxY = axisValuesOverrider?.getMaxY(model) ?: mergeMode.getMaxY(model), - xStep = xStep ?: model.xGcd, - chartEntryModel = model, - axisPosition = targetVerticalAxisPosition, ) } override fun updateHorizontalDimensions( context: MeasureContext, horizontalDimensions: MutableHorizontalDimensions, - model: ChartEntryModel, + model: ColumnCartesianLayerModel, ) { with(context) { val columnCollectionWidth = - getColumnCollectionWidth(if (model.entries.isNotEmpty()) model.entries.size else 1) + getColumnCollectionWidth(if (model.series.isNotEmpty()) model.series.size else 1) val xSpacing = columnCollectionWidth + spacingDp.pixels when (val horizontalLayout = horizontalLayout) { is HorizontalLayout.Segmented -> { @@ -377,13 +371,6 @@ public open class ColumnChart( } } - 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,7 +403,7 @@ public open class ColumnChart( } /** - * Defines how a [ColumnChart] should draw columns in column collections. + * Defines how a [ColumnCartesianLayer] should draw columns in column collections. */ public enum class MergeMode { @@ -435,54 +422,45 @@ public open class ColumnChart( /** * Returns the minimum y-axis value, taking into account the current [MergeMode]. */ - public fun getMinY(model: ChartEntryModel): Float = when (this) { + public fun getMinY(model: ColumnCartesianLayerModel): Float = when (this) { Grouped -> model.minY.coerceAtMost(0f) - Stack -> model.stackedNegativeY.coerceAtMost(0f) + Stack -> model.minAggregateY.coerceAtMost(0f) } /** * Returns the maximum y-axis value, taking into account the current [MergeMode]. */ - public fun getMaxY(model: ChartEntryModel): Float = when (this) { + public fun getMaxY(model: ColumnCartesianLayerModel): Float = when (this) { Grouped -> model.maxY - Stack -> model.stackedPositiveY + Stack -> model.maxAggregateY } } - 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 fun prepareForTransformation( + model: ColumnCartesianLayerModel?, + extraStore: MutableExtraStore, + chartValues: ChartValues, + ) { + drawingModelInterpolator.setModels( + old = extraStore.getOrNull(drawingModelKey), + new = model?.toDrawingModel(chartValues), + ) + } - override suspend fun transform(extraStore: MutableExtraStore, fraction: Float) { - getDrawingModelInterpolator() - .transform(fraction) - ?.let { extraStore[key] = it } - ?: extraStore.remove(key) - } + override suspend fun transform(extraStore: MutableExtraStore, fraction: Float) { + drawingModelInterpolator + .transform(fraction) + ?.let { extraStore[drawingModelKey] = it } + ?: extraStore.remove(drawingModelKey) + } - private fun ChartEntryModel.toDrawingModel(chartValues: ChartValues): ColumnChartDrawingModel = entries - .map { series -> - series.associate { entry -> - entry.x to ColumnChartDrawingModel.ColumnInfo(abs(entry.y) / chartValues.lengthY) - } + private fun ColumnCartesianLayerModel.toDrawingModel(chartValues: ChartValues) = series + .map { series -> + series.associate { entry -> + entry.x to ColumnCartesianLayerDrawingModel.ColumnInfo( + height = abs(entry.y) / chartValues.getYRange(targetVerticalAxisPosition).length, + ) } - .let(::ColumnChartDrawingModel) - } + } + .let(::ColumnCartesianLayerDrawingModel) } 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/ColumnCartesianLayerDrawingModel.kt similarity index 66% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnChartDrawingModel.kt rename to vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnCartesianLayerDrawingModel.kt index 3c6c26b66..03ab2e79e 100644 --- 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/ColumnCartesianLayerDrawingModel.kt @@ -20,23 +20,23 @@ 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. + * Houses drawing information for a [ColumnCartesianLayer]. [opacity] is the columns’ opacity. */ -public class ColumnChartDrawingModel(entries: List>, public val opacity: Float = 1f) : - DrawingModel(entries) { +public class ColumnCartesianLayerDrawingModel(entries: List>, public val opacity: Float = 1f) : + DrawingModel(entries) { override fun transform( drawingInfo: List>, from: DrawingModel?, fraction: Float, ): DrawingModel { - val oldOpacity = (from as ColumnChartDrawingModel?)?.opacity.orZero - return ColumnChartDrawingModel(drawingInfo, oldOpacity + (opacity - oldOpacity) * fraction) + val oldOpacity = (from as ColumnCartesianLayerDrawingModel?)?.opacity.orZero + return ColumnCartesianLayerDrawingModel(drawingInfo, oldOpacity + (opacity - oldOpacity) * fraction) } /** - * Houses positional information for a [ColumnChart]’s column. [height] expresses the column’s height as a fraction - * of the [ColumnChart]’s height. + * Houses positional information for a [ColumnCartesianLayer]’s column. [height] expresses the column’s height as a + * fraction of the [ColumnCartesianLayer]’s height. */ public class ColumnInfo(public val height: Float) : DrawingInfo { override fun transform(from: DrawingInfo?, fraction: Float): DrawingInfo { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnCartesianLayerModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnCartesianLayerModel.kt new file mode 100644 index 000000000..595578a94 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/column/ColumnCartesianLayerModel.kt @@ -0,0 +1,133 @@ +/* + * 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.CartesianLayerModel +import com.patrykandpatrick.vico.core.entry.calculateStackedYRange +import com.patrykandpatrick.vico.core.entry.calculateXDeltaGcd +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.extension.rangeOfOrNull + +/** + * Stores a [ColumnCartesianLayer]’s data. + */ +public class ColumnCartesianLayerModel : CartesianLayerModel { + public constructor(series: List>) : this(series, ExtraStore.empty) + + private constructor(series: List>, extraStore: ExtraStore) { + val entries = series.flatten() + val xRange = entries.rangeOfOrNull { it.x } ?: 0f..0f + val yRange = entries.rangeOfOrNull { it.y } ?: 0f..0f + val aggregateYRange = entries.calculateStackedYRange() + this.series = series + this.id = series.hashCode() + this.minX = xRange.start + this.maxX = xRange.endInclusive + this.minY = yRange.start + this.maxY = yRange.endInclusive + this.minAggregateY = aggregateYRange.start + this.maxAggregateY = aggregateYRange.endInclusive + this.xDeltaGcd = entries.calculateXDeltaGcd() + this.extraStore = extraStore + } + + private constructor( + series: List>, + id: Int, + minX: Float, + maxX: Float, + minY: Float, + maxY: Float, + minAggregateY: Float, + maxAggregateY: Float, + xDeltaGcd: Float, + extraStore: ExtraStore, + ) { + this.series = series + this.id = id + this.minX = minX + this.maxX = maxX + this.minY = minY + this.maxY = maxY + this.minAggregateY = minAggregateY + this.maxAggregateY = maxAggregateY + this.xDeltaGcd = xDeltaGcd + this.extraStore = extraStore + } + + /** + * The series (lists of [Entry] instances). + */ + public val series: List> + + override val id: Int + + override val minX: Float + + override val maxX: Float + + override val minY: Float + + override val maxY: Float + + /** + * The minimum sum of all _y_ values associated with a given _x_ value. + */ + public val minAggregateY: Float + + /** + * The maximum sum of all _y_ values associated with a given _x_ value. + */ + public val maxAggregateY: Float + + override val xDeltaGcd: Float + + override val extraStore: ExtraStore + + override fun copy(extraStore: ExtraStore): CartesianLayerModel = ColumnCartesianLayerModel( + series, + id, + minX, + maxX, + minY, + maxY, + minAggregateY, + maxAggregateY, + xDeltaGcd, + extraStore, + ) + + /** + * Represents a column. + */ + public interface Entry : CartesianLayerModel.Entry { + override val x: Float + + /** + * The _y_ coordinate. + */ + public val y: Float + } + + /** + * Stores the minimum amount of data required to create a [ColumnCartesianLayerModel] and facilitates this creation. + */ + public class Partial(private val series: List>) : CartesianLayerModel.Partial { + override fun complete(extraStore: ExtraStore): CartesianLayerModel = + ColumnCartesianLayerModel(series, extraStore) + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/CartesianChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/CartesianChart.kt new file mode 100644 index 000000000..185da3fae --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/CartesianChart.kt @@ -0,0 +1,166 @@ +/* + * 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.composed + +import android.graphics.RectF +import com.patrykandpatrick.vico.core.chart.CartesianLayer +import com.patrykandpatrick.vico.core.chart.column.ColumnCartesianLayer +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.insets.HorizontalInsets +import com.patrykandpatrick.vico.core.chart.insets.Insets +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayer +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.chart.values.MutableChartValues +import com.patrykandpatrick.vico.core.context.MeasureContext +import com.patrykandpatrick.vico.core.dimensions.BoundsAware +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore +import com.patrykandpatrick.vico.core.extension.set +import com.patrykandpatrick.vico.core.extension.updateAll +import com.patrykandpatrick.vico.core.marker.Marker +import java.util.TreeMap + +/** + * A chart based on a Cartesian coordinate plane, composed of [CartesianLayer]s. + */ +public open class CartesianChart(layers: List>) : BoundsAware, ChartInsetter { + private val layers: List> = layers.toList() + + /** + * Links _x_ values to [Marker.EntryModel]s. + */ + public val entryLocationMap: TreeMap> = TreeMap() + + override val bounds: RectF = RectF() + + public constructor(vararg layers: CartesianLayer<*>) : this(layers.toList()) + + override fun setBounds(left: Number, top: Number, right: Number, bottom: Number) { + bounds.set(left, top, right, bottom) + layers.forEach { it.setBounds(left, top, right, bottom) } + } + + /** + * Draws the [CartesianChart]. + */ + public fun draw(context: ChartDrawContext, model: CartesianChartModel) { + entryLocationMap.clear() + model.forEachWithLayer( + object : ModelAndLayerReceiver { + override fun invoke(model: T?, layer: CartesianLayer) { + layer.draw(context, model ?: return) + entryLocationMap.updateAll(layer.entryLocationMap) + } + }, + ) + } + + /** + * Updates [horizontalDimensions] to match the [CartesianLayer]s’ dimensions. + */ + public fun updateHorizontalDimensions( + context: MeasureContext, + horizontalDimensions: MutableHorizontalDimensions, + model: CartesianChartModel, + ) { + model.forEachWithLayer( + object : ModelAndLayerReceiver { + override fun invoke(model: T?, layer: CartesianLayer) { + layer.updateHorizontalDimensions(context, horizontalDimensions, model ?: return) + } + }, + ) + } + + /** + * Updates [chartValues] in accordance with [model]. + */ + public fun updateChartValues(chartValues: MutableChartValues, model: CartesianChartModel, xStep: Float?) { + chartValues.update(xStep ?: model.xDeltaGcd, model) + model.forEachWithLayer( + object : ModelAndLayerReceiver { + override fun invoke(model: T?, layer: CartesianLayer) { + layer.updateChartValues(chartValues, model ?: return, xStep) + } + }, + ) + } + + override fun getInsets( + context: MeasureContext, + outInsets: Insets, + horizontalDimensions: HorizontalDimensions, + ) { + layers.forEach { it.getInsets(context, outInsets, horizontalDimensions) } + } + + override fun getHorizontalInsets(context: MeasureContext, availableHeight: Float, outInsets: HorizontalInsets) { + layers.forEach { it.getHorizontalInsets(context, availableHeight, outInsets) } + } + + /** + * Prepares the [CartesianLayer]s for a difference animation. + */ + public fun prepareForTransformation( + model: CartesianChartModel?, + extraStore: MutableExtraStore, + chartValues: ChartValues, + ) { + model + ?.forEachWithLayer( + object : ModelAndLayerReceiver { + override fun invoke(model: T?, layer: CartesianLayer) { + layer.prepareForTransformation(model, extraStore, chartValues) + } + }, + ) + ?: layers.forEach { it.prepareForTransformation(null, extraStore, chartValues) } + } + + /** + * Carries out the pending difference animation. + */ + public suspend fun transform(extraStore: MutableExtraStore, fraction: Float) { + layers.forEach { it.transform(extraStore, fraction) } + } + + protected fun CartesianChartModel.forEachWithLayer(receiver: ModelAndLayerReceiver) { + val freeModels = models.toMutableList() + layers.forEach { layer -> + when (layer) { + is ColumnCartesianLayer -> freeModels.notifyReceiver(layer, receiver, freeModels) + is LineCartesianLayer -> freeModels.notifyReceiver(layer, receiver, freeModels) + else -> throw IllegalArgumentException("Unexpected `CartesianLayer` implementation.") + } + } + } + + private inline fun List.notifyReceiver( + layer: CartesianLayer, + receiver: ModelAndLayerReceiver, + freeModels: MutableList, + ) { + filterIsInstance().firstOrNull().also { receiver(it, layer) }?.also(freeModels::remove) + } + + protected interface ModelAndLayerReceiver { + public operator fun invoke(model: T?, layer: CartesianLayer) + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/CartesianChartModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/CartesianChartModel.kt new file mode 100644 index 000000000..f4d41e376 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/CartesianChartModel.kt @@ -0,0 +1,70 @@ +/* + * 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.composed + +import com.patrykandpatrick.vico.core.chart.CartesianLayer +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel +import com.patrykandpatrick.vico.core.entry.diff.DrawingModel +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore + +/** + * Stores a [CartesianChart]’s data. + */ +public interface CartesianChartModel { + /** + * The [CartesianLayer]s’ [CartesianLayerModel]s. + */ + public val models: List + + /** + * Identifies this [CartesianChartModel] in terms of the [CartesianLayerModel.id]s. + */ + public val id: Int + + /** + * Expresses the size of this [CartesianChartModel] in terms of the range of the _x_ values covered. + */ + public val width: Float + + /** + * The greatest common divisor of the _x_ values’ differences. + */ + public val xDeltaGcd: Float + + /** + * Stores auxiliary data, including [DrawingModel]s. + */ + public val extraStore: ExtraStore + + /** + * Creates an immutable copy of this [CartesianChartModel]. + */ + public fun toImmutable(): CartesianChartModel = this + + /** + * An empty [CartesianChartModel]. + */ + public companion object Empty : CartesianChartModel { + private const val ERROR_MESSAGE = "`CartesianChartModel.Empty` shouldn’t be used." + + override val models: List get() { error(ERROR_MESSAGE) } + override val id: Int get() { error(ERROR_MESSAGE) } + override val width: Float get() { error(ERROR_MESSAGE) } + override val xDeltaGcd: Float get() { error(ERROR_MESSAGE) } + override val extraStore: ExtraStore get() { error(ERROR_MESSAGE) } + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChart.kt deleted file mode 100644 index e203a43fe..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChart.kt +++ /dev/null @@ -1,169 +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.core.chart.composed - -import com.patrykandpatrick.vico.core.chart.BaseChart -import com.patrykandpatrick.vico.core.chart.Chart -import com.patrykandpatrick.vico.core.chart.Chart.ModelTransformerProvider -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.insets.HorizontalInsets -import com.patrykandpatrick.vico.core.chart.insets.Insets -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.entry.ChartEntryModel -import com.patrykandpatrick.vico.core.entry.diff.ExtraStore -import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore -import com.patrykandpatrick.vico.core.extension.set -import com.patrykandpatrick.vico.core.extension.updateAll -import com.patrykandpatrick.vico.core.marker.Marker -import java.util.TreeMap - -/** - * Combines multiple [Chart]s and draws them on top of one another. - */ -public class ComposedChart( - charts: List>, -) : BaseChart>() { - - public constructor(vararg charts: Chart) : this(charts.toList()) - - /** - * The [Chart]s that make up this [ComposedChart]. - */ - public val charts: List> = ArrayList(charts) - - private val tempInsets = Insets() - - override val entryLocationMap: TreeMap> = TreeMap() - - override val chartInsetters: Collection - get() = charts.map { it.chartInsetters }.flatten() + persistentMarkers.values - - override fun setBounds(left: Number, top: Number, right: Number, bottom: Number) { - this.bounds.set(left, top, right, bottom) - charts.forEach { chart -> chart.setBounds(left, top, right, bottom) } - } - - override fun drawChart( - context: ChartDrawContext, - model: ComposedChartEntryModel, - ) { - entryLocationMap.clear() - model.forEachModelWithChart { item, chart -> - chart.drawScrollableContent(context, item) - entryLocationMap.updateAll(chart.entryLocationMap) - } - } - - override fun drawChartInternal(context: ChartDrawContext, model: ComposedChartEntryModel) { - drawDecorationBehindChart(context) - if (model.entries.isNotEmpty()) { - drawChart(context, model) - } - } - - override fun updateHorizontalDimensions( - context: MeasureContext, - horizontalDimensions: MutableHorizontalDimensions, - model: ComposedChartEntryModel, - ) { - model.forEachModelWithChart { item, chart -> - chart.updateHorizontalDimensions(context, horizontalDimensions, item) - } - } - - override fun updateChartValues( - chartValuesManager: ChartValuesManager, - model: ComposedChartEntryModel, - xStep: Float?, - ) { - model.forEachModelWithChart { item, chart -> - chart.updateChartValues(chartValuesManager, item, xStep) - } - } - - override fun getInsets( - context: MeasureContext, - outInsets: Insets, - horizontalDimensions: HorizontalDimensions, - ) { - charts.forEach { chart -> - chart.getInsets(context, tempInsets, horizontalDimensions) - outInsets.setValuesIfGreater(tempInsets) - } - } - - override fun getHorizontalInsets(context: MeasureContext, availableHeight: Float, outInsets: HorizontalInsets) { - charts.forEach { chart -> - chart.getHorizontalInsets(context, availableHeight, tempInsets) - outInsets.setValuesIfGreater(start = tempInsets.start, end = tempInsets.end) - } - } - - private inline fun ComposedChartEntryModel.forEachModelWithChart( - action: (item: Model, chart: Chart) -> Unit, - ) { - val minSize = minOf(composedEntryCollections.size, charts.size) - for (index in 0.. getModelTransformer(): Chart.ModelTransformer = modelTransformer - } - - private class ComposedModelTransformer( - private val getModelTransformers: () -> List>, - ) : Chart.ModelTransformer() { - - override val key: ExtraStore.Key = ExtraStore.Key() - - override fun prepareForTransformation( - oldModel: T?, - newModel: T?, - extraStore: MutableExtraStore, - chartValuesProvider: ChartValuesProvider, - ) { - getModelTransformers().forEachIndexed { index, transformer -> - @Suppress("UNCHECKED_CAST") - transformer.prepareForTransformation( - (oldModel as ComposedChartEntryModel<*>?)?.composedEntryCollections?.getOrNull(index) as T?, - (newModel as ComposedChartEntryModel<*>?)?.composedEntryCollections?.getOrNull(index) as T?, - extraStore, - chartValuesProvider, - ) - } - } - - override suspend fun transform(extraStore: MutableExtraStore, fraction: Float) { - getModelTransformers().forEach { transformer -> - transformer.transform(extraStore, fraction) - } - } - } -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChartEntryModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChartEntryModel.kt deleted file mode 100644 index 3085a359e..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChartEntryModel.kt +++ /dev/null @@ -1,30 +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.core.chart.composed - -import com.patrykandpatrick.vico.core.entry.ChartEntryModel - -/** - * An extended [ChartEntryModel] that can compose multiple [ChartEntryModel]s. It is used by [ComposedChart]. - */ -public interface ComposedChartEntryModel : ChartEntryModel { - - /** - * A list of the [ChartEntryModel]s that make up this model. - */ - public val composedEntryCollections: List -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChartExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChartExtensions.kt deleted file mode 100644 index 3784a7db1..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/composed/ComposedChartExtensions.kt +++ /dev/null @@ -1,44 +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.core.chart.composed - -import com.patrykandpatrick.vico.core.chart.Chart -import com.patrykandpatrick.vico.core.entry.ChartEntryModel - -/** - * Combines this [Chart] with another one to create a [ComposedChart]. - */ -public operator fun Chart.plus( - other: Chart, -): ComposedChart = - ComposedChart(listOf(this, other)) - -/** - * Combines this [ComposedChart] with a [Chart] to create a [ComposedChart]. - */ -public operator fun ComposedChart.plus( - other: Chart, -): ComposedChart = - ComposedChart(charts + other) - -/** - * Combines this [ComposedChart] and another one into a single [ComposedChart]. - */ -public operator fun ComposedChart.plus( - other: ComposedChart, -): ComposedChart = - ComposedChart(charts + other.charts) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/Decoration.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/Decoration.kt index 30258f415..9546e26b1 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/Decoration.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/decoration/Decoration.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. @@ -17,11 +17,11 @@ package com.patrykandpatrick.vico.core.chart.decoration import android.graphics.RectF -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext /** - * A [Decoration] presents additional information on a [Chart]. + * A [Decoration] presents additional information on a [CartesianChart]. * * An example [Decoration] implementation is [ThresholdLine]. * @@ -30,18 +30,18 @@ import com.patrykandpatrick.vico.core.chart.draw.ChartDrawContext public interface Decoration { /** - * Called before the [Chart] starts drawing itself. + * Called before the [CartesianChart] starts drawing itself. * - * @param [context] holds the information needed to draw the [Chart]. - * @param [bounds] the bounding box of the [Chart]. + * @param [context] holds the information needed to draw the [CartesianChart]. + * @param [bounds] the bounding box of the [CartesianChart]. */ public fun onDrawBehindChart(context: ChartDrawContext, bounds: RectF): Unit = Unit /** - * Called immediately after the [Chart] finishes drawing itself. + * Called immediately after the [CartesianChart] finishes drawing itself. * - * @param [context] holds the information needed to draw the [Chart]. - * @param [bounds] the bounding box of the [Chart]. + * @param [context] holds the information needed to draw the [CartesianChart]. + * @param [bounds] the bounding box of the [CartesianChart]. */ public fun onDrawAboveChart(context: ChartDrawContext, bounds: RectF): Unit = Unit } 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 67cbd88ce..1c4a5eb58 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 @@ -99,18 +99,16 @@ public data class ThresholdLine( context: ChartDrawContext, bounds: RectF, ): Unit = with(context) { - val chartValues = chartValuesProvider.getChartValues() + val yRange = chartValues.getYRange(null) - val valueRange = chartValues.maxY - chartValues.minY - - val centerY = bounds.bottom - (thresholdRange.median - chartValues.minY) / valueRange * bounds.height() + val centerY = bounds.bottom - (thresholdRange.median - yRange.minY) / yRange.length * bounds.height() val topY = minOf( - bounds.bottom - (thresholdRange.endInclusive - chartValues.minY) / valueRange * bounds.height(), + bounds.bottom - (thresholdRange.endInclusive - yRange.minY) / yRange.length * bounds.height(), centerY - minimumLineThicknessDp.pixels.half, ).ceil val bottomY = maxOf( - bounds.bottom - (thresholdRange.start - chartValues.minY) / valueRange * bounds.height(), + bounds.bottom - (thresholdRange.start - yRange.minY) / yRange.length * bounds.height(), centerY + minimumLineThicknessDp.pixels.half, ).floor val textY = when (labelVerticalPosition) { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/HorizontalDimensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/HorizontalDimensions.kt index b9d731307..1c191d00f 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/HorizontalDimensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/dimensions/HorizontalDimensions.kt @@ -16,10 +16,10 @@ package com.patrykandpatrick.vico.core.chart.dimensions -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart /** - * Holds information on a [Chart]’s horizontal dimensions. + * Holds information on a [CartesianChart]’s horizontal dimensions. */ public interface HorizontalDimensions { /** @@ -73,14 +73,15 @@ public interface HorizontalDimensions { public val padding: Float get() = startPadding + endPadding /** - * Given the chart’s maximum number of major entries, calculates the width of the [Chart]’s scalable content (in - * pixels). + * Given the chart’s maximum number of major entries, calculates the width of the [CartesianChart]’s scalable + * content (in pixels). */ public fun getScalableContentWidth(maxMajorEntryCount: Int): Float = xSpacing * (maxMajorEntryCount - 1) + scalablePadding /** - * Given the chart’s maximum number of major entries, calculates the width of the [Chart]’s content (in pixels). + * Given the chart’s maximum number of major entries, calculates the width of the [CartesianChart]’s content (in + * pixels). */ public fun getContentWidth(maxMajorEntryCount: Int): Float = getScalableContentWidth(maxMajorEntryCount) + unscalablePadding 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 d0da5bef0..8cf0596b8 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 @@ -18,7 +18,7 @@ package com.patrykandpatrick.vico.core.chart.draw import android.graphics.RectF import com.patrykandpatrick.vico.core.DEF_MAX_ZOOM -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp import com.patrykandpatrick.vico.core.context.DrawContext @@ -26,17 +26,17 @@ import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.model.Point /** - * An extension of [DrawContext] that holds additional data required to render a [Chart]. + * An extension of [DrawContext] that holds additional data required to render a [CartesianChart]. */ public interface ChartDrawContext : DrawContext { /** - * The bounds in which the [Chart] will be drawn. + * The bounds in which the [CartesianChart] will be drawn. */ public val chartBounds: RectF /** - * Holds information on the [Chart]’s horizontal dimensions. + * Holds information on the [CartesianChart]’s horizontal dimensions. */ public val horizontalDimensions: HorizontalDimensions @@ -66,7 +66,7 @@ public fun MeasureContext.getMaxScrollDistance( ): Float { val contentWidth = horizontalDimensions .run { if (zoom != null) scaled(zoom) else this } - .getContentWidth(chartValuesProvider.getChartValues().getMaxMajorEntryCount()) + .getContentWidth(chartValues.getMaxMajorEntryCount()) return (layoutDirectionMultiplier * (contentWidth - chartWidth)).run { if (isLtr) coerceAtLeast(minimumValue = 0f) else coerceAtMost(maximumValue = 0f) @@ -91,7 +91,7 @@ public fun MeasureContext.getAutoZoom( autoScaleUp: AutoScaleUp, ): Float { val scalableContentWidth = - horizontalDimensions.getScalableContentWidth(chartValuesProvider.getChartValues().getMaxMajorEntryCount()) + horizontalDimensions.getScalableContentWidth(chartValues.getMaxMajorEntryCount()) val reducedChartWidth = chartBounds.width() - horizontalDimensions.unscalablePadding val fillingZoom = reducedChartWidth / scalableContentWidth return when { 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 8d2e9cd2a..fc91a4fc8 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 @@ -18,12 +18,11 @@ package com.patrykandpatrick.vico.core.chart.draw import android.graphics.Canvas import android.graphics.RectF -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.component.shape.ShapeComponent import com.patrykandpatrick.vico.core.context.DrawContext import com.patrykandpatrick.vico.core.context.MeasureContext -import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.extension.getClosestMarkerEntryModel import com.patrykandpatrick.vico.core.marker.Marker import com.patrykandpatrick.vico.core.marker.MarkerVisibilityChangeListener @@ -32,12 +31,12 @@ import com.patrykandpatrick.vico.core.model.Point /** * The anonymous implementation of [ChartDrawContext]. * - * @param canvas the canvas on which the [Chart] is to be drawn. + * @param canvas the canvas on which the [CartesianChart] is to be drawn. * @param elevationOverlayColor the color of elevation overlays, applied to [ShapeComponent]s that cast shadows. * @param measureContext holds data used for component measurements. * @param markerTouchPoint the point inside the chart’s bounds where physical touch is occurring. - * @param horizontalDimensions holds information on the [Chart]’s horizontal dimensions. - * @param chartBounds the bounds in which the [Chart] will be drawn. + * @param horizontalDimensions holds information on the [CartesianChart]’s horizontal dimensions. + * @param chartBounds the bounds in which the [CartesianChart] will be drawn. * @param horizontalScroll the horizontal scroll. * @param zoom the zoom factor. * @@ -80,10 +79,10 @@ public fun chartDrawContext( * Draws the provided [marker] on top of the chart at the given [markerTouchPoint] and notifies the * [markerVisibilityChangeListener] about the [marker]’s visibility changes. */ -public fun ChartDrawContext.drawMarker( +public fun ChartDrawContext.drawMarker( marker: Marker, markerTouchPoint: Point?, - chart: Chart, + chart: CartesianChart, markerVisibilityChangeListener: MarkerVisibilityChangeListener?, wasMarkerVisible: Boolean, setWasMarkerVisible: (Boolean) -> Unit, @@ -97,7 +96,7 @@ public fun ChartDrawContext.drawMarker( context = this, bounds = chart.bounds, markedEntries = markerEntryModels, - chartValuesProvider = chartValuesProvider, + chartValues = chartValues, ) if (wasMarkerVisible.not()) { markerVisibilityChangeListener?.onMarkerShown( diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/insets/ChartInsetter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/insets/ChartInsetter.kt index 07fc5fc37..44535bd92 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/insets/ChartInsetter.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/insets/ChartInsetter.kt @@ -17,25 +17,25 @@ package com.patrykandpatrick.vico.core.chart.insets import com.patrykandpatrick.vico.core.axis.Axis -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.context.MeasureContext import com.patrykandpatrick.vico.core.marker.Marker /** - * Enables a component to add insets to [Chart]s to make room for itself. This is used by [Axis], [Marker], and the - * like. + * Enables a component to add insets to [CartesianChart]s to make room for itself. This is used by [Axis], [Marker], and + * the like. */ public interface ChartInsetter { /** * Called during the measurement phase, before [getHorizontalInsets]. Both horizontal and vertical insets can be - * requested from this function. The final inset for a given edge of the associated [Chart] is the largest of the - * insets requested for the edge. + * requested from this function. The final inset for a given edge of the associated [CartesianChart] is the largest + * of the insets requested for the edge. * * @param context holds data used for the measuring of components. * @param outInsets used to store the requested insets. - * @param horizontalDimensions the associated [Chart]’s [HorizontalDimensions]. + * @param horizontalDimensions the [CartesianChart]’s [HorizontalDimensions]. */ public fun getInsets( context: MeasureContext, @@ -46,7 +46,7 @@ public interface ChartInsetter { /** * Called during the measurement phase, after [getInsets]. Only horizontal insets can be requested from this * function. Unless the available height is of interest, [getInsets] can be used to set all insets. The final inset - * for a given edge of the associated [Chart] is the largest of the insets requested for the edge. + * for a given edge of the associated [CartesianChart] is the largest of the insets requested for the edge. * * @param context holds data used for the measuring of components. * @param availableHeight the available height. The vertical insets are considered here. diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/insets/HorizontalInsets.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/insets/HorizontalInsets.kt index da1c25434..ff9f57837 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/insets/HorizontalInsets.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/insets/HorizontalInsets.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. @@ -16,10 +16,10 @@ package com.patrykandpatrick.vico.core.chart.insets -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart /** - * Used to apply horizontal insets to [Chart]s. + * Used to apply horizontal insets to [CartesianChart]s. * * @see ChartInsetter * @see Insets diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/layout/HorizontalLayout.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/layout/HorizontalLayout.kt index 4a73944fd..c70fde61d 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/layout/HorizontalLayout.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/layout/HorizontalLayout.kt @@ -17,26 +17,26 @@ package com.patrykandpatrick.vico.core.chart.layout import com.patrykandpatrick.vico.core.axis.horizontal.HorizontalAxis -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart /** - * Defines how a chart’s content is positioned horizontally. This affects the [Chart] and the [HorizontalAxis] + * Defines how a chart’s content is positioned horizontally. This affects the [CartesianChart] and the [HorizontalAxis] * instances. */ public sealed interface HorizontalLayout { /** - * When this is applied, the [Chart] centers each major entry in a designated segment. Some empty space is visible - * at the start and end of the [Chart]. [HorizontalAxis] instances display ticks and guidelines at the edges of the - * segments. + * When this is applied, the [CartesianChart] centers each major entry in a designated segment. Some empty space is + * visible at the start and end of the [CartesianChart]. [HorizontalAxis] instances display ticks and guidelines at + * the edges of the segments. */ public object Segmented : HorizontalLayout /** - * When this is applied, the [Chart]’s content takes up the [Chart]’s entire width (unless padding is added). - * [HorizontalAxis] instances display a tick and a guideline for each label, with the tick, guideline, and label - * vertically centered relative to one another. [scalableStartPaddingDp], [scalableEndPaddingDp], + * When this is applied, the [CartesianChart]’s content takes up the [CartesianChart]’s entire width (unless padding + * is added). [HorizontalAxis] instances display a tick and a guideline for each label, with the tick, guideline, + * and label vertically centered relative to one another. [scalableStartPaddingDp], [scalableEndPaddingDp], * [unscalableStartPaddingDp], and [unscalableEndPaddingDp] control the amount of empty space at the start and end - * of the [Chart]. Scalable padding values are multiplied by the zoom factor, unlike unscalable ones. + * of the [CartesianChart]. Scalable padding values are multiplied by the zoom factor, unlike unscalable ones. */ public class FullWidth( public val scalableStartPaddingDp: Float = 0f, 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/LineCartesianLayer.kt similarity index 74% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChart.kt rename to vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayer.kt index 3a298655c..c59722833 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/LineCartesianLayer.kt @@ -23,22 +23,19 @@ import android.graphics.RectF 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.BaseCartesianLayer import com.patrykandpatrick.vico.core.chart.DefaultPointConnector -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 import com.patrykandpatrick.vico.core.chart.insets.Insets import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout -import com.patrykandpatrick.vico.core.chart.line.LineChart.LineSpec -import com.patrykandpatrick.vico.core.chart.line.LineChart.LineSpec.PointConnector +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayer.LineSpec +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayer.LineSpec.PointConnector 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.chart.values.MutableChartValues import com.patrykandpatrick.vico.core.component.Component import com.patrykandpatrick.vico.core.component.shape.shader.DynamicShader import com.patrykandpatrick.vico.core.component.text.TextComponent @@ -46,8 +43,6 @@ import com.patrykandpatrick.vico.core.component.text.VerticalPosition import com.patrykandpatrick.vico.core.component.text.inBounds import com.patrykandpatrick.vico.core.context.DrawContext 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 @@ -65,26 +60,26 @@ import kotlin.math.max import kotlin.math.min /** - * [LineChart] displays data as a continuous line. + * [LineCartesianLayer] displays data as a continuous line. * * @param lines a [List] of [LineSpec]s defining the style of each line. * @param spacingDp the spacing between each [LineSpec.point] (in dp). * @param targetVerticalAxisPosition if this is set, any [AxisRenderer] with an [AxisPosition] equal to the provided - * value will use the [ChartValues] provided by this chart. This is meant to be used with [ComposedChart]. - * @param drawingModelInterpolator interpolates the [LineChart]’s [LineChartDrawingModel]s. + * value will use the [ChartValues] provided by this chart. + * @param drawingModelInterpolator interpolates the [LineCartesianLayer]’s [LineCartesianLayerDrawingModel]s. */ -public open class LineChart( +public open class LineCartesianLayer( public var lines: List = listOf(LineSpec()), public var spacingDp: Float = DefaultDimens.POINT_SPACING, public var targetVerticalAxisPosition: AxisPosition.Vertical? = null, public var drawingModelInterpolator: DrawingModelInterpolator< - LineChartDrawingModel.PointInfo, - LineChartDrawingModel, + LineCartesianLayerDrawingModel.PointInfo, + LineCartesianLayerDrawingModel, > = DefaultDrawingModelInterpolator(), -) : BaseChart() { +) : BaseCartesianLayer() { /** - * Creates a [LineChart] with a common style for all lines. + * Creates a [LineCartesianLayer] with a common style for all lines. * * @param line a [LineSpec] defining the style of each line. * @param spacingDp the spacing between each [LineSpec.point] (in dp). @@ -216,19 +211,19 @@ public open class LineChart( */ protected val lineBackgroundPath: Path = Path() - protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() + protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() override val entryLocationMap: HashMap> = HashMap() - override fun drawChart( + override fun drawInternal( context: ChartDrawContext, - model: ChartEntryModel, + model: LineCartesianLayerModel, ): Unit = with(context) { resetTempData() val drawingModel = model.extraStore.getOrNull(drawingModelKey) - model.entries.forEachIndexed { entryListIndex, entries -> + model.series.forEachIndexed { entryListIndex, entries -> val pointInfoMap = drawingModel?.getOrNull(entryListIndex) @@ -244,7 +239,7 @@ public open class LineChart( val drawingStart = bounds.getStart(isLtr = isLtr) + drawingStartAlignmentCorrection - horizontalScroll forEachPointWithinBoundsIndexed( - entries = entries, + series = entries, drawingStart = drawingStart, pointInfoMap = pointInfoMap, ) { entryIndex, entry, x, y, _, _ -> @@ -299,7 +294,7 @@ public open class LineChart( drawPointsAndDataLabels( lineSpec = component, - entries = entries, + series = entries, drawingStart = drawingStart, pointInfoMap = pointInfoMap, ) @@ -311,15 +306,14 @@ public open class LineChart( */ protected open fun ChartDrawContext.drawPointsAndDataLabels( lineSpec: LineSpec, - entries: List, + series: List, drawingStart: Float, - pointInfoMap: Map?, + pointInfoMap: Map?, ) { if (lineSpec.point == null && lineSpec.dataLabel == null) return - val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) forEachPointWithinBoundsIndexed( - entries = entries, + series = series, drawingStart = drawingStart, pointInfoMap = pointInfoMap, ) { _, chartEntry, x, y, previousX, nextX -> @@ -341,6 +335,7 @@ public open class LineChart( val text = lineSpec.dataLabelValueFormatter.formatValue( value = chartEntry.y, chartValues = chartValues, + verticalAxisPosition = targetVerticalAxisPosition, ) val maxWidth = getMaxDataLabelWidth(chartEntry, x, previousX, nextX) val verticalPosition = lineSpec.dataLabelVerticalPosition.inBounds( @@ -373,42 +368,39 @@ public open class LineChart( } protected fun ChartDrawContext.getMaxDataLabelWidth( - entry: ChartEntry, + entry: LineCartesianLayerModel.Entry, x: Float, previousX: Float?, nextX: Float?, - ): Int { - val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) - return when { - previousX != null && nextX != null -> min(x - previousX, nextX - x) - - previousX == null && nextX == null -> - min(horizontalDimensions.startPadding, horizontalDimensions.endPadding).doubled - - nextX != null -> { - val extraSpace = when (horizontalLayout) { - is HorizontalLayout.Segmented -> horizontalDimensions.xSpacing.half - is HorizontalLayout.FullWidth -> horizontalDimensions.startPadding - } - ((entry.x - chartValues.minX) / chartValues.xStep * horizontalDimensions.xSpacing + extraSpace) - .doubled - .coerceAtMost(nextX - x) + ): Int = when { + previousX != null && nextX != null -> min(x - previousX, nextX - x) + + previousX == null && nextX == null -> + min(horizontalDimensions.startPadding, horizontalDimensions.endPadding).doubled + + nextX != null -> { + val extraSpace = when (horizontalLayout) { + is HorizontalLayout.Segmented -> horizontalDimensions.xSpacing.half + is HorizontalLayout.FullWidth -> horizontalDimensions.startPadding } + ((entry.x - chartValues.minX) / chartValues.xStep * horizontalDimensions.xSpacing + extraSpace) + .doubled + .coerceAtMost(nextX - x) + } - else -> { - val extraSpace = when (horizontalLayout) { - is HorizontalLayout.Segmented -> horizontalDimensions.xSpacing.half - is HorizontalLayout.FullWidth -> horizontalDimensions.endPadding - } - ((chartValues.maxX - entry.x) / chartValues.xStep * horizontalDimensions.xSpacing + extraSpace) - .doubled - .coerceAtMost(x - previousX!!) + else -> { + val extraSpace = when (horizontalLayout) { + is HorizontalLayout.Segmented -> horizontalDimensions.xSpacing.half + is HorizontalLayout.FullWidth -> horizontalDimensions.endPadding } - }.toInt() - } + ((chartValues.maxX - entry.x) / chartValues.xStep * horizontalDimensions.xSpacing + extraSpace) + .doubled + .coerceAtMost(x - previousX!!) + } + }.toInt() /** - * Clears the temporary data saved during a single [drawChart] run. + * Clears the temporary data saved during a single [drawInternal] run. */ protected fun resetTempData() { entryLocationMap.clear() @@ -416,17 +408,19 @@ public open class LineChart( lineBackgroundPath.rewind() } - /** - * Performs the given [action] for each [ChartEntry] in [entries] that lies within the chart’s bounds. - */ protected open fun ChartDrawContext.forEachPointWithinBoundsIndexed( - entries: List, + series: List, drawingStart: Float, - pointInfoMap: Map?, - action: (index: Int, entry: ChartEntry, x: Float, y: Float, previousX: Float?, nextX: Float?) -> Unit, + pointInfoMap: Map?, + action: ( + index: Int, + entry: LineCartesianLayerModel.Entry, + x: Float, + y: Float, + previousX: Float?, + nextX: Float?, + ) -> Unit, ) { - val chartValues = chartValuesProvider.getChartValues(targetVerticalAxisPosition) - val minX = chartValues.minX val maxX = chartValues.maxX val xStep = chartValues.xStep @@ -435,20 +429,22 @@ public open class LineChart( var nextX: Float? = null var y: Float - var prevEntry: ChartEntry? = null - var lastEntry: ChartEntry? = null + var prevEntry: LineCartesianLayerModel.Entry? = null + var lastEntry: LineCartesianLayerModel.Entry? = null val boundsStart = bounds.getStart(isLtr = isLtr) val boundsEnd = boundsStart + layoutDirectionMultiplier * bounds.width() - fun getDrawX(entry: ChartEntry): Float = drawingStart + layoutDirectionMultiplier * + fun getDrawX(entry: LineCartesianLayerModel.Entry): Float = drawingStart + layoutDirectionMultiplier * horizontalDimensions.xSpacing * (entry.x - minX) / xStep - fun getDrawY(entry: ChartEntry): Float = - bounds.bottom - (pointInfoMap?.get(entry.x)?.y ?: ((entry.y - chartValues.minY) / chartValues.lengthY)) * + fun getDrawY(entry: LineCartesianLayerModel.Entry): Float { + val yRange = chartValues.getYRange(targetVerticalAxisPosition) + return bounds.bottom - (pointInfoMap?.get(entry.x)?.y ?: ((entry.y - yRange.minY) / yRange.length)) * bounds.height() + } - entries.forEachInAbsolutelyIndexed(minX - xStep..maxX + xStep) { index, entry, next -> + series.forEachInAbsolutelyIndexed(minX - xStep..maxX + xStep) { index, entry, next -> val previousX = x.takeIf { it.isFinite() } x = nextX ?: getDrawX(entry) @@ -479,7 +475,7 @@ public open class LineChart( override fun updateHorizontalDimensions( context: MeasureContext, horizontalDimensions: MutableHorizontalDimensions, - model: ChartEntryModel, + model: LineCartesianLayerModel, ) { with(context) { val maxPointSize = lines.maxOf { it.pointSizeDpOrZero }.pixels @@ -505,14 +501,12 @@ public open class LineChart( } } - override fun updateChartValues(chartValuesManager: ChartValuesManager, model: ChartEntryModel, xStep: Float?) { - chartValuesManager.tryUpdate( + override fun updateChartValues(chartValues: MutableChartValues, model: LineCartesianLayerModel, xStep: Float?) { + chartValues.tryUpdate( minX = axisValuesOverrider?.getMinX(model) ?: model.minX, maxX = axisValuesOverrider?.getMaxX(model) ?: model.maxX, minY = axisValuesOverrider?.getMinY(model) ?: min(model.minY, 0f), maxY = axisValuesOverrider?.getMaxY(model) ?: model.maxY, - xStep = xStep ?: model.xGcd, - chartEntryModel = model, axisPosition = targetVerticalAxisPosition, ) } @@ -522,54 +516,39 @@ public open class LineChart( outInsets: Insets, horizontalDimensions: HorizontalDimensions, ): Unit = with(context) { - outInsets.setVertical( - value = lines.maxOf { - if (it.point != null) max(a = it.lineThicknessDp, b = it.pointSizeDp) else it.lineThicknessDp - }.pixels, - ) + val verticalInset = lines + .maxOf { if (it.point != null) max(it.lineThicknessDp, it.pointSizeDp) else it.lineThicknessDp } + .pixels + .half + outInsets.setAllIfGreater(top = verticalInset, bottom = verticalInset) } - override val modelTransformerProvider: Chart.ModelTransformerProvider = object : Chart.ModelTransformerProvider { - private val modelTransformer = - LineChartModelTransformer(drawingModelKey, { targetVerticalAxisPosition }, { drawingModelInterpolator }) - - override fun getModelTransformer(): Chart.ModelTransformer = modelTransformer + override fun prepareForTransformation( + model: LineCartesianLayerModel?, + extraStore: MutableExtraStore, + chartValues: ChartValues, + ) { + drawingModelInterpolator.setModels( + old = extraStore.getOrNull(drawingModelKey), + new = model?.toDrawingModel(chartValues), + ) } - protected class LineChartModelTransformer( - override val key: ExtraStore.Key, - private val getTargetVerticalAxisPosition: () -> AxisPosition.Vertical?, - private val getDrawingModelInterpolator: () -> DrawingModelInterpolator< - LineChartDrawingModel.PointInfo, - LineChartDrawingModel, - >, - ) : 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) - } + override suspend fun transform(extraStore: MutableExtraStore, fraction: Float) { + drawingModelInterpolator + .transform(fraction) + ?.let { extraStore[drawingModelKey] = it } + ?: extraStore.remove(drawingModelKey) + } - private fun ChartEntryModel.toDrawingModel(chartValues: ChartValues): LineChartDrawingModel = entries + private fun LineCartesianLayerModel.toDrawingModel(chartValues: ChartValues): LineCartesianLayerDrawingModel { + val yRange = chartValues.getYRange(targetVerticalAxisPosition) + return series .map { series -> series.associate { entry -> - entry.x to LineChartDrawingModel.PointInfo((entry.y - chartValues.minY) / chartValues.lengthY) + entry.x to LineCartesianLayerDrawingModel.PointInfo((entry.y - yRange.minY) / yRange.length) } } - .let(::LineChartDrawingModel) + .let(::LineCartesianLayerDrawingModel) } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartDrawingModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerDrawingModel.kt similarity index 64% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartDrawingModel.kt rename to vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerDrawingModel.kt index 12a514be4..fcf8023ea 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartDrawingModel.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerDrawingModel.kt @@ -20,23 +20,23 @@ import com.patrykandpatrick.vico.core.entry.diff.DrawingModel import com.patrykandpatrick.vico.core.extension.orZero /** - * Houses drawing information for a [LineChart]. [opacity] is the lines’ opacity. + * Houses drawing information for a [LineCartesianLayer]. [opacity] is the lines’ opacity. */ -public class LineChartDrawingModel(pointInfo: List>, public val opacity: Float = 1f) : - DrawingModel(pointInfo) { +public class LineCartesianLayerDrawingModel(pointInfo: List>, public val opacity: Float = 1f) : + DrawingModel(pointInfo) { override fun transform( drawingInfo: List>, from: DrawingModel?, fraction: Float, ): DrawingModel { - val oldOpacity = (from as LineChartDrawingModel?)?.opacity.orZero - return LineChartDrawingModel(drawingInfo, oldOpacity + (opacity - oldOpacity) * fraction) + val oldOpacity = (from as LineCartesianLayerDrawingModel?)?.opacity.orZero + return LineCartesianLayerDrawingModel(drawingInfo, oldOpacity + (opacity - oldOpacity) * fraction) } /** - * Houses positional information for a [LineChart]’s point. [y] expresses the distance of the point from the bottom - * of the [LineChart] as a fraction of the [LineChart]’s height. + * Houses positional information for a [LineCartesianLayer]’s point. [y] expresses the distance of the point from + * the bottom of the [LineCartesianLayer] as a fraction of the [LineCartesianLayer]’s height. */ public class PointInfo(public val y: Float) : DrawingInfo { override fun transform(from: DrawingInfo?, fraction: Float): DrawingInfo { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerExtensions.kt similarity index 89% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartExtensions.kt rename to vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerExtensions.kt index 4e0afcb49..a8c6974be 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineChartExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerExtensions.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. @@ -34,5 +34,5 @@ internal fun Component.drawPoint( ) } -internal inline val LineChart.LineSpec.pointSizeDpOrZero: Float +internal inline val LineCartesianLayer.LineSpec.pointSizeDpOrZero: Float get() = if (point != null) pointSizeDp else 0f diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerModel.kt new file mode 100644 index 000000000..652018cfd --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/line/LineCartesianLayerModel.kt @@ -0,0 +1,106 @@ +/* + * 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.line + +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel +import com.patrykandpatrick.vico.core.entry.calculateXDeltaGcd +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.extension.rangeOfOrNull + +/** + * Stores a [LineCartesianLayer]’s data. + */ +public class LineCartesianLayerModel : CartesianLayerModel { + + public constructor(series: List>) : this(series, ExtraStore.empty) + + private constructor(series: List>, extraStore: ExtraStore) { + val entries = series.flatten() + val xRange = entries.rangeOfOrNull { it.x } ?: 0f..0f + val yRange = entries.rangeOfOrNull { it.y } ?: 0f..0f + this.series = series + this.id = series.hashCode() + this.minX = xRange.start + this.maxX = xRange.endInclusive + this.minY = yRange.start + this.maxY = yRange.endInclusive + this.xDeltaGcd = entries.calculateXDeltaGcd() + this.extraStore = extraStore + } + + private constructor( + series: List>, + id: Int, + minX: Float, + maxX: Float, + minY: Float, + maxY: Float, + xDeltaGcd: Float, + extraStore: ExtraStore, + ) { + this.series = series + this.id = id + this.minX = minX + this.maxX = maxX + this.minY = minY + this.maxY = maxY + this.xDeltaGcd = xDeltaGcd + this.extraStore = extraStore + } + + /** + * The series (lists of [Entry] instances). + */ + public val series: List> + + override val id: Int + + override val minX: Float + + override val maxX: Float + + override val minY: Float + + override val maxY: Float + + override val xDeltaGcd: Float + + override val extraStore: ExtraStore + + override fun copy(extraStore: ExtraStore): CartesianLayerModel = + LineCartesianLayerModel(series, id, minX, maxX, minY, maxY, xDeltaGcd, extraStore) + + /** + * Represents a line node. + */ + public interface Entry : CartesianLayerModel.Entry { + override val x: Float + + /** + * The _y_ coordinate. + */ + public val y: Float + } + + /** + * Stores the minimum amount of data required to create a [LineCartesianLayerModel] and facilitates this creation. + */ + public class Partial(private val series: List>) : CartesianLayerModel.Partial { + override fun complete(extraStore: ExtraStore): CartesianLayerModel = + LineCartesianLayerModel(series, extraStore) + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/AxisValuesOverrider.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/AxisValuesOverrider.kt index 2f78911ef..56b0dfd9e 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/AxisValuesOverrider.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/AxisValuesOverrider.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. @@ -16,14 +16,14 @@ package com.patrykandpatrick.vico.core.chart.values -import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel import com.patrykandpatrick.vico.core.extension.round import kotlin.math.abs /** * Overrides a chart’s minimum and maximum x-axis and y-axis values. * This can be used with [com.patrykandpatrick.vico.views.chart.BaseChartView] and the - * [com.patrykandpatrick.vico.compose.chart.Chart] composable. + * [com.patrykandpatrick.vico.compose.chart.CartesianChartHost] composable. */ public interface AxisValuesOverrider { @@ -66,37 +66,38 @@ public interface AxisValuesOverrider { maxX: Float? = null, minY: Float? = null, maxY: Float? = null, - ): AxisValuesOverrider = object : AxisValuesOverrider { + ): AxisValuesOverrider = object : AxisValuesOverrider { - override fun getMinX(model: ChartEntryModel): Float? = minX + override fun getMinX(model: CartesianLayerModel): Float? = minX - override fun getMaxX(model: ChartEntryModel): Float? = maxX + override fun getMaxX(model: CartesianLayerModel): Float? = maxX - override fun getMinY(model: ChartEntryModel): Float? = minY + override fun getMinY(model: CartesianLayerModel): Float? = minY - override fun getMaxY(model: ChartEntryModel): Float? = maxY + override fun getMaxY(model: CartesianLayerModel): Float? = maxY } /** * Creates an [AxisValuesOverrider] with adaptive minimum and maximum y-axis values. The overridden maximum - * y-axis value is equal to [ChartEntryModel.maxY] × [yFraction]. The overridden minimum y-axis value is smaller - * than [ChartEntryModel.minY] by [ChartEntryModel.maxY] × [yFraction] − [ChartEntryModel.maxY]. + * y-axis value is equal to [CartesianLayerModel.maxY] × [yFraction]. The overridden minimum y-axis value is + * smaller than [CartesianLayerModel.minY] by [CartesianLayerModel.maxY] × [yFraction] − + * [CartesianLayerModel.maxY]. */ public fun adaptiveYValues( yFraction: Float, round: Boolean = false, - ): AxisValuesOverrider = object : AxisValuesOverrider { + ): AxisValuesOverrider = object : AxisValuesOverrider { init { require(yFraction > 0f) } - override fun getMinY(model: ChartEntryModel): Float { + override fun getMinY(model: CartesianLayerModel): Float { val difference = abs(getMaxY(model) - model.maxY) return (model.minY - difference).maybeRound().coerceAtLeast(0f) } - override fun getMaxY(model: ChartEntryModel): Float = (model.maxY * yFraction).maybeRound() + override fun getMaxY(model: CartesianLayerModel): Float = (model.maxY * yFraction).maybeRound() private fun Float.maybeRound() = if (round) this.round else this } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValues.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValues.kt index 4480f9c19..1c3c1bef1 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValues.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValues.kt @@ -16,31 +16,24 @@ package com.patrykandpatrick.vico.core.chart.values -import com.patrykandpatrick.vico.core.chart.Chart -import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.axis.AxisPosition +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChartModel import kotlin.math.abs import kotlin.math.ceil /** - * Where [Chart]s get their data from. - * - * By default, [minX], [maxX], [minY], and [maxY] are equal to [ChartEntryModel.minX], - * [ChartEntryModel.maxX], [ChartEntryModel.minY], and [ChartEntryModel.maxY], respectively, - * but you can use [AxisValuesOverrider] to override these values. + * Houses information on the value ranges of a [CartesianChart]’s axes. */ public interface ChartValues { /** - * The minimum value displayed on the x-axis. By default, this is equal to [ChartEntryModel.minX] (the - * [ChartEntryModel] instance being [chartEntryModel]), but you can use [AxisValuesOverrider] to override this - * value. + * The minimum _x_ value. */ public val minX: Float /** - * The maximum value displayed on the x-axis. By default, this is equal to [ChartEntryModel.maxX] (the - * [ChartEntryModel] instance being [chartEntryModel]), but you can use [AxisValuesOverrider] to override this - * value. + * The maximum _x_ value. */ public val maxX: Float @@ -50,40 +43,59 @@ public interface ChartValues { public val xStep: Float /** - * The minimum value displayed on the y-axis. By default, this is equal to [ChartEntryModel.minY] (the - * [ChartEntryModel] instance being [chartEntryModel]), but you can use [AxisValuesOverrider] to override this - * value. + * The [CartesianChart]’s [CartesianChartModel]. */ - public val minY: Float + public val model: CartesianChartModel /** - * The maximum value displayed on the y-axis. By default, this is equal to [ChartEntryModel.maxY] (the - * [ChartEntryModel] instance being [chartEntryModel]), but you can use [AxisValuesOverrider] to override this - * value. + * Returns the _y_ range associated with the given [AxisPosition.Vertical] subclass. If [axisPosition] is `null` or + * has no associated _y_ range, the global _y_ range is returned. */ - public val maxY: Float + public fun getYRange(axisPosition: AxisPosition.Vertical?): YRange /** - * The source of the associated [Chart]’s entries. The [ChartEntryModel] defines the default values for [minX], - * [maxX], [minY], and [maxY]. + * The difference between [maxX] and [minX]. */ - public val chartEntryModel: ChartEntryModel + public val xLength: Float get() = maxX - minX /** - * The difference between [maxX] and [minX]. + * Returns the maximum number of major entries that can be present, based on [minX], [maxX], and [xStep]. */ - public val lengthX: Float - get() = maxX - minX + public fun getMaxMajorEntryCount(): Int = ceil(abs(maxX - minX) / xStep + 1).toInt() /** - * The difference between [maxY] and [minY]. + * Holds information on a _y_ range. */ - public val lengthY: Float - get() = maxY - minY + public interface YRange { + /** + * The minimum _y_ value. + */ + public val minY: Float + + /** + * The maximum _y_ value. + */ + public val maxY: Float + + /** + * The difference between [maxY] and [minY]. + */ + public val length: Float + } /** - * Returns the maximum number of major entries that can be present, based on [minX], [maxX], and [xStep]. + * An empty [ChartValues] implementation. */ - public fun getMaxMajorEntryCount(): Int = - ceil(abs(maxX - minX) / xStep + 1).toInt() + public companion object Empty : ChartValues { + private const val ERROR_MESSAGE = "ChartValues.Empty shouldn’t be used." + + override val minX: Float get() { error(ERROR_MESSAGE) } + override val maxX: Float get() { error(ERROR_MESSAGE) } + override val xStep: Float get() { error(ERROR_MESSAGE) } + override val model: CartesianChartModel = CartesianChartModel.Empty + + override fun getYRange(axisPosition: AxisPosition.Vertical?): YRange { + error("`ChartValues.Empty` shouldn’t be used.") + } + } } 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 deleted file mode 100644 index 044e5df3a..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesManager.kt +++ /dev/null @@ -1,107 +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.core.chart.values - -import com.patrykandpatrick.vico.core.axis.AxisPosition -import com.patrykandpatrick.vico.core.axis.AxisRenderer -import com.patrykandpatrick.vico.core.chart.Chart -import com.patrykandpatrick.vico.core.chart.column.ColumnChart -import com.patrykandpatrick.vico.core.chart.line.LineChart -import com.patrykandpatrick.vico.core.entry.ChartEntryModel - -/** - * Manages the [ChartValues] used by a chart. There may be many [ChartValues], but all of them have the same - * [ChartValues.minX] and [ChartValues.maxX] values. The following [ChartValues] instances exist in a chart: - * - A main [ChartValues] instance, which is used by all components by default. It’s accessible with a null key and - * always available in the drawing phase. - * - A [ChartValues] instance for [AxisRenderer]s with [AxisPosition.Vertical.Start]. It’s available when the [Chart] - * is configured to use [AxisPosition.Vertical.Start] as a key to update and retrieve its [ChartValues]. - * - A [ChartValues] instance for [AxisRenderer]s with [AxisPosition.Vertical.End]. It’s available when the [Chart] - * is configured to use [AxisPosition.Vertical.End] as a key to update and retrieve its [ChartValues]. - * - * @see ColumnChart.targetVerticalAxisPosition - * @see LineChart.targetVerticalAxisPosition - */ -public class ChartValuesManager : ChartValuesProvider { - - internal val chartValues: MutableMap = mutableMapOf() - - override fun getChartValues(axisPosition: AxisPosition.Vertical?): ChartValues = - chartValues[axisPosition] - ?.takeIf { it.hasValuesSet } - ?: chartValues.getOrPut(null) { MutableChartValues() } - - /** - * Attempts to update the stored values to the provided values. - * [MutableChartValues.minX] and [MutableChartValues.minY] can be updated to a lower value. - * [MutableChartValues.maxX] and [MutableChartValues.maxY] can be updated to a higher value. - * [MutableChartValues.chartEntryModel] and [MutableChartValues.xStep] are always updated. - * If [axisPosition] is null, only the main [ChartValues] are updated. Otherwise, both the main [ChartValues] - * and the [ChartValues] associated with the given [axisPosition] are updated. - */ - public fun tryUpdate( - minX: Float, - maxX: Float, - minY: Float, - maxY: Float, - xStep: Float, - chartEntryModel: ChartEntryModel, - axisPosition: AxisPosition.Vertical? = null, - ) { - chartValues.getOrPut(axisPosition) { MutableChartValues() } - .tryUpdate( - minX = minX, - maxX = maxX, - minY = minY, - maxY = maxY, - xStep = xStep, - chartEntryModel = chartEntryModel, - ) - - if (axisPosition != null) { - tryUpdate(minX, maxX, minY, maxY, xStep, chartEntryModel) - } else { - val mainValues = getChartValues(null) - chartValues.forEach { (key, values) -> - if (key != null) { - values.tryUpdate(minX = mainValues.minX, maxX = mainValues.maxX) - } - } - } - } - - /** - * Resets the values stored in each of the [ChartValues] instances in the [chartValues] map. - */ - public fun resetChartValues() { - 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 deleted file mode 100644 index 9f5204e29..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/chart/values/ChartValuesProvider.kt +++ /dev/null @@ -1,39 +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.core.chart.values - -import com.patrykandpatrick.vico.core.axis.AxisPosition - -/** - * Provides a chart’s [ChartValues] instances. - */ -public interface ChartValuesProvider { - - /** - * 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(axisPosition: AxisPosition.Vertical? = null): ChartValues - - /** - * An empty [ChartValuesProvider] implementation. [getChartValues] throws an exception when called. - */ - public companion object Empty : ChartValuesProvider { - override fun getChartValues(axisPosition: AxisPosition.Vertical?): ChartValues = - error("`ChartValuesProvider.Empty#getChartValues` shouldn’t be used.") - } -} 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 9245d2470..852ed0108 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 @@ -16,9 +16,11 @@ package com.patrykandpatrick.vico.core.chart.values -import com.patrykandpatrick.vico.core.entry.ChartEntry -import com.patrykandpatrick.vico.core.entry.ChartEntryModel +import com.patrykandpatrick.vico.core.axis.AxisPosition +import com.patrykandpatrick.vico.core.chart.composed.CartesianChartModel import com.patrykandpatrick.vico.core.extension.orZero +import kotlin.math.max +import kotlin.math.min /** * An implementation of [ChartValues] whose every property is mutable. @@ -31,9 +33,7 @@ public class MutableChartValues : ChartValues { private var _xStep: Float? = null - private var _minY: Float? = null - - private var _maxY: Float? = null + internal val yRanges: MutableMap = mutableMapOf() override val minX: Float get() = _minX.orZero @@ -44,65 +44,55 @@ public class MutableChartValues : ChartValues { override val xStep: Float get() = _xStep ?: 1f - override val minY: Float - get() = _minY.orZero - - override val maxY: Float - get() = _maxY.orZero + override fun getYRange(axisPosition: AxisPosition.Vertical?): ChartValues.YRange = + yRanges[axisPosition] ?: yRanges.getValue(null) - override var chartEntryModel: ChartEntryModel = emptyChartEntryModel() + override var model: CartesianChartModel = CartesianChartModel.Empty /** - * Returns `true` if all values have been set and at least one call to [tryUpdate] or [set] has been made. + * Updates [MutableChartValues.xStep] and [MutableChartValues.model]. */ - public val hasValuesSet: Boolean - get() = _minX != null || _maxX != null || _minY != null || _maxY != null + public fun update(xStep: Float? = null, model: CartesianChartModel) { + _xStep = xStep + this.model = model + } /** - * Attempts to update the stored values to the provided values. - * [MutableChartValues.minX] and [MutableChartValues.minY] can be updated to a lower value. - * [MutableChartValues.maxX] and [MutableChartValues.maxY] can be updated to a higher value. - * [MutableChartValues.chartEntryModel] and [MutableChartValues.xStep] are always updated. + * Tries to update the stored values. A minimum value can only be decreased. A maximum value can only be increased. */ - public fun tryUpdate( - minX: Float? = null, - maxX: Float? = null, - minY: Float? = null, - maxY: Float? = null, - xStep: Float? = null, - chartEntryModel: ChartEntryModel = this.chartEntryModel, - ): MutableChartValues = apply { - if (minX != null) _minX = if (_minX != null) minOf(this.minX, minX) else minX - if (maxX != null) _maxX = if (_maxX != null) maxOf(this.maxX, maxX) else maxX - if (minY != null) _minY = if (_minY != null) minOf(this.minY, minY) else minY - if (maxY != null) _maxY = if (_maxY != null) maxOf(this.maxY, maxY) else maxY - if (xStep != null) _xStep = xStep - this.chartEntryModel = chartEntryModel + public fun tryUpdate(minX: Float, maxX: Float, minY: Float, maxY: Float, axisPosition: AxisPosition.Vertical?) { + _minX = _minX?.coerceAtMost(minX) ?: minX + _maxX = _maxX?.coerceAtLeast(maxX) ?: maxX + yRanges[null]?.tryUpdate(minY, maxY) ?: run { yRanges[null] = MutableYRange(minY, maxY) } + if (axisPosition != null) { + yRanges[axisPosition]?.tryUpdate(minY, maxY) ?: run { yRanges[axisPosition] = MutableYRange(minY, maxY) } + } } /** - * Sets [minX], [maxX], [minY], and [maxY] to 0. + * Clears all values. */ public fun reset() { _minX = null _maxX = null - _minY = null - _maxY = null + yRanges.clear() _xStep = null - chartEntryModel = emptyChartEntryModel() + model = CartesianChartModel.Empty } - private companion object { - - fun emptyChartEntryModel(): ChartEntryModel = object : ChartEntryModel { - override val entries: List> = emptyList() - override val minX: Float = 0f - override val maxX: Float = 0f - override val minY: Float = 0f - override val maxY: Float = 0f - override val stackedPositiveY: Float = 0f - override val stackedNegativeY: Float = 0f - override val xGcd: Float = 1f + /** + * A mutable implementation of [ChartValues.YRange]. + */ + public class MutableYRange(override var minY: Float, override var maxY: Float) : ChartValues.YRange { + override val length: Float get() = maxY - minY + + /** + * Tries to update [MutableYRange.minY] and [MutableYRange.maxY]. [MutableYRange.minY] can only be decreased. + * [MutableYRange.maxY] can only be increased. + */ + public fun tryUpdate(minY: Float, maxY: Float) { + this.minY = min(this.minY, minY) + this.maxY = max(this.maxY, maxY) } } } @@ -114,7 +104,9 @@ 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() + private val yRanges = this@toImmutable.yRanges.toMap() + override val model: CartesianChartModel = this@toImmutable.model.toImmutable() + + override fun getYRange(axisPosition: AxisPosition.Vertical?): ChartValues.YRange = + yRanges[axisPosition] ?: yRanges.getValue(null) } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/marker/MarkerComponent.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/marker/MarkerComponent.kt index d4d54f061..61519e540 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/marker/MarkerComponent.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/component/marker/MarkerComponent.kt @@ -20,7 +20,6 @@ import android.graphics.RectF import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.chart.insets.Insets import com.patrykandpatrick.vico.core.chart.values.ChartValues -import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider import com.patrykandpatrick.vico.core.component.Component import com.patrykandpatrick.vico.core.component.shape.LineComponent import com.patrykandpatrick.vico.core.component.shape.ShapeComponent @@ -76,7 +75,7 @@ public open class MarkerComponent( context: DrawContext, bounds: RectF, markedEntries: List, - chartValuesProvider: ChartValuesProvider, + chartValues: ChartValues, ): Unit = with(context) { drawGuideline(context, bounds, markedEntries) val halfIndicatorSize = indicatorSizeDp.half.pixels @@ -91,7 +90,7 @@ public open class MarkerComponent( model.location.y + halfIndicatorSize, ) } - drawLabel(context, bounds, markedEntries, chartValuesProvider.getChartValues()) + drawLabel(context, bounds, markedEntries, chartValues) } private fun drawLabel( 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 67d526536..89c1a78f4 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 @@ -19,7 +19,6 @@ 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.ChartValues -import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider /** * [MeasureContext] holds data used by various chart components during the measuring and drawing phases. @@ -32,11 +31,9 @@ public interface MeasureContext : Extras { public val canvasBounds: RectF /** - * Provides the chart’s [ChartValues] instances. - * - * @see [ChartValuesProvider] + * The chart’s [ChartValues]. */ - public val chartValuesProvider: ChartValuesProvider + public val chartValues: ChartValues /** * 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 6922d90fa..913e1329d 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 @@ -19,7 +19,7 @@ package com.patrykandpatrick.vico.core.context import android.graphics.RectF import androidx.annotation.RestrictTo import com.patrykandpatrick.vico.core.chart.layout.HorizontalLayout -import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.chart.values.ChartValues /** * A [MeasureContext] implementation that facilitates the mutation of some of its properties. @@ -32,7 +32,7 @@ public data class MutableMeasureContext( override var horizontalLayout: HorizontalLayout = HorizontalLayout.Segmented, @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public var spToPx: (Float) -> Float, - override var chartValuesProvider: ChartValuesProvider, + override var chartValues: ChartValues, ) : 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 29f24a168..ffba59e4b 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 @@ -20,7 +20,7 @@ import android.graphics.Canvas 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.ChartValuesProvider +import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.context.DefaultExtras import com.patrykandpatrick.vico.core.context.DrawContext import com.patrykandpatrick.vico.core.context.Extras @@ -54,7 +54,7 @@ public fun drawContext( override val density: Float = density override val isLtr: Boolean = isLtr override val isHorizontalScrollEnabled: Boolean = false - override val chartValuesProvider: ChartValuesProvider = ChartValuesProvider.Empty + override val chartValues: ChartValues = ChartValues.Empty 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/CartesianLayerModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/CartesianLayerModel.kt new file mode 100644 index 000000000..48fe5b14f --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/CartesianLayerModel.kt @@ -0,0 +1,86 @@ +/* + * 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.entry + +import com.patrykandpatrick.vico.core.chart.CartesianLayer +import com.patrykandpatrick.vico.core.entry.diff.DrawingModel +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore + +/** + * Stores a [CartesianLayer]’s data. + */ +public interface CartesianLayerModel { + /** + * Identifies this [CartesianLayerModel]. + */ + public val id: Int + + /** + * The minimum _x_ value. + */ + public val minX: Float + + /** + * The maximum _x_ value. + */ + public val maxX: Float + + /** + * The minimum _y_ value. + */ + public val minY: Float + + /** + * The maximum _y_ value. + */ + public val maxY: Float + + /** + * The greatest common divisor of the _x_ values’ differences. + */ + public val xDeltaGcd: Float + + /** + * Stores auxiliary data, including [DrawingModel]s. + */ + public val extraStore: ExtraStore + + /** + * Creates a copy of this [CartesianLayerModel] with the given [ExtraStore]. + */ + public fun copy(extraStore: ExtraStore): CartesianLayerModel + + /** + * Represents a single entity in a [CartesianLayerModel]. + */ + public interface Entry { + /** + * The _x_ coordinate. + */ + public val x: Float + } + + /** + * Stores the minimum amount of data required to create a [CartesianLayerModel] and facilitates this creation. + */ + public interface Partial { + /** + * Creates a full [CartesianLayerModel] with the given [ExtraStore] from this [Partial]. + */ + public fun complete(extraStore: ExtraStore): CartesianLayerModel + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntry.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntry.kt deleted file mode 100644 index 6996a1200..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntry.kt +++ /dev/null @@ -1,51 +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.core.entry - -import com.patrykandpatrick.vico.core.chart.Chart - -/** - * The base for a single chart entry rendered by [Chart] subclasses. - * It holds information about the location of the chart entry on the x-axis and y-axis. - */ -public interface ChartEntry { - - /** - * The position of this [ChartEntry] on the x-axis. - */ - public val x: Float - - /** - * The position of this [ChartEntry] on the y-axis. - */ - public val y: Float - - /** - * @see x - */ - public operator fun component1(): Float = x - - /** - * @see y - */ - public operator fun component2(): Float = y - - /** - * Creates a copy of this [ChartEntry] implementation, but with a new [y] value. - */ - public fun withY(y: Float): ChartEntry -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryExtensions.kt index 135e4cb8f..92bf85c81 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryExtensions.kt @@ -16,86 +16,18 @@ package com.patrykandpatrick.vico.core.entry +import com.patrykandpatrick.vico.core.chart.column.ColumnCartesianLayerModel import com.patrykandpatrick.vico.core.extension.gcdWith -import com.patrykandpatrick.vico.core.extension.rangeOfOrNull import com.patrykandpatrick.vico.core.extension.rangeOfPairOrNull import kotlin.math.abs -/** - * Creates a [FloatEntry] instance. - * - * @param [x] the [FloatEntry]’s _x_ coordinate. - * @param [y] the [FloatEntry]’s _y_ coordinate. - * - * @see [entriesOf] - */ -public fun entryOf(x: Float, y: Float): FloatEntry = FloatEntry(x, y) - -/** - * Creates a [FloatEntry] instance. - - * @param [x] the [FloatEntry]’s _x_ coordinate. This will be converted to a [Float] instance. - * @param [y] the [FloatEntry]’s _y_ coordinate. This will be converted to a [Float] instance. - * - * @see [entriesOf] - */ -public fun entryOf(x: Number, y: Number): FloatEntry = entryOf(x.toFloat(), y.toFloat()) - -/** - * Creates a [List] of [FloatEntry] instances. Each of the provided [Pair]s corresponds to a single [FloatEntry], with - * the first element of the [Pair] being the [FloatEntry]’s _x_ coordinate, and the second element of the [Pair] being - * the [FloatEntry]’s _y_ coordinate. - * - * Example usage: - * - * ``` - * entriesOf(0 to 1, 1 to 2, 3 to 5) - * ``` - * - * The provided [Number] instances will be converted to [Float] instances. - * - * @see [entryOf] - */ -public fun entriesOf(vararg pairs: Pair): List = - pairs.map { (x, y) -> entryOf(x, y) } - -/** - * Creates a [List] of [FloatEntry] instances out of an array of y-axis values. - * - * The following are equivalent: - * - * ``` - * entriesOf(1, 2, 5) - * ``` - * - * ``` - * entriesOf(0 to 1, 1 to 2, 2 to 5) - * ``` - * - * An x-axis value will be automatically assigned to each y-axis value from [yValues]. The x-axis value will be equal - * to the y-axis value’s index in [yValues]. - * - * The provided [Number] instances will be converted to [Float] instances. - * - * @see [entryOf] - */ -public fun entriesOf(vararg yValues: Number): List = - yValues.mapIndexed { index, y -> entryOf(index, y) } - -internal inline val Iterable>.yRange: ClosedFloatingPointRange - get() = flatten().rangeOfOrNull { it.y } ?: 0f..0f - -internal inline val Iterable>.xRange: ClosedFloatingPointRange - get() = flatten().rangeOfOrNull { it.x } ?: 0f..0f - -internal fun Iterable>.calculateXGcd() = flatten() - .zipWithNext { firstEntry, secondEntry -> abs(secondEntry.x - firstEntry.x) } - .fold(null) { gcd, delta -> gcd?.gcdWith(delta) ?: delta } - ?.also { require(it != 0f) { "The precision of the x values is too large. The maximum is two decimal places." } } - ?: 1f +internal fun Iterable.calculateXDeltaGcd() = + zipWithNext { firstEntry, secondEntry -> abs(secondEntry.x - firstEntry.x) } + .fold(null) { gcd, delta -> gcd?.gcdWith(delta) ?: delta } + ?: 1f -internal fun Iterable>.calculateStackedYRange(): ClosedFloatingPointRange = - flatten().fold(HashMap>()) { map, entry -> +internal fun Iterable.calculateStackedYRange(): ClosedFloatingPointRange = + fold(HashMap>()) { map, entry -> val (negY, posY) = map.getOrElse(entry.x) { 0f to 0f } map[entry.x] = if (entry.y < 0f) negY + entry.y to posY else negY to posY + entry.y map 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 deleted file mode 100644 index 5ace93f79..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModel.kt +++ /dev/null @@ -1,104 +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.core.entry - -import com.patrykandpatrick.vico.core.chart.Chart -import com.patrykandpatrick.vico.core.chart.column.ColumnChart -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.entry.composed.ComposedChartEntryModelProducer -import com.patrykandpatrick.vico.core.entry.diff.DrawingModel -import com.patrykandpatrick.vico.core.entry.diff.ExtraStore - -/** - * Contains the data for a [Chart]. Pre-calculates values needed for the rendering of the [Chart]. - * - * The [Chart] may override [minX], [maxX], [minY], or [maxY] via [AxisValuesOverrider]. These overrides will be used - * in the [Chart]’s [ChartValues] instance. - * - * It’s recommended to delegate the creation of [ChartEntryModel] to [ChartEntryModelProducer] or - * [ComposedChartEntryModelProducer]. - * - * @see [ChartValues] - * @see [ChartEntryModelProducer] - * @see [ComposedChartEntryModelProducer]. - */ -public interface ChartEntryModel { - - /** - * The [ChartEntryModel]’s identifier. Different [ChartEntryModel] instances don’t necessarily have different - * identifiers. [ChartEntryModelProducer] and [ComposedChartEntryModelProducer] use the same [id] for all - * [ChartEntryModel] instances created for the purpose of running a single difference animation. This enables - * charts to differentiate between data set changes and difference animations. - */ - public val id: Int - get() = entries.hashCode() - - /** - * The chart entries ([ChartEntry] instances). Multiple lists of [ChartEntry] instances can be provided. In such a - * case, entries will be associated by index, and the [Chart] will stack or group them if it’s a [ColumnChart], - * and display multiple lines if it’s a [LineChart]. - */ - public val entries: List> - - /** - * The minimum x-axis value from among all [entries]. - */ - public val minX: Float - - /** - * The maximum x-axis value from among all [entries]. - */ - public val maxX: Float - - /** - * The minimum y-axis value from among all [entries]. - */ - public val minY: Float - - /** - * The maximum y-axis value from among all [entries]. - */ - public val maxY: Float - - /** - * The maximum cumulated y-axis value from among all sets of entries associated by [ChartEntry.x]. - */ - public val stackedPositiveY: Float - - /** - * The minimum cumulated y-axis value from among all sets of entries associated by [ChartEntry.x]. - */ - public val stackedNegativeY: Float - - /** - * The greatest common divisor of the _x_ values. - */ - public val xGcd: Float - - /** - * Houses auxiliary data, including [DrawingModel]s. - */ - public val extraStore: ExtraStore - get() = ExtraStore.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/entry/ChartEntryModelProducer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModelProducer.kt deleted file mode 100644 index 9e63c2679..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartEntryModelProducer.kt +++ /dev/null @@ -1,233 +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.core.entry - -import androidx.annotation.WorkerThread -import com.patrykandpatrick.vico.core.chart.Chart -import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider -import com.patrykandpatrick.vico.core.entry.diff.ExtraStore -import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore -import com.patrykandpatrick.vico.core.extension.copy -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex - -/** - * A [ChartModelProducer] implementation that generates [ChartEntryModel] instances. - * - * @param entryCollections the initial data set (list of series). - * @param dispatcher the [CoroutineDispatcher] to be used for update handling. - * - * @see ChartModelProducer - */ -public class ChartEntryModelProducer( - entryCollections: List>, - dispatcher: CoroutineDispatcher = Dispatchers.Default, -) : ChartModelProducer { - - private var series = emptyList>() - private var cachedInternalModel: InternalModel? = null - private val mutex = Mutex() - private val coroutineScope = CoroutineScope(dispatcher) - private val updateReceivers: HashMap = HashMap() - private val extraStore = MutableExtraStore() - - public constructor( - vararg entryCollections: List, - dispatcher: CoroutineDispatcher = Dispatchers.Default, - ) : this(entryCollections.toList(), dispatcher) - - init { - setEntries(entryCollections) - } - - /** - * Requests that the data set be updated to the provided one. If the update is accepted, `true` is returned. If the - * update is rejected, which occurs when there’s already an update in progress, `false` is returned. For suspending - * behavior, use [setEntriesSuspending]. [updateExtras] allows for adding auxiliary data, which can later be - * retrieved via [ChartEntryModel.extraStore]. - */ - public fun setEntries(entries: List>, updateExtras: (MutableExtraStore) -> Unit = {}): Boolean { - if (!mutex.tryLock()) return false - series = entries.copy() - updateExtras(extraStore) - cachedInternalModel = null - val deferredUpdates = updateReceivers.values.map { updateReceiver -> - coroutineScope.async { updateReceiver.handleUpdate() } - } - coroutineScope.launch { - deferredUpdates.awaitAll() - mutex.unlock() - } - return true - } - - /** - * Updates the data set. Unlike [setEntries], this function suspends the current coroutine and waits until an update - * can be run, meaning the update cannot be rejected. The returned [Deferred] implementation is marked as completed - * once the update has been processed. [updateExtras] allows for adding auxiliary data, which can later be retrieved - * via [ChartEntryModel.extraStore]. - */ - public suspend fun setEntriesSuspending( - entries: List>, - updateExtras: (MutableExtraStore) -> Unit = {}, - ): Deferred { - mutex.lock() - series = entries.copy() - updateExtras(extraStore) - cachedInternalModel = null - val completableDeferred = CompletableDeferred() - val deferredUpdates = updateReceivers.values.map { updateReceiver -> - coroutineScope.async { updateReceiver.handleUpdate() } - } - coroutineScope.launch { - deferredUpdates.awaitAll() - mutex.unlock() - completableDeferred.complete(Unit) - } - return completableDeferred - } - - /** - * Requests that the data set be updated to the provided one. If the update is accepted, `true` is returned. If the - * update is rejected, which occurs when there’s already an update in progress, `false` is returned. For suspending - * behavior, use [setEntriesSuspending]. [updateExtras] allows for adding auxiliary data, which can later be - * retrieved via [ChartEntryModel.extraStore]. - */ - public fun setEntries(vararg entries: List, updateExtras: (MutableExtraStore) -> Unit = {}): Boolean = - setEntries(entries.toList(), updateExtras) - - /** - * Updates the data set. Unlike [setEntries], this function suspends the current coroutine and waits until an update - * can be run, meaning the update cannot be rejected. The returned [Deferred] implementation is marked as completed - * once the update has been processed. [updateExtras] allows for adding auxiliary data, which can later be retrieved - * via [ChartEntryModel.extraStore]. - */ - public suspend fun setEntriesSuspending( - vararg entries: List, - updateExtras: (MutableExtraStore) -> Unit = {}, - ): Deferred = setEntriesSuspending(entries.toList(), updateExtras) - - private fun getInternalModel(extraStore: ExtraStore? = null) = - if (series.isEmpty()) { - null - } else { - val mergedExtraStore = this.extraStore.let { if (extraStore != null) it + extraStore else it } - cachedInternalModel?.copy(extraStore = mergedExtraStore) - ?: run { - val xRange = series.xRange - val yRange = series.yRange - val aggregateYRange = series.calculateStackedYRange() - InternalModel( - entries = series, - minX = xRange.start, - maxX = xRange.endInclusive, - minY = yRange.start, - maxY = yRange.endInclusive, - stackedPositiveY = aggregateYRange.endInclusive, - stackedNegativeY = aggregateYRange.start, - xGcd = series.calculateXGcd(), - id = series.hashCode(), - extraStore = mergedExtraStore, - ).also { cachedInternalModel = it } - } - } - - override fun getModel(): ChartEntryModel? = getInternalModel() - - override suspend fun transformModel(key: Any, fraction: Float) { - with(updateReceivers[key] ?: return) { - modelTransformer?.transform(extraStore, fraction) - val internalModel = getInternalModel(extraStore.copy()) - currentCoroutineContext().ensureActive() - onModelCreated(internalModel) - } - } - - @WorkerThread - override fun registerForUpdates( - key: Any, - cancelAnimation: () -> Unit, - startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, - getOldModel: () -> ChartEntryModel?, - modelTransformerProvider: Chart.ModelTransformerProvider?, - extraStore: MutableExtraStore, - updateChartValues: (ChartEntryModel?) -> ChartValuesProvider, - onModelCreated: (ChartEntryModel?) -> Unit, - ) { - UpdateReceiver( - cancelAnimation, - startAnimation, - onModelCreated, - extraStore, - modelTransformerProvider?.getModelTransformer(), - getOldModel, - updateChartValues, - ).run { - updateReceivers[key] = this - handleUpdate() - } - } - - override fun unregisterFromUpdates(key: Any) { - updateReceivers.remove(key) - } - - override fun isRegistered(key: Any): Boolean = updateReceivers.containsKey(key = key) - - private inner class UpdateReceiver( - val cancelAnimation: () -> Unit, - val startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, - val onModelCreated: (ChartEntryModel?) -> Unit, - val extraStore: MutableExtraStore, - val modelTransformer: Chart.ModelTransformer?, - val getOldModel: () -> ChartEntryModel?, - val updateChartValues: (ChartEntryModel?) -> ChartValuesProvider, - ) { - fun handleUpdate() { - cancelAnimation() - modelTransformer?.prepareForTransformation( - oldModel = getOldModel(), - newModel = getModel(), - extraStore = extraStore, - chartValuesProvider = updateChartValues(getModel()), - ) - startAnimation(::transformModel) - } - } - - private data class InternalModel( - override val entries: List>, - override val minX: Float, - override val maxX: Float, - override val minY: Float, - override val maxY: Float, - override val stackedPositiveY: Float, - override val stackedNegativeY: Float, - override val xGcd: Float, - override val id: Int, - override val extraStore: ExtraStore, - ) : ChartEntryModel -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartModelProducer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartModelProducer.kt deleted file mode 100644 index 8f35b4e5b..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/ChartModelProducer.kt +++ /dev/null @@ -1,77 +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.core.entry - -import com.patrykandpatrick.vico.core.chart.Chart -import com.patrykandpatrick.vico.core.chart.values.ChartValues -import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider -import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer -import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore - -/** - * Generates [ChartEntryModel]s and handles difference animations. - * - * @see ChartEntryModelProducer - * @see ComposedChartEntryModelProducer - */ -public interface ChartModelProducer { - - /** - * Returns the [ChartEntryModel] or, if no [ChartEntryModel] is available, `null`. - */ - public fun getModel(): Model? - - /** - * Returns the [ChartEntryModel] or, if no [ChartEntryModel] is available, throws an exception. - */ - public fun requireModel(): Model = getModel()!! - - /** - * Creates an intermediate [ChartEntryModel] for difference animations. [fraction] is the balance between the - * initial and target [ChartEntryModel]s. - */ - public suspend fun transformModel(key: Any, fraction: Float) - - /** - * Registers an update listener associated with a [key]. [cancelAnimation] and [startAnimation] are - * called after a data update is requested, with [cancelAnimation] being called before the update starts - * being processed (at which point [transformModel] should stop being used), and [startAnimation] being - * called once the update has been processed (at which point it’s safe to use [transformModel]). [updateChartValues] - * updates the chart’s [ChartValues] and returns its [ChartValuesProvider]. [onModelCreated] is called when a new - * [Model] has been generated. - */ - public fun registerForUpdates( - key: Any, - cancelAnimation: () -> Unit, - startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, - getOldModel: () -> Model?, - modelTransformerProvider: Chart.ModelTransformerProvider?, - extraStore: MutableExtraStore, - updateChartValues: (Model?) -> ChartValuesProvider, - onModelCreated: (Model?) -> Unit, - ) - - /** - * Checks if an update listener with the given [key] is registered. - */ - public fun isRegistered(key: Any): Boolean - - /** - * Unregisters the update listener associated with the given [key]. - */ - public fun unregisterFromUpdates(key: Any) -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/EntryListExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/EntryListExtensions.kt deleted file mode 100644 index 857e73536..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/EntryListExtensions.kt +++ /dev/null @@ -1,48 +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.core.entry - -import com.patrykandpatrick.vico.core.chart.column.ColumnChart -import com.patrykandpatrick.vico.core.chart.line.LineChart - -/** - * Creates a [ChartEntryModel] out of the given pairs of numbers, treating the first number in each pair as the _x_ - * value, and the second one as the _y_ value. - */ -public fun entryModelOf(vararg entries: Pair): ChartEntryModel = - entries - .map { (x, y) -> entryOf(x.toFloat(), y.toFloat()) } - .let { entryList -> ChartEntryModelProducer(listOf(entryList)) } - .requireModel() - -/** - * Creates a [ChartEntryModel] out of the provided array of numbers, treating each number’s index as the _x_ value, and - * the number itself as the _y_ value. - */ -public fun entryModelOf(vararg values: Number): ChartEntryModel = - values - .mapIndexed { index, value -> entryOf(index.toFloat(), value.toFloat()) } - .let { entryList -> ChartEntryModelProducer(listOf(entryList)) } - .requireModel() - -/** - * Creates a [ChartEntryModel] out of the provided list of list of [FloatEntry] instances. - * - * This can be used to create [LineChart]s with multiple lines and [ColumnChart]s with grouped or stacked columns. - */ -public fun entryModelOf(vararg values: List): ChartEntryModel = - ChartEntryModelProducer(values.toList()).requireModel() diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/CartesianChartModelProducer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/CartesianChartModelProducer.kt new file mode 100644 index 000000000..315fa1f0e --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/CartesianChartModelProducer.kt @@ -0,0 +1,274 @@ +/* + * 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.entry.composed + +import androidx.annotation.WorkerThread +import com.patrykandpatrick.vico.core.chart.CartesianLayer +import com.patrykandpatrick.vico.core.chart.composed.CartesianChartModel +import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.chart.values.MutableChartValues +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel +import com.patrykandpatrick.vico.core.entry.diff.ExtraStore +import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore +import com.patrykandpatrick.vico.core.extension.gcdWith +import com.patrykandpatrick.vico.core.extension.setAll +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex + +/** + * Creates [CartesianChartModel]s and handles data updates. + */ +public class CartesianChartModelProducer private constructor(dispatcher: CoroutineDispatcher) { + private var partials = emptyList() + private var cachedModel: Model? = null + private val mutex = Mutex() + private val coroutineScope = CoroutineScope(dispatcher) + private val updateReceivers = mutableMapOf() + private val extraStore = MutableExtraStore() + + private fun trySetPartials(partials: List): Boolean { + if (!mutex.tryLock()) return false + this.partials = partials.toList() + cachedModel = null + val deferredUpdates = updateReceivers.values.map { coroutineScope.async { it.handleUpdate() } } + coroutineScope.launch { + deferredUpdates.awaitAll() + mutex.unlock() + } + return true + } + + private suspend fun setPartials(partials: List): Deferred { + mutex.lock() + this.partials = partials.toList() + cachedModel = null + val completableDeferred = CompletableDeferred() + val deferredUpdates = updateReceivers.values.map { coroutineScope.async { it.handleUpdate() } } + coroutineScope.launch { + deferredUpdates.awaitAll() + mutex.unlock() + completableDeferred.complete(Unit) + } + return completableDeferred + } + + private fun getModel(extraStore: ExtraStore? = null) = if (partials.isEmpty()) { + null + } else { + val mergedExtraStore = this.extraStore.let { if (extraStore != null) it + extraStore else it } + cachedModel + ?.let { composedModel -> + composedModel.copy( + models = composedModel + .models + .map { model -> model.copy(extraStore = mergedExtraStore) }, + extraStore = mergedExtraStore, + ) + } + ?: Model(models = partials.map { it.complete(mergedExtraStore) }, extraStore = mergedExtraStore) + .also { cachedModel = it } + } + + /** + * Creates an intermediate [CartesianChartModel] for difference animations. [fraction] is the balance between the + * initial and target [CartesianChartModel]s. + */ + public suspend fun transformModel(key: Any, fraction: Float) { + with(updateReceivers[key] ?: return) { + transform(extraStore, fraction) + val internalModel = getModel(extraStore.copy()) + currentCoroutineContext().ensureActive() + onModelCreated(internalModel) + } + } + + /** + * Registers an update listener associated with a [key]. [cancelAnimation] and [startAnimation] are called after a + * data update is requested, with [cancelAnimation] being called before the update starts being processed (at which + * point [transformModel] should stop being used), and [startAnimation] being called once the update has been + * processed (at which point it’s safe to use [transformModel]). [updateChartValues] updates the chart’s + * [MutableChartValues] instance and returns an immutable copy of it. [onModelCreated] is called when a new + * [CartesianChartModel] has been generated. + */ + @WorkerThread + public fun registerForUpdates( + key: Any, + cancelAnimation: () -> Unit, + startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, + prepareForTransformation: (CartesianChartModel?, MutableExtraStore, ChartValues) -> Unit, + transform: suspend (MutableExtraStore, Float) -> Unit, + extraStore: MutableExtraStore, + updateChartValues: (CartesianChartModel?) -> ChartValues, + onModelCreated: (CartesianChartModel?) -> Unit, + ) { + UpdateReceiver( + cancelAnimation, + startAnimation, + onModelCreated, + extraStore, + prepareForTransformation, + transform, + updateChartValues, + ).run { + updateReceivers[key] = this + handleUpdate() + } + } + + /** + * Unregisters the update listener associated with the given [key]. + */ + public fun unregisterFromUpdates(key: Any) { + updateReceivers.remove(key) + } + + /** + * Creates a [Transaction] instance. + */ + public fun createTransaction(): Transaction = Transaction() + + /** + * Creates a [Transaction], runs [block], and calls [Transaction.tryCommit], returning its output. For suspending + * behavior, use [runTransaction]. + */ + public fun tryRunTransaction(block: Transaction.() -> Unit): Boolean = createTransaction().also(block).tryCommit() + + /** + * Creates a [Transaction], runs [block], and calls [Transaction.commit], returning its output. + */ + public suspend fun runTransaction(block: Transaction.() -> Unit): Deferred = + createTransaction().also(block).commit() + + /** + * Handles data updates. An initially empty list of [CartesianLayerModel.Partial]s is created and can be updated via + * the class’s functions. Each [CartesianLayerModel.Partial] corresponds to a [CartesianLayer]. + */ + public inner class Transaction internal constructor() { + private val newPartials = mutableListOf() + + /** + * Populates the new list of [CartesianLayerModel.Partial]s with the current [CartesianLayerModel.Partial]s. + */ + public fun populate() { + newPartials.setAll(partials) + } + + /** + * Removes the [CartesianLayerModel.Partial] at the specified index. + */ + public fun removeAt(index: Int) { + newPartials.removeAt(index) + } + + /** + * Replaces the [CartesianLayerModel.Partial] at the specified index with the provided + * [CartesianLayerModel.Partial]. + */ + public fun set(index: Int, dataSet: CartesianLayerModel.Partial) { + newPartials[index] = dataSet + } + + /** + * Adds a [CartesianLayerModel.Partial]. + */ + public fun add(dataSet: CartesianLayerModel.Partial) { + newPartials.add(dataSet) + } + + /** + * Adds a [CartesianLayerModel.Partial]. + */ + public fun add(index: Int, dataSet: CartesianLayerModel.Partial) { + newPartials.add(index, dataSet) + } + + /** + * Clears the new list of [CartesianLayerModel.Partial]s. + */ + public fun clear() { + newPartials.clear() + } + + /** + * Allows for adding auxiliary values, which can later be retrieved via [CartesianChartModel.extraStore]. + */ + public fun updateExtras(block: (MutableExtraStore) -> Unit) { + block(extraStore) + } + + /** + * Requests a data update. If the update is accepted, `true` is returned. If the update is rejected, which + * occurs when there’s already an update in progress, `false` is returned. For suspending behavior, use + * [commit]. + */ + public fun tryCommit(): Boolean = trySetPartials(newPartials) + + /** + * Runs a data update. Unlike [tryCommit], this function suspends the current coroutine and waits until an + * update can be run, meaning the update cannot be rejected. The returned [Deferred] implementation is marked as + * completed once the update has been processed. + */ + public suspend fun commit(): Deferred = setPartials(newPartials) + } + + private inner class UpdateReceiver( + val cancelAnimation: () -> Unit, + val startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, + val onModelCreated: (CartesianChartModel?) -> Unit, + val extraStore: MutableExtraStore, + val prepareForTransformation: (CartesianChartModel?, MutableExtraStore, ChartValues) -> Unit, + val transform: suspend (MutableExtraStore, Float) -> Unit, + val updateChartValues: (CartesianChartModel?) -> ChartValues, + ) { + fun handleUpdate() { + cancelAnimation() + prepareForTransformation(getModel(), extraStore, updateChartValues(getModel())) + startAnimation(::transformModel) + } + } + + private data class Model( + override val models: List, + override val extraStore: ExtraStore, + override val id: Int = models.map { it.id }.hashCode(), + override val width: Float = models.maxOf { it.maxX } - models.minOf { it.minX }, + override val xDeltaGcd: Float = models + .fold(null) { gcd, model -> gcd?.gcdWith(model.xDeltaGcd) ?: model.xDeltaGcd } + ?: 1f, + ) : CartesianChartModel + + public companion object { + /** + * Creates a [CartesianChartModelProducer], running an initial [Transaction]. [dispatcher] is the + * [CoroutineDispatcher] to be used for update handling. + */ + public fun build( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + transaction: Transaction.() -> Unit = {}, + ): CartesianChartModelProducer = + CartesianChartModelProducer(dispatcher).also { it.tryRunTransaction(transaction) } + } +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedChartEntryModelProducer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedChartEntryModelProducer.kt deleted file mode 100644 index 885692e65..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedChartEntryModelProducer.kt +++ /dev/null @@ -1,343 +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.core.entry.composed - -import androidx.annotation.WorkerThread -import com.patrykandpatrick.vico.core.chart.Chart -import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel -import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider -import com.patrykandpatrick.vico.core.entry.ChartEntry -import com.patrykandpatrick.vico.core.entry.ChartEntryModel -import com.patrykandpatrick.vico.core.entry.ChartModelProducer -import com.patrykandpatrick.vico.core.entry.calculateStackedYRange -import com.patrykandpatrick.vico.core.entry.calculateXGcd -import com.patrykandpatrick.vico.core.entry.diff.ExtraStore -import com.patrykandpatrick.vico.core.entry.diff.MutableExtraStore -import com.patrykandpatrick.vico.core.entry.xRange -import com.patrykandpatrick.vico.core.entry.yRange -import com.patrykandpatrick.vico.core.extension.copy -import com.patrykandpatrick.vico.core.extension.gcdWith -import com.patrykandpatrick.vico.core.extension.setAll -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex - -/** - * A [ChartModelProducer] implementation that generates [ComposedChartEntryModel] instances. - * - * @see ComposedChartEntryModel - * @see ChartModelProducer - */ -public class ComposedChartEntryModelProducer private constructor(dispatcher: CoroutineDispatcher) : - ChartModelProducer> { - - private var dataSets = emptyList>>() - private var cachedInternalComposedModel: InternalComposedModel? = null - private val mutex = Mutex() - private val coroutineScope = CoroutineScope(dispatcher) - private val updateReceivers = mutableMapOf() - private val extraStore = MutableExtraStore() - - private fun setDataSets(dataSets: List>>): Boolean { - if (!mutex.tryLock()) return false - this.dataSets = dataSets.copy() - cachedInternalComposedModel = null - val deferredUpdates = updateReceivers.values.map { updateReceiver -> - coroutineScope.async { updateReceiver.handleUpdate() } - } - coroutineScope.launch { - deferredUpdates.awaitAll() - mutex.unlock() - } - return true - } - - private suspend fun setDataSetsSuspending(dataSets: List>>): Deferred { - mutex.lock() - this.dataSets = dataSets.copy() - cachedInternalComposedModel = null - val completableDeferred = CompletableDeferred() - val deferredUpdates = updateReceivers.values.map { updateReceiver -> - coroutineScope.async { updateReceiver.handleUpdate() } - } - coroutineScope.launch { - deferredUpdates.awaitAll() - mutex.unlock() - completableDeferred.complete(Unit) - } - return completableDeferred - } - - private fun getInternalModel(extraStore: ExtraStore? = null) = - if (dataSets.isEmpty()) { - null - } else { - val mergedExtraStore = this.extraStore.let { if (extraStore != null) it + extraStore else it } - cachedInternalComposedModel - ?.let { composedModel -> - composedModel.copy( - composedEntryCollections = composedModel.composedEntryCollections - .map { model -> model.copy(extraStore = mergedExtraStore) }, - extraStore = mergedExtraStore, - ) - } - ?: run { - val models = dataSets.map { dataSet -> - val xRange = dataSet.xRange - val yRange = dataSet.yRange - val aggregateYRange = dataSet.calculateStackedYRange() - InternalModel( - entries = dataSet, - minX = xRange.start, - maxX = xRange.endInclusive, - minY = yRange.start, - maxY = yRange.endInclusive, - stackedPositiveY = aggregateYRange.endInclusive, - stackedNegativeY = aggregateYRange.start, - xGcd = dataSet.calculateXGcd(), - extraStore = mergedExtraStore, - ) - } - InternalComposedModel( - composedEntryCollections = models, - entries = models.map { it.entries }.flatten(), - minX = models.minOf { it.minX }, - maxX = models.maxOf { it.maxX }, - minY = models.minOf { it.minY }, - maxY = models.maxOf { it.maxY }, - stackedPositiveY = models.maxOf { it.stackedPositiveY }, - stackedNegativeY = models.minOf { it.stackedNegativeY }, - xGcd = models.fold(null) { gcd, model -> - gcd?.gcdWith(model.xGcd) ?: model.xGcd - } ?: 1f, - id = models.map { it.id }.hashCode(), - extraStore = mergedExtraStore, - ).also { cachedInternalComposedModel = it } - } - } - - override fun getModel(): ComposedChartEntryModel? = getInternalModel() - - override suspend fun transformModel(key: Any, fraction: Float) { - with(updateReceivers[key] ?: return) { - modelTransformer?.transform(extraStore, fraction) - val internalModel = getInternalModel(extraStore.copy()) - currentCoroutineContext().ensureActive() - onModelCreated(internalModel) - } - } - - @WorkerThread - override fun registerForUpdates( - key: Any, - cancelAnimation: () -> Unit, - startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, - getOldModel: () -> ComposedChartEntryModel?, - modelTransformerProvider: Chart.ModelTransformerProvider?, - extraStore: MutableExtraStore, - updateChartValues: (ComposedChartEntryModel?) -> ChartValuesProvider, - onModelCreated: (ComposedChartEntryModel?) -> Unit, - ) { - UpdateReceiver( - cancelAnimation, - startAnimation, - onModelCreated, - extraStore, - modelTransformerProvider?.getModelTransformer(), - getOldModel, - updateChartValues, - ).run { - updateReceivers[key] = this - handleUpdate() - } - } - - override fun isRegistered(key: Any): Boolean = updateReceivers.containsKey(key) - - override fun unregisterFromUpdates(key: Any) { - updateReceivers.remove(key) - } - - /** - * Creates a [Transaction] instance. - */ - public fun createTransaction(): Transaction = Transaction() - - /** - * Creates a [Transaction], runs [block], and calls [Transaction.commit], returning its output. For suspending - * behavior, use [runTransactionSuspending]. - */ - public fun runTransaction(block: Transaction.() -> Unit): Boolean = createTransaction().also(block).commit() - - /** - * Creates a [Transaction], runs [block], and calls [Transaction.commitSuspending], returning its output. - */ - public suspend fun runTransactionSuspending(block: Transaction.() -> Unit): Deferred = - createTransaction().also(block).commitSuspending() - - /** - * Handles data updates. An initially empty list of data sets is created and can be updated via the class’s - * functions. Each data set corresponds to a single nested [Chart]. - */ - public inner class Transaction internal constructor() { - private val newDataSets = mutableListOf>>() - - /** - * Populates the new list of data sets with the current data sets. - */ - public fun populate() { - newDataSets.setAll(dataSets) - } - - /** - * Replaces the data set at the specified index ([Pair.first]) with the provided data set ([Pair.second]). - */ - public fun set(pair: Pair>>) { - set(pair.first, pair.second) - } - - /** - * Removes the data set at the specified index. - */ - public fun removeAt(index: Int) { - newDataSets.removeAt(index) - } - - /** - * Replaces the data set at the specified index with the provided data set. - */ - public fun set(index: Int, dataSet: List>) { - newDataSets[index] = dataSet - } - - /** - * Adds a data set. - */ - public fun add(dataSet: List>) { - newDataSets.add(dataSet) - } - - /** - * Adds a data set. - */ - public fun add(index: Int, dataSet: List>) { - newDataSets.add(index, dataSet) - } - - /** - * Adds a data set comprising the provided series. - */ - public fun add(vararg series: List) { - add(series.toList()) - } - - /** - * Clears the new list of data sets. - */ - public fun clear() { - newDataSets.clear() - } - - /** - * Allows for adding auxiliary values, which can later be retrieved via [ChartEntryModel.extraStore]. - */ - public fun updateExtras(block: (MutableExtraStore) -> Unit) { - block(extraStore) - } - - /** - * Requests a data update. If the update is accepted, `true` is returned. If the update is rejected, which - * occurs when there’s already an update in progress, `false` is returned. For suspending behavior, use - * [commitSuspending]. - */ - public fun commit(): Boolean = setDataSets(newDataSets) - - /** - * Runs a data update. Unlike [commit], this function suspends the current coroutine and waits until an update - * can be run, meaning the update cannot be rejected. The returned [Deferred] implementation is marked as - * completed once the update has been processed. - */ - public suspend fun commitSuspending(): Deferred = setDataSetsSuspending(newDataSets) - } - - private inner class UpdateReceiver( - val cancelAnimation: () -> Unit, - val startAnimation: (transformModel: suspend (chartKey: Any, fraction: Float) -> Unit) -> Unit, - val onModelCreated: (ComposedChartEntryModel?) -> Unit, - val extraStore: MutableExtraStore, - val modelTransformer: Chart.ModelTransformer>?, - val getOldModel: () -> ComposedChartEntryModel?, - val updateChartValues: (ComposedChartEntryModel?) -> ChartValuesProvider, - ) { - fun handleUpdate() { - cancelAnimation() - modelTransformer?.prepareForTransformation( - oldModel = getOldModel(), - newModel = getModel(), - extraStore = extraStore, - chartValuesProvider = updateChartValues(getModel()), - ) - startAnimation(::transformModel) - } - } - - private data class InternalModel( - override val entries: List>, - override val minX: Float, - override val maxX: Float, - override val minY: Float, - override val maxY: Float, - override val stackedPositiveY: Float, - override val stackedNegativeY: Float, - override val xGcd: Float, - override val extraStore: ExtraStore, - ) : ChartEntryModel - - private data class InternalComposedModel( - override val composedEntryCollections: List, - override val entries: List>, - override val minX: Float, - override val maxX: Float, - override val minY: Float, - override val maxY: Float, - override val stackedPositiveY: Float, - override val stackedNegativeY: Float, - override val xGcd: Float, - override val id: Int, - override val extraStore: ExtraStore, - ) : ComposedChartEntryModel - - public companion object { - /** - * Creates a [ComposedChartEntryModelProducer], running an initial [Transaction]. [dispatcher] is the - * [CoroutineDispatcher] to be used for update handling. - */ - public fun build( - dispatcher: CoroutineDispatcher = Dispatchers.Default, - transaction: Transaction.() -> Unit = {}, - ): ComposedChartEntryModelProducer = - ComposedChartEntryModelProducer(dispatcher).also { it.runTransaction(transaction) } - } -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedEntryListExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedEntryListExtensions.kt deleted file mode 100644 index bc452ae0d..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/composed/ComposedEntryListExtensions.kt +++ /dev/null @@ -1,35 +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.core.entry.composed - -import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel -import com.patrykandpatrick.vico.core.entry.ChartEntryModel - -private fun ComposedChartEntryModelProducer.Transaction.add(chartEntryModels: List) { - chartEntryModels.forEach { add(it.entries) } -} - -/** - * Combines two [ChartEntryModel] implementations—the receiver and [other]—into a [ComposedChartEntryModel]. - */ -public operator fun Model.plus(other: Model): ComposedChartEntryModel = - ComposedChartEntryModelProducer - .build { - if (this@plus is ComposedChartEntryModel<*>) add(composedEntryCollections) else add(entries) - if (other is ComposedChartEntryModel<*>) add(other.composedEntryCollections) else add(other.entries) - } - .requireModel() diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModel.kt index b02ffbfaf..7078808a9 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModel.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/entry/diff/DrawingModel.kt @@ -16,10 +16,10 @@ package com.patrykandpatrick.vico.core.entry.diff -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.CartesianLayer /** - * Houses drawing information for a [Chart]. + * Houses drawing information for a [CartesianLayer]. */ public abstract class DrawingModel(private val drawingInfo: List>) : List> by drawingInfo { @@ -37,7 +37,7 @@ public abstract class DrawingModel(private val dra ): DrawingModel /** - * Houses positional information for a single [Chart] entity (e.g., a column or a point). + * Houses positional information for a single [CartesianLayer] entity (e.g., a column or a point). */ public interface DrawingInfo { /** diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/MapExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/MapExtensions.kt index dee58c98f..c5957a6d2 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/MapExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/extension/MapExtensions.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. @@ -16,7 +16,7 @@ package com.patrykandpatrick.vico.core.extension -import com.patrykandpatrick.vico.core.entry.ChartEntry +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel import com.patrykandpatrick.vico.core.marker.Marker import com.patrykandpatrick.vico.core.model.Point import java.util.TreeMap @@ -34,7 +34,8 @@ public fun Map>.getClosestMarkerEntryModel( ): List? = keys.findClosestPositiveValue(touchPoint.x)?.let(::get) /** - * Returns those of the [Marker.EntryModel]s stored in the [Map] whose [ChartEntry.x] is equal to [xValue]. + * Returns those of the [Marker.EntryModel]s stored in the [Map] whose [CartesianLayerModel.Entry.x] is equal to + * [xValue]. * * @see Marker.EntryModel */ diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/DecimalFormatValueFormatter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/DecimalFormatValueFormatter.kt index 195e70714..6a6930a45 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/DecimalFormatValueFormatter.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/DecimalFormatValueFormatter.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. @@ -16,6 +16,7 @@ package com.patrykandpatrick.vico.core.formatter +import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.chart.values.ChartValues import java.math.RoundingMode import java.text.DecimalFormat @@ -41,6 +42,7 @@ public open class DecimalFormatValueFormatter(private val decimalFormat: Decimal override fun formatValue( value: Float, chartValues: ChartValues, + verticalAxisPosition: AxisPosition.Vertical?, ): String = decimalFormat.format(value) public companion object { diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/DefaultValueFormatter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/DefaultValueFormatter.kt index 4ebdae340..78a313eb2 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/DefaultValueFormatter.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/DefaultValueFormatter.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. @@ -16,6 +16,7 @@ package com.patrykandpatrick.vico.core.formatter +import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.extension.toPrettyString @@ -26,5 +27,6 @@ public open class DefaultValueFormatter : ValueFormatter { override fun formatValue( value: Float, chartValues: ChartValues, + verticalAxisPosition: AxisPosition.Vertical?, ): String = value.toPrettyString() } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/PercentageFormatValueFormatter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/PercentageFormatValueFormatter.kt index 3f2340524..11a0d6384 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/PercentageFormatValueFormatter.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/PercentageFormatValueFormatter.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. @@ -16,6 +16,7 @@ package com.patrykandpatrick.vico.core.formatter +import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.chart.values.ChartValues import java.text.DecimalFormat @@ -37,8 +38,9 @@ public open class PercentageFormatValueFormatter(pattern: String) : ValueFormatt override fun formatValue( value: Float, chartValues: ChartValues, + verticalAxisPosition: AxisPosition.Vertical?, ): String { - val percentage = value / chartValues.maxY + val percentage = value / chartValues.getYRange(verticalAxisPosition).maxY return decimalFormat.format(percentage) } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/ValueFormatter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/ValueFormatter.kt index e816afc40..e26042380 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/ValueFormatter.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/formatter/ValueFormatter.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. @@ -16,6 +16,7 @@ package com.patrykandpatrick.vico.core.formatter +import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.chart.values.ChartValues /** @@ -36,5 +37,6 @@ public interface ValueFormatter { public fun formatValue( value: Float, chartValues: ChartValues, + verticalAxisPosition: AxisPosition.Vertical?, ): CharSequence } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/layout/VirtualLayout.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/layout/VirtualLayout.kt index 74a5b37b9..da3b911ec 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/layout/VirtualLayout.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/layout/VirtualLayout.kt @@ -18,12 +18,11 @@ package com.patrykandpatrick.vico.core.layout import android.graphics.RectF import com.patrykandpatrick.vico.core.axis.AxisManager -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter import com.patrykandpatrick.vico.core.chart.insets.Insets import com.patrykandpatrick.vico.core.context.MeasureContext -import com.patrykandpatrick.vico.core.entry.ChartEntryModel import com.patrykandpatrick.vico.core.extension.orZero import com.patrykandpatrick.vico.core.legend.Legend @@ -54,10 +53,10 @@ public open class VirtualLayout( * * @return the bounds applied to the chart. */ - public open fun setBounds( + public open fun setBounds( context: MeasureContext, contentBounds: RectF, - chart: Chart, + chart: CartesianChart, legend: Legend?, horizontalDimensions: HorizontalDimensions, vararg chartInsetter: ChartInsetter?, @@ -70,7 +69,6 @@ public open class VirtualLayout( axisManager.addInsetters(tempInsetters) chartInsetter.filterNotNull().forEach(tempInsetters::add) - tempInsetters.addAll(chart.chartInsetters) tempInsetters.add(chart) tempInsetters.forEach { insetter -> diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/marker/DefaultMarkerLabelFormatter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/marker/DefaultMarkerLabelFormatter.kt index b6e98aab0..3398f0452 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/marker/DefaultMarkerLabelFormatter.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/marker/DefaultMarkerLabelFormatter.kt @@ -18,7 +18,10 @@ package com.patrykandpatrick.vico.core.marker import android.text.Spannable import android.text.style.ForegroundColorSpan +import com.patrykandpatrick.vico.core.chart.column.ColumnCartesianLayerModel +import com.patrykandpatrick.vico.core.chart.line.LineCartesianLayerModel import com.patrykandpatrick.vico.core.chart.values.ChartValues +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel import com.patrykandpatrick.vico.core.extension.appendCompat import com.patrykandpatrick.vico.core.extension.sumOf import com.patrykandpatrick.vico.core.extension.transformToSpannable @@ -50,6 +53,13 @@ public class DefaultMarkerLabelFormatter(private val colorCode: Boolean = true) } } + private val CartesianLayerModel.Entry.y + get() = when (this) { + is ColumnCartesianLayerModel.Entry -> y + is LineCartesianLayerModel.Entry -> y + else -> throw IllegalArgumentException("Unexpected `CartesianLayerModel.Entry` implementation.") + } + private companion object { const val PATTERN = "%.02f" } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/marker/Marker.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/marker/Marker.kt index c4a35a535..332abcd92 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/marker/Marker.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/marker/Marker.kt @@ -17,11 +17,11 @@ package com.patrykandpatrick.vico.core.marker import android.graphics.RectF -import com.patrykandpatrick.vico.core.chart.Chart +import com.patrykandpatrick.vico.core.chart.composed.CartesianChart import com.patrykandpatrick.vico.core.chart.insets.ChartInsetter -import com.patrykandpatrick.vico.core.chart.values.ChartValuesProvider +import com.patrykandpatrick.vico.core.chart.values.ChartValues import com.patrykandpatrick.vico.core.context.DrawContext -import com.patrykandpatrick.vico.core.entry.ChartEntry +import com.patrykandpatrick.vico.core.entry.CartesianLayerModel import com.patrykandpatrick.vico.core.model.Point /** @@ -34,25 +34,25 @@ public interface Marker : ChartInsetter { * @param context the [DrawContext] used to draw the marker. * @param bounds the bounds in which the marker is drawn. * @param markedEntries a list of [EntryModel]s representing the entries to which the marker refers. - * @param chartValuesProvider the [Chart]’s [ChartValuesProvider]. + * @param chartValues the [CartesianChart]’s [ChartValues]. */ public fun draw( context: DrawContext, bounds: RectF, markedEntries: List, - chartValuesProvider: ChartValuesProvider, + chartValues: ChartValues, ) /** * Contains information on a single chart entry to which a chart marker refers. * @param location the coordinates of the indicator. - * @param entry the [ChartEntry]. - * @param color the color associated with the [ChartEntry]. - * @param index the index of the [ChartEntry] in its series. + * @param entry the [CartesianLayerModel.Entry]. + * @param color the color associated with the [CartesianLayerModel.Entry]. + * @param index the index of the [CartesianLayerModel.Entry] in its series. */ public data class EntryModel( public val location: Point, - public val entry: ChartEntry, + public val entry: CartesianLayerModel.Entry, public val color: Int, public val index: Int, ) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/scroll/AutoScrollCondition.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/scroll/AutoScrollCondition.kt index 253c9609e..e161c1f53 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/scroll/AutoScrollCondition.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/scroll/AutoScrollCondition.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. @@ -16,42 +16,31 @@ package com.patrykandpatrick.vico.core.scroll -import com.patrykandpatrick.vico.core.entry.ChartEntryModel -import com.patrykandpatrick.vico.core.extension.ifNotNull +import com.patrykandpatrick.vico.core.chart.composed.CartesianChartModel /** * Defines when an automatic scroll should be performed. */ -public fun interface AutoScrollCondition { +public fun interface AutoScrollCondition { /** * Given a chart’s new and old models, defines whether an automatic scroll should be performed. */ - public fun shouldPerformAutoScroll(newModel: Model, oldModel: Model?): Boolean + public fun shouldPerformAutoScroll(newModel: CartesianChartModel, oldModel: CartesianChartModel?): Boolean public companion object { /** * Prevents any automatic scrolling from occurring. */ - public val Never: AutoScrollCondition = AutoScrollCondition { _, _ -> false } + public val Never: AutoScrollCondition = AutoScrollCondition { _, _ -> false } /** * Triggers an automatic scroll when the size of the model increases (that is, the contents of the chart become * wider). */ - public val OnModelSizeIncreased: AutoScrollCondition = AutoScrollCondition { n, o -> - if (o != null) { - val new = n.entries - val old = o.entries - new.size > old.size || - ifNotNull( - t1 = new.maxOfOrNull { entries -> entries.size }, - t2 = old.maxOfOrNull { entries -> entries.size }, - ) { t1, t2 -> t1 > t2 } == true - } else { - false - } + public val OnModelSizeIncreased: AutoScrollCondition = AutoScrollCondition { new, old -> + old != null && (new.models.size > new.models.size || new.width > old.width) } } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/RandomEntriesGenerator.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/RandomEntriesGenerator.kt deleted file mode 100644 index 120c3a094..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/util/RandomEntriesGenerator.kt +++ /dev/null @@ -1,74 +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.core.util - -import com.patrykandpatrick.vico.core.chart.composed.ComposedChartEntryModel -import com.patrykandpatrick.vico.core.entry.ChartEntryModel -import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer -import com.patrykandpatrick.vico.core.entry.ChartModelProducer -import com.patrykandpatrick.vico.core.entry.FloatEntry -import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer -import com.patrykandpatrick.vico.core.entry.entryOf -import kotlin.random.Random - -/** - * Generates randomized chart entries. - * @param xRange the range of _x_ values. - * @param yRange the range from which _y_ values are randomly selected. - */ -public class RandomEntriesGenerator( - private val xRange: IntProgression = 0..X_RANGE_TOP, - private val yRange: IntProgression = 0..Y_RANGE_TOP, -) { - /** - * Generates a [List] of [FloatEntry] instances with randomized _y_ values. - * The size of the [List] is equal to the number of values in [xRange]. - */ - public fun generateRandomEntries(): List { - val result = ArrayList() - val yLength = yRange.last - yRange.first - for (x in xRange) { - result += entryOf(x.toFloat(), yRange.first + Random.nextFloat() * yLength) - } - return result - } - - /** - * Creates a [ChartEntryModel] containing a collection of [FloatEntry] instances with randomized _y_ values. - * The size of the collection is equal to the number of values in [xRange]. - */ - public fun randomEntryModel(): ChartEntryModel = - getChartEntryModelProducer().requireModel() - - /** - * Creates a [ComposedChartEntryModel] with three [ChartEntryModelProducer]s, each containing a collection of - * [FloatEntry] instances with randomized y values. The size of each collection is equal to the number of values in - * [xRange]. - */ - public fun randomComposedEntryModel(): ComposedChartEntryModel = ComposedChartEntryModelProducer - .build { repeat(MODEL_SIZE) { add(List(MODEL_SIZE) { generateRandomEntries() }) } } - .requireModel() - - private companion object { - const val X_RANGE_TOP = 10 - const val Y_RANGE_TOP = 20 - const val MODEL_SIZE = 3 - - fun RandomEntriesGenerator.getChartEntryModelProducer(): ChartModelProducer = - ChartEntryModelProducer(List(MODEL_SIZE) { generateRandomEntries() }) - } -}