Skip to content

Commit

Permalink
Add dashed lines to LineCartesianLayer
Browse files Browse the repository at this point in the history
Co-authored-by: Patrick Michalik <[email protected]>
  • Loading branch information
2 people authored and Gowsky committed Dec 19, 2024
1 parent 1d03d3f commit 5172052
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.patrykandpatrick.vico.R
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart
import com.patrykandpatrick.vico.compose.cartesian.layer.dashed
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLine
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
Expand All @@ -38,6 +39,7 @@ import com.patrykandpatrick.vico.compose.common.component.rememberTextComponent
import com.patrykandpatrick.vico.compose.common.component.shapeComponent
import com.patrykandpatrick.vico.compose.common.dimensions
import com.patrykandpatrick.vico.compose.common.fill
import com.patrykandpatrick.vico.compose.common.shader.horizontalGradient
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
Expand All @@ -46,6 +48,7 @@ import com.patrykandpatrick.vico.core.cartesian.data.lineSeries
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker
import com.patrykandpatrick.vico.core.common.data.ExtraStore
import com.patrykandpatrick.vico.core.common.shader.DynamicShader
import com.patrykandpatrick.vico.core.common.shape.CorneredShape
import com.patrykandpatrick.vico.databinding.Chart3Binding
import com.patrykandpatrick.vico.sample.showcase.Defaults
Expand Down Expand Up @@ -88,7 +91,15 @@ private fun ComposeChart3(modelProducer: CartesianChartModelProducer, modifier:
lineProvider =
LineCartesianLayer.LineProvider.series(
LineCartesianLayer.rememberLine(
remember { LineCartesianLayer.LineFill.single(fill(lineColor)) }
fill =
LineCartesianLayer.LineFill.single(
fill(
DynamicShader.horizontalGradient(
arrayOf(Color(LINE_COLOR_1), Color(LINE_COLOR_2))
)
)
),
stroke = LineCartesianLayer.LineStroke.dashed(),
)
),
rangeProvider = rangeProvider,
Expand All @@ -102,7 +113,7 @@ private fun ComposeChart3(modelProducer: CartesianChartModelProducer, modifier:
color = Color.Black,
margins = dimensions(end = 4.dp),
padding = dimensions(8.dp, 2.dp),
background = rememberShapeComponent(fill(lineColor), CorneredShape.Pill),
background = rememberShapeComponent(fill(Color(LINE_COLOR_1)), CorneredShape.Pill),
),
title = stringResource(R.string.y_axis),
),
Expand Down Expand Up @@ -136,12 +147,26 @@ private fun ViewChart3(modelProducer: CartesianChartModelProducer, modifier: Mod
AndroidViewBinding(Chart3Binding::inflate, modifier) {
chartView.modelProducer = modelProducer
val chart = requireNotNull(chartView.chart)
val lineLayer = (chart.layers[0] as LineCartesianLayer).copy(rangeProvider = rangeProvider)
val lineLayer =
(chart.layers[0] as LineCartesianLayer).copy(
rangeProvider = rangeProvider,
lineProvider =
LineCartesianLayer.LineProvider.series(
LineCartesianLayer.Line(
fill =
LineCartesianLayer.LineFill.single(
fill(DynamicShader.horizontalGradient(LINE_COLOR_1, LINE_COLOR_2))
),
stroke = LineCartesianLayer.LineStroke.Dashed(),
)
),
)
chartView.chart = chart.copy(lineLayer, marker = marker)
}
}

