From 908fbfbfd3d62bf3ce8579b2219fe1cc4dea45c7 Mon Sep 17 00:00:00 2001 From: Patrick Michalik <120058021+patrickmichalik@users.noreply.github.com> Date: Sun, 31 Mar 2024 12:57:25 +0200 Subject: [PATCH] Make `CartesianMarker`-related updates (WIP) --- .../vico/sample/showcase/Marker.kt | 28 ++-- .../vico/sample/showcase/charts/Chart3.kt | 6 +- .../compose/cartesian/CartesianChartHost.kt | 6 +- .../marker/DefaultCartesianMarker.kt | 51 ++++++ .../common/component/ComponentExtensions.kt | 10 -- .../common/component/MarkerComponent.kt | 77 --------- .../vico/core/cartesian/CartesianChart.kt | 19 +-- .../draw/ChartDrawContextExtensions.kt | 38 ++--- .../layer/CandlestickCartesianLayer.kt | 52 +++--- .../layer/CandlestickCartesianMarkerEntry.kt | 47 ++++++ .../core/cartesian/layer/CartesianLayer.kt | 6 +- .../cartesian/layer/ColumnCartesianLayer.kt | 24 ++- .../layer/ColumnCartesianMarkerEntry.kt | 50 ++++++ .../cartesian/layer/LineCartesianLayer.kt | 30 ++-- .../layer/LineCartesianMarkerEntry.kt | 50 ++++++ .../core/cartesian/marker/CartesianMarker.kt | 54 ++----- .../marker/CartesianMarkerLabelFormatter.kt | 14 +- ...CartesianMarkerVisibilityChangeListener.kt | 30 +--- .../marker/DefaultCartesianMarker.kt} | 152 ++++++++---------- .../DefaultCartesianMarkerLabelFormatter.kt | 98 +++++++---- .../cartesian/model/CartesianLayerModel.kt | 6 +- .../core/common/extension/MapExtensions.kt | 47 ------ .../views/cartesian/CartesianChartView.kt | 6 +- 23 files changed, 452 insertions(+), 449 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/layer/CandlestickCartesianMarkerEntry.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/ColumnCartesianMarkerEntry.kt create mode 100644 vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianMarkerEntry.kt rename vico/core/src/main/java/com/patrykandpatrick/vico/core/{common/component/CartesianMarkerComponent.kt => cartesian/marker/DefaultCartesianMarker.kt} (57%) 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..18c03cfda 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 @@ -34,7 +34,7 @@ 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 +42,7 @@ import com.patrykandpatrick.vico.core.common.shape.Shapes @Composable internal fun rememberMarker( - labelPosition: CartesianMarkerComponent.LabelPosition = CartesianMarkerComponent.LabelPosition.Top, + labelPosition: DefaultCartesianMarker.LabelPosition = DefaultCartesianMarker.LabelPosition.Top, ): CartesianMarker { val labelBackgroundShape = Shapes.markerCorneredShape(Corner.FullyRounded) val labelBackground = @@ -82,18 +82,18 @@ internal fun rememberMarker( 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) - } - } - } - + object : DefaultCartesianMarker( + label = label, + labelPosition = labelPosition, + indicator = indicator, + indicatorSizeDp = 36f, + setIndicatorColor = { color -> + indicatorRearComponent.color = color.copyColor(alpha = .15f) + indicatorCenterComponent.color = color + indicatorCenterComponent.setShadow(radius = 12f, color = color) + }, + guideline = guideline, + ) { override fun getInsets( context: CartesianMeasureContext, outInsets: Insets, 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 82297fe25..336ed5743 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 @@ -210,7 +210,7 @@ internal fun CartesianChartHostImpl( with(LocalContext.current) { ::spToPx }, chartValues, ) - val lastMarkerEntryModels = remember { mutableStateOf(emptyList()) } + val previousMarkerEntries = remember { mutableStateOf(emptyList()) } val elevationOverlayColor = vicoTheme.elevationOverlayColor.toArgb() val (wasMarkerVisible, setWasMarkerVisible) = remember { mutableStateOf(false) } @@ -287,8 +287,8 @@ internal fun CartesianChartHostImpl( markerVisibilityChangeListener = markerVisibilityChangeListener, wasMarkerVisible = wasMarkerVisible, setWasMarkerVisible = setWasMarkerVisible, - lastMarkerEntryModels = lastMarkerEntryModels.value, - onMarkerEntryModelsChange = lastMarkerEntryModels.component2(), + previousEntries = previousMarkerEntries.value, + onEntriesChanged = previousMarkerEntries.component2(), ) } 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..26e40fa18 --- /dev/null +++ b/vico/compose/src/main/java/com/patrykandpatrick/vico/compose/cartesian/marker/DefaultCartesianMarker.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.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.CartesianMarkerLabelFormatter +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarkerLabelFormatter +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, + labelFormatter: CartesianMarkerLabelFormatter = remember { DefaultCartesianMarkerLabelFormatter() }, + labelPosition: DefaultCartesianMarker.LabelPosition = DefaultCartesianMarker.LabelPosition.Top, + indicator: Component? = null, + indicatorSize: Dp = 0.dp, + setIndicatorColor: ((Int) -> Unit)? = null, + guideline: LineComponent? = null, +): DefaultCartesianMarker = + remember(label, labelFormatter, labelPosition, indicator, indicatorSize, setIndicatorColor, guideline) { + DefaultCartesianMarker( + label, + labelFormatter, + 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..a9ef0c5b4 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 _markerEntries = 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.markerEntries.forEach { _markerEntries.getOrPut(it.key) { mutableListOf() }.add(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 pixel _x_ values to [CartesianMarker.Entry] instances. */ + public val markerEntries: Map> = _markerEntries /** * The start axis. @@ -174,7 +170,7 @@ public open class CartesianChart( bounds: RectF, marker: CartesianMarker?, ) { - entryLocationMap.clear() + _markerEntries.clear() model.forEachWithLayer( horizontalDimensionUpdateModelAndLayerConsumer.apply { this.context = context @@ -208,9 +204,8 @@ public open class CartesianChart( decorations.forEach { it.onDrawAboveChart(context, bounds) } } persistentMarkers.forEach { (x, marker) -> - entryLocationMap.getEntryModel(x)?.let { model -> - marker.draw(context, bounds, model, context.chartValues) - } + val entries = markerEntries.values.filter { entries -> entries.first().x == x }.flatten() + if (entries.isNotEmpty()) marker.draw(context, entries) } 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..1c4465ad8 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 @@ -26,7 +26,7 @@ import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityChangeListener import com.patrykandpatrick.vico.core.common.DrawContext import com.patrykandpatrick.vico.core.common.Point -import com.patrykandpatrick.vico.core.common.extension.getClosestMarkerEntryModel +import com.patrykandpatrick.vico.core.common.extension.findClosestPositiveValue /** @suppress */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -75,34 +75,21 @@ public fun CartesianChartDrawContext.drawMarker( markerVisibilityChangeListener: CartesianMarkerVisibilityChangeListener?, wasMarkerVisible: Boolean, setWasMarkerVisible: (Boolean) -> Unit, - lastMarkerEntryModels: List, - onMarkerEntryModelsChange: (List) -> Unit, + previousEntries: List, + onEntriesChanged: (List) -> Unit, ) { markerTouchPoint - ?.let(chart.entryLocationMap::getClosestMarkerEntryModel) - ?.let { markerEntryModels -> - marker.draw( - context = this, - bounds = chart.bounds, - markedEntries = markerEntryModels, - chartValues = chartValues, - ) + ?.let { chart.markerEntries[chart.markerEntries.keys.findClosestPositiveValue(it.x)] } + ?.let { entries -> + marker.draw(this, entries) if (wasMarkerVisible.not()) { - markerVisibilityChangeListener?.onMarkerShown( - marker = marker, - markerEntryModels = markerEntryModels, - ) + markerVisibilityChangeListener?.onMarkerShown(marker, entries) setWasMarkerVisible(true) } - val didMarkerMove = lastMarkerEntryModels.hasMoved(markerEntryModels) + val didMarkerMove = previousEntries.hasMoved(entries) if (wasMarkerVisible && didMarkerMove) { - onMarkerEntryModelsChange(markerEntryModels) - if (lastMarkerEntryModels.isNotEmpty()) { - markerVisibilityChangeListener?.onMarkerMoved( - marker = marker, - markerEntryModels = markerEntryModels, - ) - } + onEntriesChanged(entries) + if (previousEntries.isNotEmpty()) markerVisibilityChangeListener?.onMarkerMoved(marker, entries) } } ?: marker .takeIf { wasMarkerVisible } @@ -112,7 +99,6 @@ public fun CartesianChartDrawContext.drawMarker( } } -private fun List.xPosition(): Float? = firstOrNull()?.entry?.x +private fun List.xPosition() = firstOrNull()?.x -private fun List.hasMoved(other: List): Boolean = - xPosition() != other.xPosition() +private fun List.hasMoved(other: List) = 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 94ac0ab44..0c49c378b 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 @@ -26,11 +26,10 @@ import com.patrykandpatrick.vico.core.cartesian.dimensions.MutableHorizontalDime import com.patrykandpatrick.vico.core.cartesian.draw.CartesianChartDrawContext import com.patrykandpatrick.vico.core.cartesian.layer.CandlestickCartesianLayer.Candle 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 @@ -87,6 +86,8 @@ public open class CandlestickCartesianLayer( public companion object } + private val _markerEntries = mutableMapOf() + /** * Holds information on the [CandlestickCartesianLayer]’s horizontal dimensions. */ @@ -94,14 +95,14 @@ public open class CandlestickCartesianLayer( protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() - override val entryLocationMap: HashMap> = HashMap() + override val markerEntries: Map = _markerEntries override fun drawInternal( context: CartesianChartDrawContext, model: CandlestickCartesianLayerModel, ): Unit = with(context) { - entryLocationMap.clear() + _markerEntries.clear() drawChartInternal( chartValues = chartValues, model = model, @@ -126,7 +127,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) @@ -153,13 +154,7 @@ public open class CandlestickCartesianLayer( thicknessScale = zoom, ) ) { - updateMarkerLocationMap( - entry = entry, - entryX = bodyCenterX, - entryY = (bodyBottomY + bodyTopY).half, - body = candle.body, - entryIndex = index, - ) + updateMarkerLocationMap(entry, bodyCenterX, bodyBottomY, bodyTopY, bottomWickY, topWickY, candle) candle.body.drawVertical(this, bodyTopY, bodyBottomY, bodyCenterX, zoom) @@ -184,19 +179,28 @@ public open class CandlestickCartesianLayer( private fun updateMarkerLocationMap( entry: CandlestickCartesianLayerModel.Entry, - entryX: Float, - entryY: Float, - entryIndex: Int, - body: LineComponent, + canvasX: Float, + bodyBottomY: Float, + bodyTopY: Float, + bottomWickY: Float, + topWickY: Float, + candle: Candle, ) { - if (entryX in bounds.left..bounds.right) { - entryLocationMap.put( - x = entryX, - y = entryY.coerceIn(bounds.top, bounds.bottom), - entry = entry, - color = body.solidOrStrokeColor, - index = entryIndex, - ) + if (canvasX in bounds.left..bounds.right) { + _markerEntries[canvasX] = + CandlestickCartesianMarkerEntry( + x = entry.x, + canvasX = canvasX, + entry = entry, + openingY = if (entry.absoluteChange == Change.Bullish) bodyBottomY else bodyTopY, + closingY = if (entry.absoluteChange == Change.Bullish) bodyTopY else bodyBottomY, + lowY = bottomWickY, + highY = topWickY, + openingColor = candle.body.solidOrStrokeColor, + closingColor = candle.body.solidOrStrokeColor, + lowColor = candle.bottomWick.solidOrStrokeColor, + highColor = candle.topWick.solidOrStrokeColor, + ) } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CandlestickCartesianMarkerEntry.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CandlestickCartesianMarkerEntry.kt new file mode 100644 index 000000000..9cd7f2074 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/CandlestickCartesianMarkerEntry.kt @@ -0,0 +1,47 @@ +/* + * 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.layer + +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.model.CandlestickCartesianLayerModel + +/** + * Houses information on a marked [CandlestickCartesianLayerModel.Entry]. + * + * @property entry the [CandlestickCartesianLayerModel.Entry]. + * @property openingY the pixel _y_ value corresponding to [CandlestickCartesianLayerModel.Entry.opening]. + * @property closingY the pixel _y_ value corresponding to [CandlestickCartesianLayerModel.Entry.closing]. + * @property lowY the pixel _y_ value corresponding to [CandlestickCartesianLayerModel.Entry.low]. + * @property highY the pixel _y_ value corresponding to [CandlestickCartesianLayerModel.Entry.high]. + * @property openingColor the color corresponding to [CandlestickCartesianLayerModel.Entry.opening]. + * @property closingColor the color corresponding to [CandlestickCartesianLayerModel.Entry.closing]. + * @property lowColor the color corresponding to [CandlestickCartesianLayerModel.Entry.low]. + * @property highColor the color corresponding to [CandlestickCartesianLayerModel.Entry.high]. + */ +public data class CandlestickCartesianMarkerEntry( + override val x: Float, + override val canvasX: Float, + public val entry: CandlestickCartesianLayerModel.Entry, + public val openingY: Float, + public val closingY: Float, + public val lowY: Float, + public val highY: Float, + public val openingColor: Int, + public val closingColor: Int, + public val lowColor: Int, + public val highColor: Int, +) : CartesianMarker.Entry 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..6e8cb84f6 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 pixel _x_ values to [CartesianMarker.Entry] instances. */ + public val markerEntries: 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..bf26282eb 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,9 @@ 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.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 +132,8 @@ public open class ColumnCartesianLayer( ) public constructor() : this(ColumnProvider.series()) + private val _markerEntries = mutableMapOf() + protected val stackInfo: MutableMap = mutableMapOf() /** @@ -158,14 +159,14 @@ public open class ColumnCartesianLayer( columnProvider = ColumnProvider.series(value) } - override val entryLocationMap: HashMap> = HashMap() + override val markerEntries: Map = _markerEntries override fun drawInternal( context: CartesianChartDrawContext, model: ColumnCartesianLayerModel, ): Unit = with(context) { - entryLocationMap.clear() + _markerEntries.clear() drawChartInternal( chartValues = chartValues, model = model, @@ -194,7 +195,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 +237,7 @@ public open class ColumnCartesianLayer( thicknessScale = zoom, ) ) { - updateMarkerLocationMap(entry, columnSignificantY, columnCenterX, column, entryIndex) + updateMarkerLocationMap(entry, columnSignificantY, columnCenterX, column) column.drawVertical(this, columnTop, columnBottom, columnCenterX, zoom, drawingModel?.opacity ?: 1f) } @@ -381,16 +382,11 @@ public open class ColumnCartesianLayer( columnTop: Float, columnCenterX: 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, - ) + _markerEntries + .getOrPut(columnCenterX) { MutableColumnCartesianMarkerEntry(entry.x, columnCenterX) } + .add(entry, columnTop.coerceIn(bounds.top, bounds.bottom), column.solidOrStrokeColor) } } diff --git a/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/ColumnCartesianMarkerEntry.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/ColumnCartesianMarkerEntry.kt new file mode 100644 index 000000000..638d8e559 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/ColumnCartesianMarkerEntry.kt @@ -0,0 +1,50 @@ +/* + * 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.layer + +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.model.ColumnCartesianLayerModel + +/** Houses information on a set of marked [ColumnCartesianLayerModel.Entry] instances. */ +public interface ColumnCartesianMarkerEntry : CartesianMarker.Entry { + /** The [ColumnCartesianLayerModel.Entry] instances. */ + public val entries: List + + /** The pixel _y_ values corresponding to the [ColumnCartesianLayerModel.Entry] instances. */ + public val y: List + + /** The colors corresponding to the [ColumnCartesianLayerModel.Entry] instances. */ + public val colors: List +} + +internal data class MutableColumnCartesianMarkerEntry( + override val x: Float, + override val canvasX: Float, + override val entries: MutableList = mutableListOf(), + override val y: MutableList = mutableListOf(), + override val colors: MutableList = mutableListOf(), +) : ColumnCartesianMarkerEntry { + fun add( + entry: ColumnCartesianLayerModel.Entry, + y: Float, + color: Int, + ) { + entries += entry + this.y += y + colors += color + } +} 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..ec682fb2a 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,9 @@ 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.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 +250,8 @@ public open class LineCartesianLayer( } } + private val _markerEntries = mutableMapOf() + /** * The [Path] used to draw the lines, each of which corresponds to a [LineSpec]. */ @@ -263,7 +264,7 @@ public open class LineCartesianLayer( protected val drawingModelKey: ExtraStore.Key = ExtraStore.Key() - override val entryLocationMap: HashMap> = HashMap() + override val markerEntries: Map = _markerEntries override fun drawInternal( context: CartesianChartDrawContext, @@ -295,7 +296,7 @@ public open class LineCartesianLayer( series = entries, drawingStart = drawingStart, pointInfoMap = pointInfoMap, - ) { entryIndex, entry, x, y, _, _ -> + ) { entry, x, y, _, _ -> if (linePath.isEmpty) { linePath.moveTo(x, y) } else { @@ -315,13 +316,9 @@ public open class LineCartesianLayer( 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, - ) + _markerEntries + .getOrPut(x) { MutableLineCartesianMarkerEntry(entry.x, x) } + .add(entry, coercedY, component.shader.getColorAt(Point(x, coercedY), context, bounds)) } } @@ -363,7 +360,7 @@ public open class LineCartesianLayer( 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,7 +456,7 @@ public open class LineCartesianLayer( * Clears the temporary data saved during a single [drawInternal] run. */ protected fun resetTempData() { - entryLocationMap.clear() + _markerEntries.clear() linePath.rewind() lineBackgroundPath.rewind() } @@ -469,7 +466,6 @@ public open class LineCartesianLayer( drawingStart: Float, pointInfoMap: Map?, action: ( - index: Int, entry: LineCartesianLayerModel.Entry, x: Float, y: Float, @@ -497,7 +493,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 +502,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/layer/LineCartesianMarkerEntry.kt b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianMarkerEntry.kt new file mode 100644 index 000000000..05c74f3f9 --- /dev/null +++ b/vico/core/src/main/java/com/patrykandpatrick/vico/core/cartesian/layer/LineCartesianMarkerEntry.kt @@ -0,0 +1,50 @@ +/* + * 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.layer + +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.model.LineCartesianLayerModel + +/** Houses information on a set of marked [LineCartesianLayerModel.Entry] instances. */ +public interface LineCartesianMarkerEntry : CartesianMarker.Entry { + /** The [LineCartesianLayerModel.Entry] instances. */ + public val entries: List + + /** The pixel _y_ values corresponding to the [LineCartesianLayerModel.Entry] instances. */ + public val y: List + + /** The colors corresponding to the [LineCartesianLayerModel.Entry] instances. */ + public val colors: List +} + +internal data class MutableLineCartesianMarkerEntry( + override val x: Float, + override val canvasX: Float, + override val entries: MutableList = mutableListOf(), + override val y: MutableList = mutableListOf(), + override val colors: MutableList = mutableListOf(), +) : LineCartesianMarkerEntry { + fun add( + entry: LineCartesianLayerModel.Entry, + y: Float, + color: Int, + ) { + entries += entry + this.y += y + colors += color + } +} 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..417eef127 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. - */ +/** Highlights [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]. */ public fun draw( - context: CartesianDrawContext, - bounds: RectF, - markedEntries: List, - chartValues: ChartValues, + context: CartesianChartDrawContext, + entries: 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 a marked object. */ + public interface Entry { + /** The object’s _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 object’s 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/CartesianMarkerLabelFormatter.kt index ac7f1255a..d6ed796c4 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/CartesianMarkerLabelFormatter.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. - */ +/** Formats [CartesianMarker] labels. */ public fun interface CartesianMarkerLabelFormatter { - /** - * Creates a formatted label for the given list of marked entries. - */ + /** Returns a formatted label for the given [CartesianMarker.Entry] list. */ public fun getLabel( - markedEntries: List, - chartValues: ChartValues, + context: CartesianChartDrawContext, + entries: 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 index ef2e013d7..5fc3fbbf4 100644 --- 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 @@ -16,38 +16,20 @@ package com.patrykandpatrick.vico.core.cartesian.marker -/** - * Allows for listening to [CartesianMarker] visibility changes. - */ +/** 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. - */ + /** Called when the [CartesianMarker] is shown. */ public fun onMarkerShown( marker: CartesianMarker, - markerEntryModels: List, + entries: 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. - */ + /** Called when the [CartesianMarker]’s _x_ value changes. */ public fun onMarkerMoved( marker: CartesianMarker, - markerEntryModels: List, + entries: List, ): Unit = Unit - /** - * Called when the linked [CartesianMarker] is hidden. - * - * @param marker the linked [CartesianMarker], which has been hidden. - */ + /** Called when the [CartesianMarker] is hidden. */ public fun onMarkerHidden(marker: CartesianMarker) } 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 57% 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..ded773cfc 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,22 @@ * 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.cartesian.layer.CandlestickCartesianMarkerEntry +import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianMarkerEntry +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianMarkerEntry +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 +41,20 @@ import com.patrykandpatrick.vico.core.common.position.VerticalPosition * The default [CartesianMarker] implementation. * * @param label the [TextComponent] for the label. + * @param labelFormatter formats the label. * @param labelPosition specifies the position of the label. * @param indicator drawn at the marked points. + * @param indicatorSizeDp the indicator size (in dp). + * @param setIndicatorColor updates the indicator color. * @param guideline drawn vertically through the marked points. */ -public open class CartesianMarkerComponent( +public open class DefaultCartesianMarker( public val label: TextComponent, + public var labelFormatter: CartesianMarkerLabelFormatter = DefaultCartesianMarkerLabelFormatter(), public val labelPosition: LabelPosition = LabelPosition.Top, public val indicator: Component? = null, + public var indicatorSizeDp: Float = 0f, + public var setIndicatorColor: ((Int) -> Unit)? = null, public val guideline: LineComponent? = null, ) : CartesianMarker { protected val tempBounds: RectF = RectF() @@ -52,92 +62,79 @@ 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, + entries: List, ): Unit = with(context) { - drawGuideline(context, bounds, markedEntries) + drawGuideline(entries) 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, - ) + entries.forEach { entry -> + when (entry) { + is ColumnCartesianMarkerEntry -> { + entry.y.forEachIndexed { index, y -> + drawIndicator(entry.colors[index], entry.canvasX, y, halfIndicatorSize) + } + } + is LineCartesianMarkerEntry -> { + entry.y.forEachIndexed { index, y -> + drawIndicator(entry.colors[index], entry.canvasX, y, halfIndicatorSize) + } + } + } } - drawLabel(context, bounds, markedEntries, chartValues) + drawLabel(context, entries) } + protected fun CartesianChartDrawContext.drawIndicator( + color: Int, + x: Float, + y: Float, + 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, + entries: List, ): Unit = with(context) { - val text = labelFormatter.getLabel(markedEntries, chartValues) - val entryX = markedEntries.averageOf { it.location.x } + val text = labelFormatter.getLabel(context, entries) + val entryX = entries.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) + val x = overrideXPositionToFit(entryX, chartBounds, halfOfTextWidth) extraStore[MarkerCorneredShape.tickXKey] = entryX 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 topEntryY = + entries.maxOf { entry -> + when (entry) { + is CandlestickCartesianMarkerEntry -> entry.highY + is ColumnCartesianMarkerEntry -> entry.y.max() + is LineCartesianMarkerEntry -> entry.y.max() + else -> error("Unexpected `CartesianMarker.Entry` implementation.") + } + } val flip = labelPosition == LabelPosition.AroundPoint && - topEntryY - labelBounds.height() - label.tickSizeDp.pixels < bounds.top + topEntryY - 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 @@ -151,7 +148,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 +163,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(entries: List) { + entries + .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 +179,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 index b5fe5f0b0..a671cc98e 100644 --- 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 @@ -17,20 +17,19 @@ 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.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.cartesian.draw.CartesianChartDrawContext +import com.patrykandpatrick.vico.core.cartesian.layer.CandlestickCartesianMarkerEntry +import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianMarkerEntry +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianMarkerEntry 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. + * 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, @@ -44,35 +43,68 @@ public open class DefaultCartesianMarkerLabelFormatter( ) 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 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 fun SpannableStringBuilder.append( + entry: CartesianMarker.Entry, + entryCount: Int, + ) { + when (entry) { + is CandlestickCartesianMarkerEntry -> { + if (entryCount == 1) { + append("O ") + append(entry.entry.opening, entry.openingColor) + append(", C ") + append(entry.entry.closing, entry.closingColor) + append(", L ") + append(entry.entry.low, entry.lowColor) + append(", H ") + append(entry.entry.high, entry.highColor) + } else { + append(entry.entry.closing, entry.closingColor) + } + } + is ColumnCartesianMarkerEntry -> { + val includeSum = entry.entries.size > 1 + if (includeSum) { + append(entry.entries.sumOf { it.y }) + append(" (") + } + entry.entries.forEachIndexed { index, layerModelEntry -> + append(layerModelEntry.y, entry.colors[index]) + if (index != entry.entries.lastIndex) append(", ") + } + if (includeSum) append(")") } + is LineCartesianMarkerEntry -> { + entry.entries.forEachIndexed { index, layerModelEntry -> + append(layerModelEntry.y, entry.colors[index]) + if (index != entry.entries.lastIndex) append(", ") + } + } + else -> throw IllegalArgumentException("Unexpected `CartesianLayerModel.Entry` implementation.") } + } - 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 getLabel( + context: CartesianChartDrawContext, + entries: List, + ): CharSequence = + SpannableStringBuilder().apply { + entries.forEachIndexed { index, entry -> + append(entry, entries.size) + if (index != entries.lastIndex) append(", ") } + } override fun equals(other: Any?): Boolean = this === other || 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/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..3b934e60d 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 @@ -98,7 +98,7 @@ public open class CartesianChartView private var scrollDirectionResolved = false - private var lastMarkerEntryModels = emptyList() + private var previousMarkerEntries = emptyList() private var horizontalDimensions = MutableHorizontalDimensions() @@ -383,8 +383,8 @@ public open class CartesianChartView markerVisibilityChangeListener = markerVisibilityChangeListener, wasMarkerVisible = wasMarkerVisible, setWasMarkerVisible = { wasMarkerVisible = it }, - lastMarkerEntryModels = lastMarkerEntryModels, - onMarkerEntryModelsChange = { lastMarkerEntryModels = it }, + previousEntries = previousMarkerEntries, + onEntriesChanged = { previousMarkerEntries = it }, ) } measureContext.reset()