From d46d8e9c4d92722dbcbb71c8e93e0128623f7345 Mon Sep 17 00:00:00 2001 From: Patrick Michalik <120058021+patrickmichalik@users.noreply.github.com> Date: Thu, 4 Apr 2024 17:18:08 +0200 Subject: [PATCH] Make `CartesianMarker`-related updates Co-authored-by: Patryk Goworowski --- .../vico/sample/showcase/Marker.kt | 44 ++--- .../vico/sample/showcase/charts/Chart10.kt | 4 +- .../vico/sample/showcase/charts/Chart3.kt | 6 +- .../compose/cartesian/CartesianChartHost.kt | 37 ++-- .../marker/DefaultCartesianMarker.kt | 52 ++++++ .../common/component/ComponentExtensions.kt | 10 -- .../common/component/MarkerComponent.kt | 77 --------- .../vico/core/cartesian/CartesianChart.kt | 20 +-- .../draw/ChartDrawContextExtensions.kt | 71 +++----- .../layer/CandlestickCartesianLayer.kt | 56 +++--- .../core/cartesian/layer/CartesianLayer.kt | 6 +- .../cartesian/layer/ColumnCartesianLayer.kt | 38 ++-- .../cartesian/layer/LineCartesianLayer.kt | 60 ++++--- .../CandlestickCartesianLayerMarkerTarget.kt | 51 ++++++ .../core/cartesian/marker/CartesianMarker.kt | 54 ++---- ...er.kt => CartesianMarkerValueFormatter.kt} | 18 +- ...CartesianMarkerVisibilityChangeListener.kt | 53 ------ .../CartesianMarkerVisibilityListener.kt | 35 ++++ .../ColumnCartesianLayerMarkerTarget.kt | 43 +++++ .../marker/DefaultCartesianMarker.kt} | 162 ++++++++---------- .../DefaultCartesianMarkerLabelFormatter.kt | 87 ---------- .../DefaultCartesianMarkerValueFormatter.kt | 105 ++++++++++++ .../marker/LineCartesianLayerMarkerTarget.kt | 41 +++++ .../cartesian/model/CartesianLayerModel.kt | 6 +- .../vico/core/common/Defaults.kt | 2 +- .../common/extension/CollectionExtensions.kt | 15 -- .../core/common/extension/MapExtensions.kt | 47 ----- .../views/cartesian/CartesianChartView.kt | 26 ++- 28 files changed, 602 insertions(+), 624 deletions(-) create mode 100644 vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/marker/DefaultCartesianMarker.kt delete mode 100644 vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/component/MarkerComponent.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CandlestickCartesianLayerMarkerTarget.kt rename vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/{CartesianMarkerLabelFormatter.kt => CartesianMarkerValueFormatter.kt} (66%) delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerVisibilityChangeListener.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerVisibilityListener.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/ColumnCartesianLayerMarkerTarget.kt rename vico/core/src/main/java/com/patrykandpatrick/vico/core/{common/component/CartesianMarkerComponent.kt => cartesian/marker/DefaultCartesianMarker.kt} (51%) delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarkerLabelFormatter.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarkerValueFormatter.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/LineCartesianLayerMarkerTarget.kt delete mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/common/extension/MapExtensions.kt diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/Marker.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/Marker.kt index 10eb9f6db..cd22d64f0 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/Marker.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/Marker.kt @@ -22,19 +22,18 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent import com.patrykandpatrick.vico.compose.common.component.fixed import com.patrykandpatrick.vico.compose.common.component.rememberLayeredComponent -import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent import com.patrykandpatrick.vico.compose.common.component.rememberTextComponent import com.patrykandpatrick.vico.compose.common.dimension.dimensionsOf -import com.patrykandpatrick.vico.compose.common.shape.dashedShape import com.patrykandpatrick.vico.compose.common.shape.markerCorneredShape import com.patrykandpatrick.vico.core.cartesian.CartesianMeasureContext import com.patrykandpatrick.vico.core.cartesian.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.cartesian.insets.Insets import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.common.component.CartesianMarkerComponent +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker import com.patrykandpatrick.vico.core.common.component.TextComponent import com.patrykandpatrick.vico.core.common.extension.copyColor import com.patrykandpatrick.vico.core.common.shape.Corner @@ -42,7 +41,8 @@ import com.patrykandpatrick.vico.core.common.shape.Shapes @Composable internal fun rememberMarker( - labelPosition: CartesianMarkerComponent.LabelPosition = CartesianMarkerComponent.LabelPosition.Top, + labelPosition: DefaultCartesianMarker.LabelPosition = DefaultCartesianMarker.LabelPosition.Top, + showIndicator: Boolean = true, ): CartesianMarker { val labelBackgroundShape = Shapes.markerCorneredShape(Corner.FullyRounded) val labelBackground = @@ -75,25 +75,25 @@ internal fun rememberMarker( ), padding = dimensionsOf(10.dp), ) - val guideline = - rememberLineComponent( - color = MaterialTheme.colorScheme.onSurface.copy(.2f), - thickness = 2.dp, - shape = Shapes.dashedShape(shape = Shapes.pillShape, dashLength = 8.dp, gapLength = 4.dp), - ) - return remember(label, labelPosition, indicator, guideline) { - object : CartesianMarkerComponent(label, labelPosition, indicator, guideline) { - init { - indicatorSizeDp = 36f - onApplyEntryColor = { entryColor -> - indicatorRearComponent.color = entryColor.copyColor(alpha = .15f) - with(indicatorCenterComponent) { - color = entryColor - setShadow(radius = 12f, color = entryColor) + val guideline = rememberAxisGuidelineComponent() + return remember(label, labelPosition, indicator, showIndicator, guideline) { + object : DefaultCartesianMarker( + label = label, + labelPosition = labelPosition, + indicator = if (showIndicator) indicator else null, + indicatorSizeDp = 36f, + setIndicatorColor = + if (showIndicator) { + { color -> + indicatorRearComponent.color = color.copyColor(alpha = .15f) + indicatorCenterComponent.color = color + indicatorCenterComponent.setShadow(radius = 12f, color = color) } - } - } - + } else { + null + }, + guideline = guideline, + ) { override fun getInsets( context: CartesianMeasureContext, outInsets: Insets, diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart10.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart10.kt index 1f416d152..76bd0dc94 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart10.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart10.kt @@ -66,7 +66,7 @@ private fun ComposeChart10( modelProducer: CartesianChartModelProducer, modifier: Modifier, ) { - val marker = rememberMarker() + val marker = rememberMarker(showIndicator = false) CartesianChartHost( chart = rememberCartesianChart( @@ -85,7 +85,7 @@ private fun ViewChart10( modelProducer: CartesianChartModelProducer, modifier: Modifier, ) { - val marker = rememberMarker() + val marker = rememberMarker(showIndicator = false) AndroidViewBinding(Chart10Binding::inflate, modifier = modifier) { with(chartView) { runInitialAnimation = false diff --git a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart3.kt b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart3.kt index ce1c46eb9..23820e9d4 100644 --- a/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart3.kt +++ b/sample/src/main/java/com/patrykandpatrick/vico/sample/showcase/charts/Chart3.kt @@ -42,10 +42,10 @@ import com.patrykandpatrick.vico.compose.common.shader.color import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker import com.patrykandpatrick.vico.core.cartesian.model.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.model.lineSeries import com.patrykandpatrick.vico.core.cartesian.values.AxisValueOverrider -import com.patrykandpatrick.vico.core.common.component.CartesianMarkerComponent import com.patrykandpatrick.vico.core.common.shader.DynamicShaders import com.patrykandpatrick.vico.core.common.shape.Shapes import com.patrykandpatrick.vico.databinding.Chart3Binding @@ -122,7 +122,7 @@ private fun ComposeChart3( ), modelProducer = modelProducer, modifier = modifier, - marker = rememberMarker(CartesianMarkerComponent.LabelPosition.AroundPoint), + marker = rememberMarker(DefaultCartesianMarker.LabelPosition.AroundPoint), runInitialAnimation = false, horizontalLayout = HorizontalLayout.fullWidth(), zoomState = rememberVicoZoomState(zoomEnabled = false), @@ -134,7 +134,7 @@ private fun ViewChart3( modelProducer: CartesianChartModelProducer, modifier: Modifier, ) { - val marker = rememberMarker(CartesianMarkerComponent.LabelPosition.AroundPoint) + val marker = rememberMarker(DefaultCartesianMarker.LabelPosition.AroundPoint) AndroidViewBinding(Chart3Binding::inflate, modifier) { with(chartView) { diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/CartesianChartHost.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/CartesianChartHost.kt index 02b8bb32e..6f65b79bb 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/CartesianChartHost.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/CartesianChartHost.kt @@ -52,7 +52,7 @@ import com.patrykandpatrick.vico.core.cartesian.dimensions.MutableHorizontalDime import com.patrykandpatrick.vico.core.cartesian.draw.cartesianChartDrawContext import com.patrykandpatrick.vico.core.cartesian.draw.drawMarker import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityChangeListener +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener import com.patrykandpatrick.vico.core.cartesian.model.CartesianChartModel import com.patrykandpatrick.vico.core.cartesian.model.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.values.ChartValues @@ -74,7 +74,7 @@ import kotlinx.coroutines.launch * @param modelProducer creates and updates the [CartesianChartModel]. * @param modifier the modifier to be applied to the chart. * @param marker appears when the chart is touched, highlighting the entry or entries nearest to the touch point. - * @param markerVisibilityChangeListener allows for listening to [marker] visibility changes. + * @param markerVisibilityListener allows for listening to [marker] visibility changes. * @param scrollState houses information on the [CartesianChart]’s scroll value. Allows for scroll customization and * programmatic scrolling. * @param zoomState houses information on the [CartesianChart]’s zoom factor. Allows for zoom customization. @@ -92,7 +92,7 @@ public fun CartesianChartHost( modelProducer: CartesianChartModelProducer, modifier: Modifier = Modifier, marker: CartesianMarker? = null, - markerVisibilityChangeListener: CartesianMarkerVisibilityChangeListener? = null, + markerVisibilityListener: CartesianMarkerVisibilityListener? = null, scrollState: VicoScrollState = rememberVicoScrollState(), zoomState: VicoZoomState = rememberDefaultVicoZoomState(scrollState.scrollEnabled), diffAnimationSpec: AnimationSpec? = defaultCartesianDiffAnimationSpec, @@ -118,7 +118,7 @@ public fun CartesianChartHost( model = model, oldModel = previousModel, marker = marker, - markerVisibilityChangeListener = markerVisibilityChangeListener, + markerVisibilityListener = markerVisibilityListener, scrollState = scrollState, zoomState = zoomState, horizontalLayout = horizontalLayout, @@ -138,7 +138,7 @@ public fun CartesianChartHost( * @param model the [CartesianChartModel]. * @param modifier the modifier to be applied to the chart. * @param marker appears when the chart is touched, highlighting the entry or entries nearest to the touch point. - * @param markerVisibilityChangeListener allows for listening to [marker] visibility changes. + * @param markerVisibilityListener allows for listening to [marker] visibility changes. * @param scrollState houses information on the [CartesianChart]’s scroll value. Allows for scroll customization and * programmatic scrolling. * @param zoomState houses information on the [CartesianChart]’s zoom factor. Allows for zoom customization. @@ -155,7 +155,7 @@ public fun CartesianChartHost( model: CartesianChartModel, modifier: Modifier = Modifier, marker: CartesianMarker? = null, - markerVisibilityChangeListener: CartesianMarkerVisibilityChangeListener? = null, + markerVisibilityListener: CartesianMarkerVisibilityListener? = null, scrollState: VicoScrollState = rememberVicoScrollState(), zoomState: VicoZoomState = rememberDefaultVicoZoomState(scrollState.scrollEnabled), oldModel: CartesianChartModel? = null, @@ -177,7 +177,7 @@ public fun CartesianChartHost( chart = chart, model = model, marker = marker, - markerVisibilityChangeListener = markerVisibilityChangeListener, + markerVisibilityListener = markerVisibilityListener, scrollState = scrollState, zoomState = zoomState, oldModel = oldModel, @@ -192,7 +192,7 @@ internal fun CartesianChartHostImpl( chart: CartesianChart, model: CartesianChartModel, marker: CartesianMarker?, - markerVisibilityChangeListener: CartesianMarkerVisibilityChangeListener?, + markerVisibilityListener: CartesianMarkerVisibilityListener?, scrollState: VicoScrollState, zoomState: VicoZoomState, oldModel: CartesianChartModel?, @@ -209,10 +209,9 @@ internal fun CartesianChartHostImpl( with(LocalContext.current) { ::spToPx }, chartValues, ) - val lastMarkerEntryModels = remember { mutableStateOf(emptyList()) } + val previousMarkerX = remember { ValueWrapper(null) } val elevationOverlayColor = vicoTheme.elevationOverlayColor.toArgb() - val (wasMarkerVisible, setWasMarkerVisible) = remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() var previousModelID by remember { ValueWrapper(model.id) } val horizontalDimensions = remember { MutableHorizontalDimensions() } @@ -279,16 +278,14 @@ internal fun CartesianChartHostImpl( chart.draw(chartDrawContext, model) if (marker != null) { - chartDrawContext.drawMarker( - marker = marker, - markerTouchPoint = markerTouchPoint.value, - chart = chart, - markerVisibilityChangeListener = markerVisibilityChangeListener, - wasMarkerVisible = wasMarkerVisible, - setWasMarkerVisible = setWasMarkerVisible, - lastMarkerEntryModels = lastMarkerEntryModels.value, - onMarkerEntryModelsChange = lastMarkerEntryModels.component2(), - ) + previousMarkerX.value = + chartDrawContext.drawMarker( + marker, + markerTouchPoint.value, + chart, + markerVisibilityListener, + previousMarkerX.value, + ) } measureContext.reset() diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/marker/DefaultCartesianMarker.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/marker/DefaultCartesianMarker.kt new file mode 100644 index 000000000..4d06b249a --- /dev/null +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/marker/DefaultCartesianMarker.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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.cartesian.marker + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerValueFormatter +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarkerValueFormatter +import com.patrykandpatrick.vico.core.common.Defaults +import com.patrykandpatrick.vico.core.common.component.Component +import com.patrykandpatrick.vico.core.common.component.LineComponent +import com.patrykandpatrick.vico.core.common.component.TextComponent + +/** Creates and remembers a [DefaultCartesianMarker]. */ +@Composable +public fun rememberDefaultCartesianMarker( + label: TextComponent, + valueFormatter: CartesianMarkerValueFormatter = remember { DefaultCartesianMarkerValueFormatter() }, + labelPosition: DefaultCartesianMarker.LabelPosition = DefaultCartesianMarker.LabelPosition.Top, + indicator: Component? = null, + indicatorSize: Dp = Defaults.MARKER_INDICATOR_SIZE.dp, + setIndicatorColor: ((Int) -> Unit)? = null, + guideline: LineComponent? = null, +): DefaultCartesianMarker = + remember(label, valueFormatter, labelPosition, indicator, indicatorSize, setIndicatorColor, guideline) { + DefaultCartesianMarker( + label, + valueFormatter, + labelPosition, + indicator, + indicatorSize.value, + setIndicatorColor, + guideline, + ) + } diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/component/ComponentExtensions.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/component/ComponentExtensions.kt index 8ccd4b4df..16e9fa813 100644 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/component/ComponentExtensions.kt +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/component/ComponentExtensions.kt @@ -23,18 +23,8 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.patrykandpatrick.vico.core.common.Defaults.SHADOW_COLOR -import com.patrykandpatrick.vico.core.common.component.CartesianMarkerComponent import com.patrykandpatrick.vico.core.common.component.ShapeComponent -/** - * The indicator size. - */ -public var CartesianMarkerComponent.indicatorSize: Dp - get() = indicatorSizeDp.dp - set(value) { - indicatorSizeDp = value.value - } - /** * Applies a drop shadow to this [ShapeComponent]. * diff --git a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/component/MarkerComponent.kt b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/component/MarkerComponent.kt deleted file mode 100644 index 4eb0d73cf..000000000 --- a/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/common/component/MarkerComponent.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2024 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.common.component - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerLabelFormatter -import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarkerLabelFormatter -import com.patrykandpatrick.vico.core.common.component.CartesianMarkerComponent -import com.patrykandpatrick.vico.core.common.component.Component -import com.patrykandpatrick.vico.core.common.component.LineComponent -import com.patrykandpatrick.vico.core.common.component.TextComponent - -/** Creates and remembers a [CartesianMarkerComponent]. */ -@Composable -public fun rememberMarkerComponent( - label: TextComponent, - labelFormatter: CartesianMarkerLabelFormatter = remember { DefaultCartesianMarkerLabelFormatter() }, - labelPosition: CartesianMarkerComponent.LabelPosition = CartesianMarkerComponent.LabelPosition.Top, - indicator: Component? = null, - guideline: LineComponent? = null, -): CartesianMarkerComponent = - remember(label, labelPosition, indicator, guideline) { - CartesianMarkerComponent(label, labelPosition, indicator, guideline) - } - .apply { this.labelFormatter = labelFormatter } - -/** Creates and remembers a [CartesianMarkerComponent]. */ -@Suppress("DeprecatedCallableAddReplaceWith") -@Deprecated( - "Use the overload with a `labelFormatter` parameter instead. (If you’re using named arguments, ignore this " + - "warning. The deprecated overload is more specific, but the new one matches and will be used once the " + - "deprecated one has been removed.)", -) -@Composable -public fun rememberMarkerComponent( - label: TextComponent, - labelPosition: CartesianMarkerComponent.LabelPosition = CartesianMarkerComponent.LabelPosition.Top, - indicator: Component? = null, - guideline: LineComponent? = null, -): CartesianMarkerComponent = - remember(label, labelPosition, indicator, guideline) { - CartesianMarkerComponent(label, labelPosition, indicator, guideline) - } - -/** - * Creates a [MarkerComponent]. - */ -@Composable -@Deprecated( - "Use `rememberMarkerComponent` instead.", - ReplaceWith( - "rememberMarkerComponent(label = label, indicator = indicator, guideline = guideline)", - "com.patrykandpatrick.vico.compose.component.marker.rememberMarkerComponent", - ), -) -public fun markerComponent( - label: TextComponent, - indicator: Component, - guideline: LineComponent, -): CartesianMarkerComponent = - @Suppress("DEPRECATION") - rememberMarkerComponent(label = label, indicator = indicator, guideline = guideline) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CartesianChart.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CartesianChart.kt index ea211325c..17ef170d4 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CartesianChart.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/CartesianChart.kt @@ -38,13 +38,10 @@ import com.patrykandpatrick.vico.core.cartesian.values.ChartValues import com.patrykandpatrick.vico.core.cartesian.values.MutableChartValues import com.patrykandpatrick.vico.core.common.MutableExtraStore import com.patrykandpatrick.vico.core.common.dimension.BoundsAware -import com.patrykandpatrick.vico.core.common.extension.getEntryModel import com.patrykandpatrick.vico.core.common.extension.inClip import com.patrykandpatrick.vico.core.common.extension.set import com.patrykandpatrick.vico.core.common.extension.setAll -import com.patrykandpatrick.vico.core.common.extension.updateAll import com.patrykandpatrick.vico.core.common.legend.Legend -import java.util.TreeMap /** * A chart based on a Cartesian coordinate plane, composed of [CartesianLayer]s. @@ -62,6 +59,7 @@ public open class CartesianChart( private val tempInsets = Insets() private val axisManager = AxisManager() private val virtualLayout = VirtualLayout(axisManager) + private val _markerTargets = mutableMapOf>() private val drawingModelAndLayerConsumer = object : ModelAndLayerConsumer { @@ -72,7 +70,7 @@ public open class CartesianChart( layer: CartesianLayer, ) { layer.draw(context, model ?: return) - entryLocationMap.updateAll(layer.entryLocationMap) + layer.markerTargets.forEach { _markerTargets.getOrPut(it.key) { mutableListOf() } += it.value } } } @@ -124,10 +122,8 @@ public open class CartesianChart( */ public val chartInsetters: Collection = persistentMarkers.values - /** - * Links _x_ values to [CartesianMarker.EntryModel]s. - */ - public val entryLocationMap: TreeMap> = TreeMap() + /** Links _x_ values to [CartesianMarker.Target]s. */ + public val markerTargets: Map> = _markerTargets /** * The start axis. @@ -174,7 +170,7 @@ public open class CartesianChart( bounds: RectF, marker: CartesianMarker?, ) { - entryLocationMap.clear() + _markerTargets.clear() model.forEachWithLayer( horizontalDimensionUpdateModelAndLayerConsumer.apply { this.context = context @@ -207,11 +203,7 @@ public open class CartesianChart( context.canvas.inClip(bounds.left, 0f, bounds.right, context.canvas.height.toFloat()) { decorations.forEach { it.onDrawAboveChart(context, bounds) } } - persistentMarkers.forEach { (x, marker) -> - entryLocationMap.getEntryModel(x)?.let { model -> - marker.draw(context, bounds, model, context.chartValues) - } - } + persistentMarkers.forEach { (x, marker) -> markerTargets[x]?.let { targets -> marker.draw(context, targets) } } legend?.draw(context, bounds) } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/draw/ChartDrawContextExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/draw/ChartDrawContextExtensions.kt index f2ecf2dec..15062c51c 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/draw/ChartDrawContextExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/draw/ChartDrawContextExtensions.kt @@ -23,10 +23,10 @@ import com.patrykandpatrick.vico.core.cartesian.CartesianChart import com.patrykandpatrick.vico.core.cartesian.CartesianMeasureContext import com.patrykandpatrick.vico.core.cartesian.dimensions.HorizontalDimensions import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityChangeListener +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener import com.patrykandpatrick.vico.core.common.DrawContext import com.patrykandpatrick.vico.core.common.Point -import com.patrykandpatrick.vico.core.common.extension.getClosestMarkerEntryModel +import kotlin.math.abs /** @suppress */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -72,47 +72,28 @@ public fun CartesianChartDrawContext.drawMarker( marker: CartesianMarker, markerTouchPoint: Point?, chart: CartesianChart, - markerVisibilityChangeListener: CartesianMarkerVisibilityChangeListener?, - wasMarkerVisible: Boolean, - setWasMarkerVisible: (Boolean) -> Unit, - lastMarkerEntryModels: List, - onMarkerEntryModelsChange: (List) -> Unit, -) { - markerTouchPoint - ?.let(chart.entryLocationMap::getClosestMarkerEntryModel) - ?.let { markerEntryModels -> - marker.draw( - context = this, - bounds = chart.bounds, - markedEntries = markerEntryModels, - chartValues = chartValues, - ) - if (wasMarkerVisible.not()) { - markerVisibilityChangeListener?.onMarkerShown( - marker = marker, - markerEntryModels = markerEntryModels, - ) - setWasMarkerVisible(true) - } - val didMarkerMove = lastMarkerEntryModels.hasMoved(markerEntryModels) - if (wasMarkerVisible && didMarkerMove) { - onMarkerEntryModelsChange(markerEntryModels) - if (lastMarkerEntryModels.isNotEmpty()) { - markerVisibilityChangeListener?.onMarkerMoved( - marker = marker, - markerEntryModels = markerEntryModels, - ) - } - } - } ?: marker - .takeIf { wasMarkerVisible } - ?.also { - markerVisibilityChangeListener?.onMarkerHidden(marker = marker) - setWasMarkerVisible(false) - } + visibilityListener: CartesianMarkerVisibilityListener?, + previousX: Float?, +): Float? { + if (markerTouchPoint == null || chart.markerTargets.isEmpty()) { + if (previousX != null) visibilityListener?.onHidden(marker) + return null + } + var targets = chart.markerTargets.values.first() + var previousDistance = abs(markerTouchPoint.x - targets.first().canvasX) + for (i in 1.. previousDistance) break + targets = potentialTargets + previousDistance = distance + } + marker.draw(this, targets) + val x = targets.first().x + if (previousX == null) { + visibilityListener?.onShown(marker, targets) + } else if (x != previousX) { + visibilityListener?.onMoved(marker, targets) + } + return x } - -private fun List.xPosition(): Float? = firstOrNull()?.entry?.x - -private fun List.hasMoved(other: List): Boolean = - xPosition() != other.xPosition() diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CandlestickCartesianLayer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CandlestickCartesianLayer.kt index ef4d3a7d5..5abc04bae 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CandlestickCartesianLayer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CandlestickCartesianLayer.kt @@ -25,12 +25,12 @@ import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis import com.patrykandpatrick.vico.core.cartesian.dimensions.MutableHorizontalDimensions import com.patrykandpatrick.vico.core.cartesian.draw.CartesianChartDrawContext import com.patrykandpatrick.vico.core.cartesian.layer.CandlestickCartesianLayer.Candle +import com.patrykandpatrick.vico.core.cartesian.marker.CandlestickCartesianLayerMarkerTarget import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.cartesian.marker.put import com.patrykandpatrick.vico.core.cartesian.model.CandlestickCartesianLayerDrawingModel import com.patrykandpatrick.vico.core.cartesian.model.CandlestickCartesianLayerModel import com.patrykandpatrick.vico.core.cartesian.model.CandlestickCartesianLayerModel.Entry.Change -import com.patrykandpatrick.vico.core.cartesian.model.forEachInIndexed +import com.patrykandpatrick.vico.core.cartesian.model.forEachIn import com.patrykandpatrick.vico.core.cartesian.values.ChartValues import com.patrykandpatrick.vico.core.cartesian.values.MutableChartValues import com.patrykandpatrick.vico.core.common.DefaultDrawingModelInterpolator @@ -88,6 +88,8 @@ public open class CandlestickCartesianLayer( public companion object } + private val _markerTargets = mutableMapOf() + /** * Holds information on the [CandlestickCartesianLayer]’s horizontal dimensions. */ @@ -95,14 +97,14 @@ public open class CandlestickCartesianLayer( protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() - override val entryLocationMap: HashMap> = HashMap() + override val markerTargets: Map = _markerTargets override fun drawInternal( context: CartesianChartDrawContext, model: CandlestickCartesianLayerModel, ): Unit = with(context) { - entryLocationMap.clear() + _markerTargets.clear() drawChartInternal( chartValues = chartValues, model = model, @@ -127,7 +129,7 @@ public open class CandlestickCartesianLayer( var candle: Candle val minBodyHeight = minCandleBodyHeightDp.pixels - model.series.forEachInIndexed(range = chartValues.minX..chartValues.maxX) { index, entry, _ -> + model.series.forEachIn(chartValues.minX..chartValues.maxX) { entry, _ -> candle = candles.getCandle(entry, model.extraStore) val candleInfo = drawingModel?.entries?.get(entry.x) ?: entry.toCandleInfo(yRange) @@ -154,13 +156,7 @@ public open class CandlestickCartesianLayer( thicknessScale = zoom, ) ) { - updateMarkerLocationMap( - entry = entry, - entryX = bodyCenterX, - entryY = (bodyBottomY + bodyTopY).half, - body = candle.body, - entryIndex = index, - ) + updateMarkerTargets(entry, bodyCenterX, bodyBottomY, bodyTopY, bottomWickY, topWickY, candle) candle.body.drawVertical(this, bodyTopY, bodyBottomY, bodyCenterX, zoom) @@ -183,22 +179,34 @@ public open class CandlestickCartesianLayer( } } - private fun updateMarkerLocationMap( + protected open fun updateMarkerTargets( entry: CandlestickCartesianLayerModel.Entry, - entryX: Float, - entryY: Float, - entryIndex: Int, - body: LineComponent, + canvasX: Float, + bodyBottomCanvasY: Float, + bodyTopCanvasY: Float, + lowCanvasY: Float, + highCanvasY: Float, + candle: Candle, ) { - if (entryX in bounds.left..bounds.right) { - entryLocationMap.put( - x = entryX, - y = entryY.coerceIn(bounds.top, bounds.bottom), + if (canvasX <= bounds.left - 1 || canvasX >= bounds.right + 1) return + val limitedBodyBottomCanvasY = bodyBottomCanvasY.coerceIn(bounds.top, bounds.bottom) + val limitedBodyTopCanvasY = bodyTopCanvasY.coerceIn(bounds.top, bounds.bottom) + _markerTargets[entry.x] = + CandlestickCartesianLayerMarkerTarget( + x = entry.x, + canvasX = canvasX, entry = entry, - color = body.solidOrStrokeColor, - index = entryIndex, + openingCanvasY = + if (entry.absoluteChange == Change.Bullish) limitedBodyBottomCanvasY else limitedBodyTopCanvasY, + closingCanvasY = + if (entry.absoluteChange == Change.Bullish) limitedBodyTopCanvasY else limitedBodyBottomCanvasY, + lowCanvasY = lowCanvasY.coerceIn(bounds.top, bounds.bottom), + highCanvasY = highCanvasY.coerceIn(bounds.top, bounds.bottom), + openingColor = candle.body.solidOrStrokeColor, + closingColor = candle.body.solidOrStrokeColor, + lowColor = candle.bottomWick.solidOrStrokeColor, + highColor = candle.topWick.solidOrStrokeColor, ) - } } override fun updateChartValues( diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CartesianLayer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CartesianLayer.kt index 283eccad4..07f4174e0 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CartesianLayer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CartesianLayer.kt @@ -32,10 +32,8 @@ import com.patrykandpatrick.vico.core.common.dimension.BoundsAware * Visualizes data on a Cartesian plane. [CartesianLayer]s are combined and drawn by [CartesianChart]s. */ public interface CartesianLayer : BoundsAware, ChartInsetter { - /** - * Links _x_ values to [CartesianMarker.EntryModel]s. - */ - public val entryLocationMap: Map> + /** Links _x_ values to [CartesianMarker.Target]s. */ + public val markerTargets: Map /** * Draws the [CartesianLayer]. diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/ColumnCartesianLayer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/ColumnCartesianLayer.kt index 03aeb1bd4..f3ea7c45d 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/ColumnCartesianLayer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/ColumnCartesianLayer.kt @@ -25,10 +25,11 @@ import com.patrykandpatrick.vico.core.cartesian.draw.CartesianChartDrawContext import com.patrykandpatrick.vico.core.cartesian.formatter.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.formatter.DecimalFormatValueFormatter import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.cartesian.marker.put +import com.patrykandpatrick.vico.core.cartesian.marker.ColumnCartesianLayerMarkerTarget +import com.patrykandpatrick.vico.core.cartesian.marker.MutableColumnCartesianLayerMarkerTarget import com.patrykandpatrick.vico.core.cartesian.model.ColumnCartesianLayerDrawingModel import com.patrykandpatrick.vico.core.cartesian.model.ColumnCartesianLayerModel -import com.patrykandpatrick.vico.core.cartesian.model.forEachInIndexed +import com.patrykandpatrick.vico.core.cartesian.model.forEachIn import com.patrykandpatrick.vico.core.cartesian.values.ChartValues import com.patrykandpatrick.vico.core.cartesian.values.MutableChartValues import com.patrykandpatrick.vico.core.common.DefaultDrawingModelInterpolator @@ -133,6 +134,8 @@ public open class ColumnCartesianLayer( ) public constructor() : this(ColumnProvider.series()) + private val _markerTargets = mutableMapOf() + protected val stackInfo: MutableMap = mutableMapOf() /** @@ -158,14 +161,14 @@ public open class ColumnCartesianLayer( columnProvider = ColumnProvider.series(value) } - override val entryLocationMap: HashMap> = HashMap() + override val markerTargets: Map = _markerTargets override fun drawInternal( context: CartesianChartDrawContext, model: ColumnCartesianLayerModel, ): Unit = with(context) { - entryLocationMap.clear() + _markerTargets.clear() drawChartInternal( chartValues = chartValues, model = model, @@ -194,7 +197,7 @@ public open class ColumnCartesianLayer( drawingStart = getDrawingStart(index, model.series.size, mergeMode) - horizontalScroll - entryCollection.forEachInIndexed(chartValues.minX..chartValues.maxX) { entryIndex, entry, _ -> + entryCollection.forEachIn(chartValues.minX..chartValues.maxX) { entry, _ -> val columnInfo = drawingModel?.getOrNull(index)?.get(entry.x) height = (columnInfo?.height ?: (abs(entry.y) / yRange.length)) * bounds.height() @@ -236,7 +239,7 @@ public open class ColumnCartesianLayer( thicknessScale = zoom, ) ) { - updateMarkerLocationMap(entry, columnSignificantY, columnCenterX, column, entryIndex) + updateMarkerTargets(entry, columnCenterX, columnSignificantY, column) column.drawVertical(this, columnTop, columnBottom, columnCenterX, zoom, drawingModel?.opacity ?: 1f) } @@ -376,22 +379,21 @@ public open class ColumnCartesianLayer( } } - protected open fun updateMarkerLocationMap( + protected open fun updateMarkerTargets( entry: ColumnCartesianLayerModel.Entry, - columnTop: Float, - columnCenterX: Float, + canvasX: Float, + canvasY: Float, column: LineComponent, - index: Int, ) { - if (columnCenterX > bounds.left - 1 && columnCenterX < bounds.right + 1) { - entryLocationMap.put( - x = columnCenterX, - y = columnTop.coerceIn(bounds.top, bounds.bottom), - entry = entry, - color = column.solidOrStrokeColor, - index = index, + if (canvasX <= bounds.left - 1 || canvasX >= bounds.right + 1) return + _markerTargets + .getOrPut(entry.x) { MutableColumnCartesianLayerMarkerTarget(entry.x, canvasX) } + .columns += + ColumnCartesianLayerMarkerTarget.Column( + entry, + canvasY.coerceIn(bounds.top, bounds.bottom), + column.solidOrStrokeColor, ) - } } override fun updateChartValues( diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianLayer.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianLayer.kt index 58e35c2e0..791dcb4a3 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianLayer.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianLayer.kt @@ -33,10 +33,11 @@ import com.patrykandpatrick.vico.core.cartesian.insets.Insets import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer.LineSpec import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer.LineSpec.PointConnector import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.cartesian.marker.put +import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget +import com.patrykandpatrick.vico.core.cartesian.marker.MutableLineCartesianLayerMarkerTarget import com.patrykandpatrick.vico.core.cartesian.model.LineCartesianLayerDrawingModel import com.patrykandpatrick.vico.core.cartesian.model.LineCartesianLayerModel -import com.patrykandpatrick.vico.core.cartesian.model.forEachInIndexed +import com.patrykandpatrick.vico.core.cartesian.model.forEachIn import com.patrykandpatrick.vico.core.cartesian.values.ChartValues import com.patrykandpatrick.vico.core.cartesian.values.MutableChartValues import com.patrykandpatrick.vico.core.common.DefaultDrawingModelInterpolator @@ -251,6 +252,8 @@ public open class LineCartesianLayer( } } + private val _markerTargets = mutableMapOf() + /** * The [Path] used to draw the lines, each of which corresponds to a [LineSpec]. */ @@ -263,7 +266,7 @@ public open class LineCartesianLayer( protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() - override val entryLocationMap: HashMap> = HashMap() + override val markerTargets: Map = _markerTargets override fun drawInternal( context: CartesianChartDrawContext, @@ -282,7 +285,7 @@ public open class LineCartesianLayer( linePath.rewind() lineBackgroundPath.rewind() - val component = lines.getRepeating(entryListIndex) + val component = lines.getRepeating(entryListIndex).apply { setSplitY(zeroLineYFraction) } var prevX = bounds.getStart(isLtr = isLtr) var prevY = bounds.bottom @@ -291,11 +294,11 @@ public open class LineCartesianLayer( val drawingStart = bounds.getStart(isLtr = isLtr) + drawingStartAlignmentCorrection - horizontalScroll - forEachPointWithinBoundsIndexed( + forEachPointInBounds( series = entries, drawingStart = drawingStart, pointInfoMap = pointInfoMap, - ) { entryIndex, entry, x, y, _, _ -> + ) { entry, x, y, _, _ -> if (linePath.isEmpty) { linePath.moveTo(x, y) } else { @@ -312,17 +315,7 @@ public open class LineCartesianLayer( prevX = x prevY = y - if (x > bounds.left - 1 && x < bounds.right + 1) { - val coercedY = y.coerceIn(bounds.top, bounds.bottom) - component.setSplitY(zeroLineYFraction) - entryLocationMap.put( - x = x, - y = coercedY, - entry = entry, - color = component.shader.getColorAt(Point(x, coercedY), context, bounds), - index = entryIndex, - ) - } + updateMarkerTargets(entry, x, y, component) } if (component.hasLineBackgroundShader) { @@ -348,6 +341,24 @@ public open class LineCartesianLayer( } } + protected open fun CartesianChartDrawContext.updateMarkerTargets( + entry: LineCartesianLayerModel.Entry, + canvasX: Float, + canvasY: Float, + lineSpec: LineSpec, + ) { + if (canvasX <= bounds.left - 1 || canvasX >= bounds.right + 1) return + val limitedCanvasY = canvasY.coerceIn(bounds.top, bounds.bottom) + _markerTargets + .getOrPut(entry.x) { MutableLineCartesianLayerMarkerTarget(entry.x, canvasX) } + .points += + LineCartesianLayerMarkerTarget.Point( + entry, + limitedCanvasY, + lineSpec.shader.getColorAt(Point(canvasX, limitedCanvasY), this, bounds), + ) + } + /** * Draws a line’s points ([LineSpec.point]) and their corresponding data labels ([LineSpec.dataLabel]). */ @@ -359,11 +370,11 @@ public open class LineCartesianLayer( ) { if (lineSpec.point == null && lineSpec.dataLabel == null) return - forEachPointWithinBoundsIndexed( + forEachPointInBounds( series = series, drawingStart = drawingStart, pointInfoMap = pointInfoMap, - ) { _, chartEntry, x, y, previousX, nextX -> + ) { chartEntry, x, y, previousX, nextX -> if (lineSpec.point != null) lineSpec.drawPoint(context = this, x = x, y = y) @@ -459,17 +470,16 @@ public open class LineCartesianLayer( * Clears the temporary data saved during a single [drawInternal] run. */ protected fun resetTempData() { - entryLocationMap.clear() + _markerTargets.clear() linePath.rewind() lineBackgroundPath.rewind() } - protected open fun CartesianChartDrawContext.forEachPointWithinBoundsIndexed( + protected open fun CartesianChartDrawContext.forEachPointInBounds( series: List, drawingStart: Float, pointInfoMap: Map?, action: ( - index: Int, entry: LineCartesianLayerModel.Entry, x: Float, y: Float, @@ -497,7 +507,7 @@ public open class LineCartesianLayer( bounds.height() } - series.forEachInIndexed(range = minX..maxX, padding = 1) { index, entry, next -> + series.forEachIn(range = minX..maxX, padding = 1) { entry, next -> val previousX = x val immutableX = nextX ?: getDrawX(entry) val immutableNextX = next?.let(::getDrawX) @@ -506,9 +516,9 @@ public open class LineCartesianLayer( if (immutableNextX != null && (isLtr && immutableX < boundsStart || !isLtr && immutableX > boundsStart) && (isLtr && immutableNextX < boundsStart || !isLtr && immutableNextX > boundsStart) ) { - return@forEachInIndexed + return@forEachIn } - action(index, entry, immutableX, getDrawY(entry), previousX, nextX) + action(entry, immutableX, getDrawY(entry), previousX, nextX) if (isLtr && immutableX > boundsEnd || isLtr.not() && immutableX < boundsEnd) return } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CandlestickCartesianLayerMarkerTarget.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CandlestickCartesianLayerMarkerTarget.kt new file mode 100644 index 000000000..db37ab07d --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CandlestickCartesianLayerMarkerTarget.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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.cartesian.marker + +import com.patrykandpatrick.vico.core.cartesian.layer.CandlestickCartesianLayer +import com.patrykandpatrick.vico.core.cartesian.model.CandlestickCartesianLayerModel + +/** + * Houses information on a [CandlestickCartesianLayer] candle to be marked. + * + * @property entry the [CandlestickCartesianLayerModel.Entry]. + * @property openingCanvasY the pixel _y_ coordinate of the candle edge corresponding to + * [CandlestickCartesianLayerModel.Entry.opening]. + * @property closingCanvasY the pixel _y_ coordinate of the candle edge corresponding to + * [CandlestickCartesianLayerModel.Entry.closing]. + * @property lowCanvasY the pixel _y_ coordinate of the bottom wick’s bottom edge, which corresponds to + * [CandlestickCartesianLayerModel.Entry.low]. + * @property highCanvasY the pixel _y_ coordinate of the top wick’s top edge, which corresponds to + * [CandlestickCartesianLayerModel.Entry.high]. + * @property openingColor the color of [CandlestickCartesianLayer.Candle.body]. + * @property closingColor the color of [CandlestickCartesianLayer.Candle.body]. + * @property lowColor the color of [CandlestickCartesianLayer.Candle.bottomWick]. + * @property highColor the color of [CandlestickCartesianLayer.Candle.topWick]. + */ +public data class CandlestickCartesianLayerMarkerTarget( + override val x: Float, + override val canvasX: Float, + public val entry: CandlestickCartesianLayerModel.Entry, + public val openingCanvasY: Float, + public val closingCanvasY: Float, + public val lowCanvasY: Float, + public val highCanvasY: Float, + public val openingColor: Int, + public val closingColor: Int, + public val lowColor: Int, + public val highColor: Int, +) : CartesianMarker.Target diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarker.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarker.kt index 91c72dd2e..5d0e67097 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarker.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarker.kt @@ -16,54 +16,24 @@ package com.patrykandpatrick.vico.core.cartesian.marker -import android.graphics.RectF import com.patrykandpatrick.vico.core.cartesian.CartesianChart -import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext +import com.patrykandpatrick.vico.core.cartesian.draw.CartesianChartDrawContext import com.patrykandpatrick.vico.core.cartesian.insets.ChartInsetter -import com.patrykandpatrick.vico.core.cartesian.model.CartesianLayerModel -import com.patrykandpatrick.vico.core.cartesian.values.ChartValues -import com.patrykandpatrick.vico.core.common.Point -import com.patrykandpatrick.vico.core.common.extension.updateList -/** - * Highlights points on a chart and displays their corresponding values in a bubble. - */ +/** Marks [CartesianChart] objects. */ public interface CartesianMarker : ChartInsetter { - /** - * Draws the marker. - * @param context the [CartesianDrawContext] 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 chartValues the [CartesianChart]’s [ChartValues]. - */ + /** Draws the [CartesianMarker] for the specified [Target]s. */ public fun draw( - context: CartesianDrawContext, - bounds: RectF, - markedEntries: List, - chartValues: ChartValues, + context: CartesianChartDrawContext, + targets: List, ) - /** - * Contains information on a single chart entry to which a chart marker refers. - * @param location the coordinates of the indicator. - * @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: CartesianLayerModel.Entry, - public val color: Int, - public val index: Int, - ) -} + /** Houses information on an object to be marked. */ + public interface Target { + /** The _x_ value. */ + public val x: Float -internal fun HashMap>.put( - x: Float, - y: Float, - entry: CartesianLayerModel.Entry, - color: Int, - index: Int, -) { - updateList(x) { add(CartesianMarker.EntryModel(Point(x, y), entry, color, index)) } + /** The pixel _x_ coordinate. */ + public val canvasX: Float + } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerLabelFormatter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerValueFormatter.kt similarity index 66% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerLabelFormatter.kt rename to vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerValueFormatter.kt index ac7f1255a..48a62068e 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerLabelFormatter.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerValueFormatter.kt @@ -16,17 +16,13 @@ package com.patrykandpatrick.vico.core.cartesian.marker -import com.patrykandpatrick.vico.core.cartesian.values.ChartValues +import com.patrykandpatrick.vico.core.cartesian.draw.CartesianChartDrawContext -/** - * Formats marker labels. - */ -public fun interface CartesianMarkerLabelFormatter { - /** - * Creates a formatted label for the given list of marked entries. - */ - public fun getLabel( - markedEntries: List, - chartValues: ChartValues, +/** Formats [CartesianMarker] values for display. */ +public fun interface CartesianMarkerValueFormatter { + /** Returns a label for the given [CartesianMarker.Target]s. */ + public fun format( + context: CartesianChartDrawContext, + targets: List, ): CharSequence } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerVisibilityChangeListener.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerVisibilityChangeListener.kt deleted file mode 100644 index ef2e013d7..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerVisibilityChangeListener.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2024 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.cartesian.marker - -/** - * Allows for listening to [CartesianMarker] visibility changes. - */ -public interface CartesianMarkerVisibilityChangeListener { - /** - * Called when the linked [CartesianMarker] is shown. - * - * @param marker the linked [CartesianMarker], which has been shown. - * @param markerEntryModels a list of [CartesianMarker.EntryModel]s, which contain information about the marked chart - * entries. - */ - public fun onMarkerShown( - marker: CartesianMarker, - markerEntryModels: List, - ) - - /** - * Called when the linked [CartesianMarker] moves (that is, when there’s a change in which chart entries it highlights). - * - * @param marker the linked [CartesianMarker], which moved. - * @param markerEntryModels a list of [CartesianMarker.EntryModel]s, which contain information about the marked chart - * entries. - */ - public fun onMarkerMoved( - marker: CartesianMarker, - markerEntryModels: List, - ): Unit = Unit - - /** - * Called when the linked [CartesianMarker] is hidden. - * - * @param marker the linked [CartesianMarker], which has been hidden. - */ - public fun onMarkerHidden(marker: CartesianMarker) -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerVisibilityListener.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerVisibilityListener.kt new file mode 100644 index 000000000..af9534f99 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/CartesianMarkerVisibilityListener.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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.cartesian.marker + +/** Allows for listening to [CartesianMarker] visibility changes. */ +public interface CartesianMarkerVisibilityListener { + /** Called when the specified [CartesianMarker] is shown. */ + public fun onShown( + marker: CartesianMarker, + targets: List, + ) + + /** Called when the specified [CartesianMarker]’s _x_ value changes. */ + public fun onMoved( + marker: CartesianMarker, + targets: List, + ): Unit = Unit + + /** Called when the specified [CartesianMarker] is hidden. */ + public fun onHidden(marker: CartesianMarker) +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/ColumnCartesianLayerMarkerTarget.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/ColumnCartesianLayerMarkerTarget.kt new file mode 100644 index 000000000..6e11cb26a --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/ColumnCartesianLayerMarkerTarget.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 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.cartesian.marker + +import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer +import com.patrykandpatrick.vico.core.cartesian.model.ColumnCartesianLayerModel +import com.patrykandpatrick.vico.core.common.component.LineComponent + +/** Houses information on a set of [ColumnCartesianLayer] columns to be marked. */ +public interface ColumnCartesianLayerMarkerTarget : CartesianMarker.Target { + /** Holds [Column] instances, each of which houses information on a [ColumnCartesianLayer] column to be marked. */ + public val columns: List + + /** + * Houses information on a [ColumnCartesianLayer] column to be marked. + * + * @param entry the [ColumnCartesianLayerModel.Entry]. + * @param canvasY the pixel _y_ coordinate of the column’s top or bottom edge (depending on the sign of + * [ColumnCartesianLayerModel.Entry.y]). + * @param color the column [LineComponent]’s color. + */ + public data class Column(val entry: ColumnCartesianLayerModel.Entry, val canvasY: Float, val color: Int) +} + +internal data class MutableColumnCartesianLayerMarkerTarget( + override val x: Float, + override val canvasX: Float, + override val columns: MutableList = mutableListOf(), +) : ColumnCartesianLayerMarkerTarget diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/component/CartesianMarkerComponent.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarker.kt similarity index 51% rename from vico/core/src/main/java/com/patrykandpatrick/vico/core/common/component/CartesianMarkerComponent.kt rename to vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarker.kt index 70202ae64..4d3624c52 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/component/CartesianMarkerComponent.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarker.kt @@ -14,18 +14,20 @@ * limitations under the License. */ -package com.patrykandpatrick.vico.core.common.component +package com.patrykandpatrick.vico.core.cartesian.marker import android.graphics.RectF import com.patrykandpatrick.vico.core.cartesian.CartesianChart -import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext import com.patrykandpatrick.vico.core.cartesian.CartesianMeasureContext import com.patrykandpatrick.vico.core.cartesian.dimensions.HorizontalDimensions +import com.patrykandpatrick.vico.core.cartesian.draw.CartesianChartDrawContext import com.patrykandpatrick.vico.core.cartesian.insets.Insets -import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerLabelFormatter -import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarkerLabelFormatter -import com.patrykandpatrick.vico.core.cartesian.values.ChartValues +import com.patrykandpatrick.vico.core.common.Defaults +import com.patrykandpatrick.vico.core.common.component.Component +import com.patrykandpatrick.vico.core.common.component.LineComponent +import com.patrykandpatrick.vico.core.common.component.MarkerCorneredShape +import com.patrykandpatrick.vico.core.common.component.ShapeComponent +import com.patrykandpatrick.vico.core.common.component.TextComponent import com.patrykandpatrick.vico.core.common.extension.averageOf import com.patrykandpatrick.vico.core.common.extension.ceil import com.patrykandpatrick.vico.core.common.extension.doubled @@ -37,14 +39,20 @@ import com.patrykandpatrick.vico.core.common.position.VerticalPosition * The default [CartesianMarker] implementation. * * @param label the [TextComponent] for the label. + * @param valueFormatter formats the values. * @param labelPosition specifies the position of the label. * @param indicator drawn at the marked points. + * @param indicatorSizeDp the indicator size (in dp). + * @param setIndicatorColor sets the indicator color for each marked point. * @param guideline drawn vertically through the marked points. */ -public open class CartesianMarkerComponent( +public open class DefaultCartesianMarker( public val label: TextComponent, + public var valueFormatter: CartesianMarkerValueFormatter = DefaultCartesianMarkerValueFormatter(), public val labelPosition: LabelPosition = LabelPosition.Top, public val indicator: Component? = null, + public var indicatorSizeDp: Float = Defaults.MARKER_INDICATOR_SIZE, + public var setIndicatorColor: ((Int) -> Unit)? = null, public val guideline: LineComponent? = null, ) : CartesianMarker { protected val tempBounds: RectF = RectF() @@ -52,95 +60,90 @@ public open class CartesianMarkerComponent( protected val TextComponent.tickSizeDp: Float get() = ((background as? ShapeComponent)?.shape as? MarkerCorneredShape)?.tickSizeDp.orZero - /** - * The indicator size (in dp). - */ - public var indicatorSizeDp: Float = 0f - - /** - * An optional lambda function that allows for applying the color associated with a given data entry to a - * [Component]. - */ - public var onApplyEntryColor: ((entryColor: Int) -> Unit)? = null - - /** - * The [CartesianMarkerLabelFormatter] for this marker. - */ - public var labelFormatter: CartesianMarkerLabelFormatter = DefaultCartesianMarkerLabelFormatter() - - /** - * Creates a [CartesianMarkerComponent] with [LabelPosition.Top]. - * - * @param label the [TextComponent] for the label. - * @param indicator drawn at the marked points. - * @param guideline drawn vertically through the marked points. - */ - @Deprecated( - "Use the primary constructor, which has `labelPosition` and `minimumWidth` parameters and default " + - "values for `indicator` and `guideline`. (If you’re using named arguments, ignore this warning. " + - "The deprecated constructor is more specific, but the primary one matches and will be used once " + - "the deprecated one has been removed.)", - ) - public constructor(label: TextComponent, indicator: Component?, guideline: LineComponent?) : - this(label, LabelPosition.Top, indicator, guideline) - override fun draw( - context: CartesianDrawContext, - bounds: RectF, - markedEntries: List, - chartValues: ChartValues, + context: CartesianChartDrawContext, + targets: List, ): Unit = with(context) { - drawGuideline(context, bounds, markedEntries) + drawGuideline(targets) val halfIndicatorSize = indicatorSizeDp.half.pixels - markedEntries.forEachIndexed { _, model -> - onApplyEntryColor?.invoke(model.color) - indicator?.draw( - context, - model.location.x - halfIndicatorSize, - model.location.y - halfIndicatorSize, - model.location.x + halfIndicatorSize, - model.location.y + halfIndicatorSize, - ) + targets.forEach { target -> + when (target) { + is CandlestickCartesianLayerMarkerTarget -> { + drawIndicator(target.canvasX, target.openingCanvasY, target.openingColor, halfIndicatorSize) + drawIndicator(target.canvasX, target.closingCanvasY, target.closingColor, halfIndicatorSize) + drawIndicator(target.canvasX, target.lowCanvasY, target.lowColor, halfIndicatorSize) + drawIndicator(target.canvasX, target.highCanvasY, target.highColor, halfIndicatorSize) + } + is ColumnCartesianLayerMarkerTarget -> { + target.columns.forEach { column -> + drawIndicator(target.canvasX, column.canvasY, column.color, halfIndicatorSize) + } + } + is LineCartesianLayerMarkerTarget -> { + target.points.forEach { point -> + drawIndicator(target.canvasX, point.canvasY, point.color, halfIndicatorSize) + } + } + } } - drawLabel(context, bounds, markedEntries, chartValues) + drawLabel(context, targets) } + protected open fun CartesianChartDrawContext.drawIndicator( + x: Float, + y: Float, + color: Int, + halfIndicatorSize: Float, + ) { + if (indicator == null) return + setIndicatorColor?.invoke(color) + indicator.draw(this, x - halfIndicatorSize, y - halfIndicatorSize, x + halfIndicatorSize, y + halfIndicatorSize) + } + protected fun drawLabel( - context: CartesianDrawContext, - bounds: RectF, - markedEntries: List, - chartValues: ChartValues, + context: CartesianChartDrawContext, + targets: List, ): Unit = with(context) { - val text = labelFormatter.getLabel(markedEntries, chartValues) - val entryX = markedEntries.averageOf { it.location.x } + val text = valueFormatter.format(context, targets) + val targetX = targets.averageOf { it.canvasX } val labelBounds = label.getTextBounds( context = context, text = text, - width = bounds.width().toInt(), + width = chartBounds.width().toInt(), outRect = tempBounds, ) val halfOfTextWidth = labelBounds.width().half - val x = overrideXPositionToFit(entryX, bounds, halfOfTextWidth) - extraStore[MarkerCorneredShape.tickXKey] = entryX + val x = overrideXPositionToFit(targetX, chartBounds, halfOfTextWidth) + extraStore[MarkerCorneredShape.tickXKey] = targetX val tickPosition: MarkerCorneredShape.TickPosition val y: Float val verticalPosition: VerticalPosition if (labelPosition == LabelPosition.Top) { tickPosition = MarkerCorneredShape.TickPosition.Bottom - y = bounds.top - label.tickSizeDp.pixels + y = context.chartBounds.top - label.tickSizeDp.pixels verticalPosition = VerticalPosition.Top } else { - val topEntryY = markedEntries.minOf { it.location.y } + val topPointY = + targets.maxOf { target -> + when (target) { + is CandlestickCartesianLayerMarkerTarget -> target.highCanvasY + is ColumnCartesianLayerMarkerTarget -> + target.columns.maxOf(ColumnCartesianLayerMarkerTarget.Column::canvasY) + is LineCartesianLayerMarkerTarget -> + target.points.maxOf(LineCartesianLayerMarkerTarget.Point::canvasY) + else -> error("Unexpected `CartesianMarker.Target` implementation.") + } + } val flip = labelPosition == LabelPosition.AroundPoint && - topEntryY - labelBounds.height() - label.tickSizeDp.pixels < bounds.top + topPointY - labelBounds.height() - label.tickSizeDp.pixels < context.chartBounds.top tickPosition = if (flip) MarkerCorneredShape.TickPosition.Top else MarkerCorneredShape.TickPosition.Bottom - y = topEntryY + (if (flip) 1 else -1) * label.tickSizeDp.pixels + y = topPointY + (if (flip) 1 else -1) * label.tickSizeDp.pixels verticalPosition = if (flip) VerticalPosition.Bottom else VerticalPosition.Top } extraStore[MarkerCorneredShape.tickPositionKey] = tickPosition @@ -151,7 +154,7 @@ public open class CartesianMarkerComponent( textX = x, textY = y, verticalPosition = verticalPosition, - maxTextWidth = minOf(bounds.right - x, x - bounds.left).doubled.ceil.toInt(), + maxTextWidth = minOf(chartBounds.right - x, x - chartBounds.left).doubled.ceil.toInt(), ) } @@ -166,22 +169,11 @@ public open class CartesianMarkerComponent( else -> xPosition } - protected fun drawGuideline( - context: CartesianDrawContext, - bounds: RectF, - markedEntries: List, - ) { - markedEntries - .map { it.location.x } + protected fun CartesianChartDrawContext.drawGuideline(targets: List) { + targets + .map { it.canvasX } .toSet() - .forEach { x -> - guideline?.drawVertical( - context, - bounds.top, - bounds.bottom, - x, - ) - } + .forEach { x -> guideline?.drawVertical(this, chartBounds.top, chartBounds.bottom, x) } } override fun getInsets( @@ -193,9 +185,7 @@ public open class CartesianMarkerComponent( with(context) { outInsets.top = label.getHeight(context) + label.tickSizeDp.pixels } } - /** - * Specifies the position of a [CartesianMarkerComponent]’s label. - */ + /** Specifies the position of a [DefaultCartesianMarker]’s label. */ public enum class LabelPosition { /** * Positions the label at the top of the [CartesianChart]. Sufficient room is made. diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarkerLabelFormatter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarkerLabelFormatter.kt deleted file mode 100644 index b5fe5f0b0..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarkerLabelFormatter.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2024 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.cartesian.marker - -import android.text.Spannable -import android.text.style.ForegroundColorSpan -import com.patrykandpatrick.vico.core.cartesian.model.CandlestickCartesianLayerModel -import com.patrykandpatrick.vico.core.cartesian.model.CartesianLayerModel -import com.patrykandpatrick.vico.core.cartesian.model.ColumnCartesianLayerModel -import com.patrykandpatrick.vico.core.cartesian.model.LineCartesianLayerModel -import com.patrykandpatrick.vico.core.cartesian.values.ChartValues -import com.patrykandpatrick.vico.core.common.extension.appendCompat -import com.patrykandpatrick.vico.core.common.extension.sumOf -import com.patrykandpatrick.vico.core.common.extension.transformToSpannable -import java.text.DecimalFormat - -/** - * The default [CartesianMarkerLabelFormatter]. The _y_ values are formatted via [decimalFormat] and, if [colorCode] is `true`, - * color-coded. - */ -public open class DefaultCartesianMarkerLabelFormatter( - private val decimalFormat: DecimalFormat = defaultDecimalFormat, - private val colorCode: Boolean = true, -) : CartesianMarkerLabelFormatter { - /** The default [CartesianMarkerLabelFormatter]. [colorCode] specifies whether to color-code the _y_ values. */ - @Deprecated( - "Use the primary constructor, which has a `decimalFormat` parameter. (If you’re using named arguments, " + - "ignore this warning. The deprecated constructor is more specific, but the primary one matches and will " + - "be used once the deprecated one has been removed.)", - ) - public constructor(colorCode: Boolean) : this(defaultDecimalFormat, colorCode) - - override fun getLabel( - markedEntries: List, - chartValues: ChartValues, - ): CharSequence = - markedEntries.transformToSpannable( - prefix = - if (markedEntries.size > 1) decimalFormat.format(markedEntries.sumOf { it.entry.y }) + " (" else "", - postfix = if (markedEntries.size > 1) ")" else "", - separator = "; ", - ) { model -> - if (colorCode) { - appendCompat( - decimalFormat.format(model.entry.y), - ForegroundColorSpan(model.color), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, - ) - } else { - append(decimalFormat.format(model.entry.y)) - } - } - - protected val CartesianLayerModel.Entry.y: Float - get() = - when (this) { - is ColumnCartesianLayerModel.Entry -> y - is LineCartesianLayerModel.Entry -> y - is CandlestickCartesianLayerModel.Entry -> high - else -> throw IllegalArgumentException("Unexpected `CartesianLayerModel.Entry` implementation.") - } - - override fun equals(other: Any?): Boolean = - this === other || - other is DefaultCartesianMarkerLabelFormatter && decimalFormat == other.decimalFormat && - colorCode == other.colorCode - - override fun hashCode(): Int = 31 * decimalFormat.hashCode() + colorCode.hashCode() - - private companion object { - val defaultDecimalFormat = DecimalFormat("#.##;−#.##") - } -} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarkerValueFormatter.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarkerValueFormatter.kt new file mode 100644 index 000000000..6b0695b64 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/DefaultCartesianMarkerValueFormatter.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2024 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.cartesian.marker + +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan +import com.patrykandpatrick.vico.core.cartesian.draw.CartesianChartDrawContext +import com.patrykandpatrick.vico.core.cartesian.model.CartesianLayerModel +import com.patrykandpatrick.vico.core.common.extension.appendCompat +import com.patrykandpatrick.vico.core.common.extension.sumOf +import java.text.DecimalFormat + +/** + * The default [CartesianMarkerValueFormatter]. The labels produced include the [CartesianLayerModel.Entry] + * instances’ _y_ values, which are formatted via [decimalFormat] and, if [colorCode] is `true`, color-coded. + */ +public open class DefaultCartesianMarkerValueFormatter( + private val decimalFormat: DecimalFormat = DecimalFormat("#.##;−#.##"), + private val colorCode: Boolean = true, +) : CartesianMarkerValueFormatter { + protected open fun SpannableStringBuilder.append( + y: Float, + color: Int? = null, + ) { + if (colorCode && color != null) { + appendCompat(decimalFormat.format(y), ForegroundColorSpan(color), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + append(decimalFormat.format(y)) + } + } + + protected open fun SpannableStringBuilder.append( + target: CartesianMarker.Target, + shorten: Boolean, + ) { + when (target) { + is CandlestickCartesianLayerMarkerTarget -> { + if (shorten) { + append(target.entry.closing, target.closingColor) + } else { + append("O ") + append(target.entry.opening, target.openingColor) + append(", C ") + append(target.entry.closing, target.closingColor) + append(", L ") + append(target.entry.low, target.lowColor) + append(", H ") + append(target.entry.high, target.highColor) + } + } + is ColumnCartesianLayerMarkerTarget -> { + val includeSum = target.columns.size > 1 + if (includeSum) { + append(target.columns.sumOf { it.entry.y }) + append(" (") + } + target.columns.forEachIndexed { index, column -> + append(column.entry.y, column.color) + if (index != target.columns.lastIndex) append(", ") + } + if (includeSum) append(")") + } + is LineCartesianLayerMarkerTarget -> { + target.points.forEachIndexed { index, point -> + append(point.entry.y, point.color) + if (index != target.points.lastIndex) append(", ") + } + } + else -> throw IllegalArgumentException("Unexpected `CartesianMarker.Target` implementation.") + } + } + + override fun format( + context: CartesianChartDrawContext, + targets: List, + ): CharSequence = + SpannableStringBuilder().apply { + targets.forEachIndexed { index, target -> + append(target = target, shorten = targets.size > 1) + if (index != targets.lastIndex) append(", ") + } + } + + override fun equals(other: Any?): Boolean = + this === other || + other is DefaultCartesianMarkerValueFormatter && decimalFormat == other.decimalFormat && + colorCode == other.colorCode + + override fun hashCode(): Int = 31 * decimalFormat.hashCode() + colorCode.hashCode() +} diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/LineCartesianLayerMarkerTarget.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/LineCartesianLayerMarkerTarget.kt new file mode 100644 index 000000000..ca78d6552 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/marker/LineCartesianLayerMarkerTarget.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.cartesian.marker + +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.cartesian.model.LineCartesianLayerModel + +/** Houses information on a set of [LineCartesianLayer] points to be marked. */ +public interface LineCartesianLayerMarkerTarget : CartesianMarker.Target { + /** Holds [Point] instances, each of which houses information on a marked point. */ + public val points: List + + /** + * Houses information on a [LineCartesianLayer] point to be marked. + * + * @param entry the [LineCartesianLayerModel.Entry]. + * @param canvasY the point’s pixel _y_ coordinate. + * @param color the [LineCartesianLayer.LineSpec]’s color for the point. + */ + public data class Point(val entry: LineCartesianLayerModel.Entry, val canvasY: Float, val color: Int) +} + +internal data class MutableLineCartesianLayerMarkerTarget( + override val x: Float, + override val canvasX: Float, + override val points: MutableList = mutableListOf(), +) : LineCartesianLayerMarkerTarget diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/model/CartesianLayerModel.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/model/CartesianLayerModel.kt index ba1f815e0..33a5d48a6 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/model/CartesianLayerModel.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/model/CartesianLayerModel.kt @@ -111,10 +111,10 @@ internal fun List.getXDeltaGcd(): Float { ?: 1f } -internal inline fun List.forEachInIndexed( +internal inline fun List.forEachIn( range: ClosedFloatingPointRange, padding: Int = 0, - action: (Int, T, T?) -> Unit, + action: (T, T?) -> Unit, ) { var start = 0 var end = 0 @@ -127,5 +127,5 @@ internal inline fun List.forEachInIndexed( } start = (start - padding).coerceAtLeast(0) end = (end + padding).coerceAtMost(lastIndex) - (start..end).forEach { action(it, this[it], getOrNull(it + 1)) } + (start..end).forEach { action(this[it], getOrNull(it + 1)) } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/Defaults.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/Defaults.kt index 32fd721a5..0f9bc7978 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/Defaults.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/Defaults.kt @@ -56,7 +56,7 @@ public object Defaults { public const val LABEL_LINE_COUNT: Int = 1 public const val LINE_COMPONENT_THICKNESS_DP: Float = 1f public const val LINE_SPEC_THICKNESS_DP: Float = 2f - public const val MARKER_INDICATOR_SIZE: Float = 36f + public const val MARKER_INDICATOR_SIZE: Float = 16f public const val MARKER_HORIZONTAL_PADDING: Float = 8f public const val MARKER_VERTICAL_PADDING: Float = 4f public const val MARKER_TICK_SIZE: Float = 6f diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/extension/CollectionExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/extension/CollectionExtensions.kt index e030f217f..c54ebb29c 100644 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/extension/CollectionExtensions.kt +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/extension/CollectionExtensions.kt @@ -18,7 +18,6 @@ package com.patrykandpatrick.vico.core.common.extension import androidx.annotation.RestrictTo import com.patrykandpatrick.vico.core.common.ERR_REPEATING_COLLECTION_EMPTY -import kotlin.math.abs @JvmName("copyDouble") internal fun List>.copy() = map { it.toList() } @@ -38,20 +37,6 @@ internal fun MutableMap.setAll(other: Map) { other.forEach { (key, value) -> set(key, value) } } -internal fun Collection.findClosestPositiveValue(value: Float): Float? { - if (isEmpty()) return null - var closestValue: Float? = null - forEach { checkedValue -> - closestValue = - when { - closestValue == null -> checkedValue - abs(closestValue!! - value) > abs(checkedValue - value) -> checkedValue - else -> closestValue - } - } - return closestValue -} - internal fun Collection.averageOf(selector: (T) -> Float): Float = fold(0f) { sum, element -> sum + selector(element) diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/extension/MapExtensions.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/extension/MapExtensions.kt deleted file mode 100644 index eef5b29b3..000000000 --- a/vico/core/src/main/java/com/patrykandpatrick/vico/core/common/extension/MapExtensions.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2024 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.common.extension - -import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.common.Point -import java.util.TreeMap - -internal fun Map>.getClosestMarkerEntryModel( - touchPoint: Point, -): List? = keys.findClosestPositiveValue(touchPoint.x)?.let(::get) - -internal fun Map>.getEntryModel( - xValue: Float, -): List? = - values - .mapNotNull { entries -> entries.takeIf { it.firstOrNull()?.entry?.x == xValue } } - .flatten() - .takeIf { it.isNotEmpty() } - -internal fun TreeMap>.updateAll(other: Map>) { - other.forEach { (key, value) -> - put(key, get(key)?.apply { addAll(value) } ?: mutableListOf(value)) - } -} - -internal inline fun HashMap>.updateList( - key: K, - initialCapacity: Int = 0, - block: MutableList.() -> Unit, -) { - block(getOrPut(key) { ArrayList(initialCapacity) }) -} diff --git a/vico/views/src/main/java/com/patrykandpatrick/vico/views/cartesian/CartesianChartView.kt b/vico/views/src/main/java/com/patrykandpatrick/vico/views/cartesian/CartesianChartView.kt index 08045d5ba..63866f165 100644 --- a/vico/views/src/main/java/com/patrykandpatrick/vico/views/cartesian/CartesianChartView.kt +++ b/vico/views/src/main/java/com/patrykandpatrick/vico/views/cartesian/CartesianChartView.kt @@ -33,7 +33,7 @@ import com.patrykandpatrick.vico.core.cartesian.dimensions.MutableHorizontalDime import com.patrykandpatrick.vico.core.cartesian.draw.cartesianChartDrawContext import com.patrykandpatrick.vico.core.cartesian.draw.drawMarker import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityChangeListener +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener import com.patrykandpatrick.vico.core.cartesian.model.CartesianChartModel import com.patrykandpatrick.vico.core.cartesian.model.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.values.ChartValues @@ -94,12 +94,10 @@ public open class CartesianChartView private var markerTouchPoint: Point? = null - private var wasMarkerVisible: Boolean = false + private var previousMarkerX: Float? = null private var scrollDirectionResolved = false - private var lastMarkerEntryModels = emptyList() - private var horizontalDimensions = MutableHorizontalDimensions() private val themeHandler: ThemeHandler = ThemeHandler(context, attrs) @@ -217,7 +215,7 @@ public open class CartesianChartView /** * Allows for listening to [marker] visibility changes. */ - public var markerVisibilityChangeListener: CartesianMarkerVisibilityChangeListener? = null + public var markerVisibilityListener: CartesianMarkerVisibilityListener? = null init { if (isInEditMode && attrs != null) { @@ -376,16 +374,14 @@ public open class CartesianChartView chart.draw(chartDrawContext, model) marker?.also { marker -> - chartDrawContext.drawMarker( - marker = marker, - markerTouchPoint = markerTouchPoint, - chart = chart, - markerVisibilityChangeListener = markerVisibilityChangeListener, - wasMarkerVisible = wasMarkerVisible, - setWasMarkerVisible = { wasMarkerVisible = it }, - lastMarkerEntryModels = lastMarkerEntryModels, - onMarkerEntryModelsChange = { lastMarkerEntryModels = it }, - ) + previousMarkerX = + chartDrawContext.drawMarker( + marker, + markerTouchPoint, + chart, + markerVisibilityListener, + previousMarkerX, + ) } measureContext.reset() }