private val lineColor = Color(0xffffbb00)
private const val LINE_COLOR_1 = 0xffffbb00.toInt()
private const val LINE_COLOR_2 = 0xffff4888.toInt()
private val bottomAxisLabelBackgroundColor = Color(0xff9db591)
private val rangeProvider =
object : CartesianLayerRangeProvider {
Expand Down
2 changes: 2 additions & 0 deletions sample/src/main/res/values/chart_3_styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@

<style name="Chart3Line1Style">
<item name="android:color">@color/chart_3_color_1</item>
<item name="dashLength">4dp</item>
<item name="gapLength">8dp</item>
</style>

<style name="Chart3StartAxisTitleBackgroundStyle" parent="Chart3AxisTitleBackgroundStyle">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,8 @@ public fun LineCartesianLayer.Companion.rememberLine(
vicoTheme.lineCartesianLayerColors.first().let { color ->
remember(color) { LineCartesianLayer.LineFill.single(fill(color)) }
},
thickness: Dp = Defaults.LINE_SPEC_THICKNESS_DP.dp,
stroke: LineCartesianLayer.LineStroke = LineCartesianLayer.LineStroke.continuous(),
areaFill: LineCartesianLayer.AreaFill? = remember(fill) { fill.getDefaultAreaFill() },
cap: StrokeCap = StrokeCap.Round,
pointProvider: LineCartesianLayer.PointProvider? = null,
pointConnector: LineCartesianLayer.PointConnector = remember {
LineCartesianLayer.PointConnector.cubic()
Expand All @@ -109,9 +108,8 @@ public fun LineCartesianLayer.Companion.rememberLine(
): LineCartesianLayer.Line =
remember(
fill,
thickness,
stroke,
areaFill,
cap,
pointProvider,
pointConnector,
dataLabel,
Expand All @@ -121,9 +119,8 @@ public fun LineCartesianLayer.Companion.rememberLine(
) {
LineCartesianLayer.Line(
fill,
thickness.value,
stroke,
areaFill,
cap.paintCap,
pointProvider,
pointConnector,
dataLabel,
Expand All @@ -150,3 +147,22 @@ private val StrokeCap.paintCap: Paint.Cap
"Not `StrokeCap.Butt`, `StrokeCap.Round`, or `StrokeCap.Square`."
)
}

/** Creates a [LineCartesianLayer.LineStroke.Continuous] instance. */
public fun LineCartesianLayer.LineStroke.Companion.continuous(
thickness: Dp = Defaults.LINE_SPEC_THICKNESS_DP.dp
): LineCartesianLayer.LineStroke = LineCartesianLayer.LineStroke.Continuous(thickness.value)

/** Creates a [LineCartesianLayer.LineStroke.Dashed] instance. */
public fun LineCartesianLayer.LineStroke.Companion.dashed(
thickness: Dp = Defaults.LINE_SPEC_THICKNESS_DP.dp,
cap: StrokeCap = StrokeCap.Round,
dashLength: Dp = Defaults.LINE_PATTERN_DASHED_LENGTH.dp,
gapLength: Dp = Defaults.LINE_PATTERN_DASHED_GAP.dp,
): LineCartesianLayer.LineStroke.Dashed =
LineCartesianLayer.LineStroke.Dashed(
thickness.value,
cap.paintCap,
dashLength.value,
gapLength.value,
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ package com.patrykandpatrick.vico.core.cartesian.layer

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PorterDuff
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.patrykandpatrick.vico.core.cartesian.CartesianDrawingContext
Expand Down Expand Up @@ -91,23 +91,21 @@ protected constructor(
/**
* Defines the appearance of a line in a line chart.
*
* @param cap the stroke cap.
* @property fill draws the line fill.
* @property thicknessDp the line thickness (in dp).
* @property stroke determines the line stroke to apply to the line.
* @property areaFill draws the area fill.
* @property pointProvider provides the [Point]s.
* @property pointConnector connects the line’s points, thus defining its shape.
* @property dataLabel used for the data labels.
* @property dataLabelVerticalPosition the vertical position of the data labels relative to the
* points.
* @property dataLabelValueFormatter formats the data-label values.
* @property dataLabelRotationDegrees the data-label rotation (in degrees).
* @property pointConnector connects the line’s points, thus defining its shape.
*/
public open class Line(
protected val fill: LineFill,
public val thicknessDp: Float = Defaults.LINE_SPEC_THICKNESS_DP,
public val stroke: LineStroke = LineStroke.Continuous(),
protected val areaFill: AreaFill? = fill.getDefaultAreaFill(),
cap: Paint.Cap = Paint.Cap.ROUND,
public val pointProvider: PointProvider? = null,
public val pointConnector: PointConnector = PointConnector.cubic(),
public val dataLabel: TextComponent? = null,
Expand All @@ -116,10 +114,7 @@ protected constructor(
public val dataLabelRotationDegrees: Float = 0f,
) {
protected val linePaint: Paint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeCap = cap
}
Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE }

/** Draws the line. */
public fun draw(
Expand All @@ -129,9 +124,8 @@ protected constructor(
verticalAxisPosition: Axis.Position.Vertical?,
) {
with(context) {
val thickness = thicknessDp.pixels
linePaint.strokeWidth = thickness
val halfThickness = thickness.half
stroke.apply(this, linePaint)
val halfThickness = stroke.thicknessDp.pixels.half
areaFill?.draw(context, path, halfThickness, verticalAxisPosition)
fillCanvas.drawPath(path, linePaint)
withOtherCanvas(fillCanvas) { fill.draw(context, halfThickness, verticalAxisPosition) }
Expand Down Expand Up @@ -166,6 +160,60 @@ protected constructor(
}
}

/** Defines the style of a [LineCartesianLayer] line’s stroke. */
@Immutable
public sealed interface LineStroke {

/** The stroke thickness (in dp). */
public val thicknessDp: Float

/** Applies the stroke style to [paint]. */
public fun apply(context: CartesianDrawingContext, paint: Paint)

/**
* Produces a continuous stroke.
*
* @property cap the stroke cap.
*/
public data class Continuous(
override val thicknessDp: Float = Defaults.LINE_SPEC_THICKNESS_DP,
public val cap: Paint.Cap = Paint.Cap.ROUND,
) : LineStroke {
override fun apply(context: CartesianDrawingContext, paint: Paint) {
with(context) {
paint.strokeWidth = thicknessDp.pixels
paint.strokeCap = cap
paint.pathEffect = null
}
}
}

/**
* Produces a dashed stroke.
*
* @property cap the stroke cap.
* @property dashLengthDp the dash length (in dp).
* @property gapLengthDp the gap length (in dp).
*/
public data class Dashed(
public override val thicknessDp: Float = Defaults.LINE_SPEC_THICKNESS_DP,
public val cap: Paint.Cap = Paint.Cap.ROUND,
public val dashLengthDp: Float = Defaults.LINE_PATTERN_DASHED_LENGTH,
public val gapLengthDp: Float = Defaults.LINE_PATTERN_DASHED_GAP,
) : LineStroke {
override fun apply(context: CartesianDrawingContext, paint: Paint) {
with(context) {
paint.strokeWidth = thicknessDp.pixels
paint.strokeCap = cap
paint.pathEffect =
DashPathEffect(floatArrayOf(dashLengthDp.pixels, gapLengthDp.pixels), 0f)
}
}
}

public companion object
}

/** Draws a [LineCartesianLayer] line’s area fill. */
public interface AreaFill {
/** Draws the area fill. */
Expand Down Expand Up @@ -354,7 +402,12 @@ protected constructor(
val drawingStart =
layerBounds.getStart(isLtr = isLtr) + drawingStartAlignmentCorrection - scroll

forEachPointInBounds(series, drawingStart, pointInfoMap) { _, x, y, _, _ ->
forEachPointInBounds(
series = series,
drawingStart = drawingStart,
pointInfoMap = pointInfoMap,
drawFullLineLength = line.stroke is LineStroke.Dashed,
) { _, x, y, _, _ ->
if (linePath.isEmpty) {
linePath.moveTo(x, y)
} else {
Expand Down Expand Up @@ -428,7 +481,7 @@ protected constructor(
chartEntry.x == ranges.maxX && horizontalDimensions.endPadding > 0
}
?.let { textComponent ->
val distanceFromLine = max(line.thicknessDp, point?.sizeDp.orZero).half.pixels
val distanceFromLine = max(line.stroke.thicknessDp, point?.sizeDp.orZero).half.pixels

val text = line.dataLabelValueFormatter.format(this, chartEntry.y, verticalAxisPosition)
val maxWidth = getMaxDataLabelWidth(chartEntry, x, previousX, nextX)
Expand Down Expand Up @@ -500,6 +553,7 @@ protected constructor(
series: List<LineCartesianLayerModel.Entry>,
drawingStart: Float,
pointInfoMap: Map<Double, LineCartesianLayerDrawingModel.PointInfo>?,
drawFullLineLength: Boolean = false,
action:
(
entry: LineCartesianLayerModel.Entry, x: Float, y: Float, previousX: Float?, nextX: Float?,
Expand Down Expand Up @@ -535,7 +589,8 @@ protected constructor(
x = immutableX
nextX = immutableNextX
if (
immutableNextX != null &&
drawFullLineLength.not() &&
immutableNextX != null &&
(isLtr && immutableX < boundsStart || !isLtr && immutableX > boundsStart) &&
(isLtr && immutableNextX < boundsStart || !isLtr && immutableNextX > boundsStart)
) {
Expand Down Expand Up @@ -595,7 +650,10 @@ protected constructor(
(0..<model.series.size)
.mapNotNull { lineProvider.getLine(it, model.extraStore) }
.maxOf {
max(it.thicknessDp, it.pointProvider?.getLargestPoint(model.extraStore)?.sizeDp.orZero)
max(
it.stroke.thicknessDp,
it.pointProvider?.getLargestPoint(model.extraStore)?.sizeDp.orZero,
)
}
.half
.pixels
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public object Defaults {
public const val HOLLOW_CANDLE_STROKE_THICKNESS_DP: Float = 1f
public const val CANDLE_SPACING_DP: Float = 4f
public const val LINE_CURVATURE: Float = 0.5f
public const val LINE_PATTERN_DASHED_LENGTH: Float = 4f
public const val LINE_PATTERN_DASHED_GAP: Float = 8f
public const val DASH_LENGTH: Float = 4f
public const val DASH_GAP: Float = 2f
public const val FADING_EDGE_VISIBILITY_THRESHOLD_DP: Float = 16f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.patrykandpatrick.vico.views.common.theme
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.Paint
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
import com.patrykandpatrick.vico.core.common.DefaultAlpha
import com.patrykandpatrick.vico.core.common.Defaults
Expand Down Expand Up @@ -160,15 +161,19 @@ internal fun TypedArray.getLine(context: Context, defaultColor: Int): LineCartes
),
)

val dashLength = getRawDimension(context, R.styleable.LineStyle_dashLength, 0f)
val dashGap = getRawDimension(context, R.styleable.LineStyle_gapLength, 0f)
val thicknessDp =
getRawDimension(context, R.styleable.LineStyle_thickness, Defaults.LINE_SPEC_THICKNESS_DP)
val cap = Paint.Cap.entries[getInteger(R.styleable.LineStyle_android_strokeLineCap, 1)]

return LineCartesianLayer.Line(
fill =
if (positiveLineColor != negativeLineColor) {
LineCartesianLayer.LineFill.double(Fill(positiveLineColor), Fill(negativeLineColor))
} else {
LineCartesianLayer.LineFill.single(Fill(positiveLineColor))
},
thicknessDp =
getRawDimension(context, R.styleable.LineStyle_thickness, Defaults.LINE_SPEC_THICKNESS_DP),
areaFill =
LineCartesianLayer.AreaFill.double(
Fill(DynamicShader.verticalGradient(positiveGradientTopColor, positiveGradientBottomColor)),
Expand Down Expand Up @@ -203,5 +208,11 @@ internal fun TypedArray.getLine(context: Context, defaultColor: Int): LineCartes
dataLabelVerticalPosition =
VerticalPosition.entries[getInteger(R.styleable.LineStyle_dataLabelVerticalPosition, 0)],
dataLabelRotationDegrees = getFloat(R.styleable.LineStyle_dataLabelRotationDegrees, 0f),
stroke =
if (dashLength > 0f && dashGap > 0f) {
LineCartesianLayer.LineStroke.Dashed(thicknessDp, cap, dashLength, dashGap)
} else {
LineCartesianLayer.LineStroke.Continuous(thicknessDp, cap)
},
)
}
Loading

0 comments on commit 5172052

Please sign in to comment.