diff --git a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/ChartContent.kt b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/ChartContent.kt index 41db76e2..305b170c 100644 --- a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/ChartContent.kt +++ b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/ChartContent.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas -import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* @@ -119,9 +118,7 @@ internal fun ChartContent( } } else { - if (linesParameters.size >= 2) { - clickedPoints.clear() - } + // Tooltips enabled for multi-line charts linesParameters.forEach { line -> if (line.lineType == LineType.DEFAULT_LINE) { diff --git a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/CircleWithRectAndText.kt b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/CircleWithRectAndText.kt deleted file mode 100644 index 646271f4..00000000 --- a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/CircleWithRectAndText.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.aay.compose.lineChart.components - -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector1D -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.TextMeasurer -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.drawText -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.aay.compose.lineChart.model.LineParameters -import com.aay.compose.utils.formatToThousandsMillionsBillions - -@OptIn(ExperimentalTextApi::class) -internal fun DrawScope.circleWithRectAndText( - animatedProgress: Animatable, - textMeasure: TextMeasurer, - info: Double, - stroke: Stroke, - line: LineParameters, - x: Dp, - y: Double, -) { - chartCircle(x.toPx(), y.toFloat(), line.lineColor, animatedProgress, stroke) - chartRectangleWithText(x, y, line.lineColor, textMeasure, info) -} - - -@OptIn(ExperimentalTextApi::class) -private fun DrawScope.chartRectangleWithText( - x: Dp, y: Double, color: Color, textMeasurer: TextMeasurer, infoText: Double, -) { - val rectSize = Size(50.dp.toPx(), 30.dp.toPx()) - val rectTopLeft = Offset( - x.toPx() - rectSize.width / 1.5.toFloat(), - y.toFloat() - rectSize.height * 1.5.toFloat() - ) - val rectBounds = Rect(rectTopLeft, rectSize) - val text = "Value:${infoText.toFloat().formatToThousandsMillionsBillions()}" - - val textStyle = TextStyle(fontSize = 8.sp, color = Color.Black) - - val textLayoutResult = textMeasurer.measure( - text = AnnotatedString(text), - style = textStyle - ) - - val textOffset = Offset( - rectTopLeft.x + rectSize.width / 2 - textLayoutResult.size.width / 2, - rectTopLeft.y + rectSize.height / 4 + textLayoutResult.size.height / 2 - ) - - drawRoundRect( - color = color, - topLeft = rectBounds.topLeft, - size = rectBounds.size, - cornerRadius = CornerRadius(16.dp.toPx()), - style = Stroke(width = 1.dp.toPx()) - ) - - drawContext.canvas.nativeCanvas.apply { - drawText( - textMeasurer = textMeasurer, - text = text, - style = textStyle, - topLeft = textOffset - ) - } - -} \ No newline at end of file diff --git a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/DefaultLines.kt b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/DefaultLines.kt index e625b302..5c6813f8 100644 --- a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/DefaultLines.kt +++ b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/DefaultLines.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ExperimentalTextApi @@ -31,6 +30,7 @@ internal fun DrawScope.drawDefaultLineWithShadow( clickedPoints: MutableList>, textMeasure: TextMeasurer, xRegionWidth: Dp, + xAxisData: List = emptyList(), ) { val strokePathOfDefaultLine = drawLineAsDefault( @@ -41,7 +41,8 @@ internal fun DrawScope.drawDefaultLineWithShadow( spacingY = spacingY, clickedPoints = clickedPoints, textMeasure = textMeasure, - xRegionWidth = xRegionWidth + xRegionWidth = xRegionWidth, + xAxisData = xAxisData ) if (line.lineShadow) { @@ -71,6 +72,7 @@ private fun DrawScope.drawLineAsDefault( clickedPoints: MutableList>, textMeasure: TextMeasurer, xRegionWidth: Dp, + xAxisData: List = emptyList(), ) = Path().apply { val height = size.height.toDp() drawPathLineWrapper( @@ -100,14 +102,15 @@ private fun DrawScope.drawLineAsDefault( lastClickedPoint = null } else { lastClickedPoint = Pair(startXPoint.toPx(), startYPoint.toFloat()) - circleWithRectAndText( + drawTooltipWithMarker( x = startXPoint, y = startYPoint, - textMeasure = textMeasure, - info = info, - stroke = Stroke(width = 2.dp.toPx()), + textMeasurer = textMeasure, + xIndex = index, + yValue = info, line = lineParameter, - animatedProgress = animatedProgress + animatedProgress = animatedProgress, + xAxisData = xAxisData ) } } diff --git a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/QuadraticLines.kt b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/QuadraticLines.kt index b7816106..f66f1ed5 100644 --- a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/QuadraticLines.kt +++ b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/QuadraticLines.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ExperimentalTextApi @@ -32,6 +31,7 @@ internal fun DrawScope.drawQuarticLineWithShadow( clickedPoints: MutableList>, xRegionWidth: Dp, textMeasurer: TextMeasurer, + xAxisData: List = emptyList(), ) { val strokePathOfQuadraticLine = drawLineAsQuadratic( line = line, @@ -42,7 +42,8 @@ internal fun DrawScope.drawQuarticLineWithShadow( specialChart = specialChart, clickedPoints = clickedPoints, textMeasurer = textMeasurer, - xRegionWidth = xRegionWidth + xRegionWidth = xRegionWidth, + xAxisData = xAxisData ) if (line.lineShadow && !specialChart) { @@ -73,7 +74,8 @@ fun DrawScope.drawLineAsQuadratic( specialChart: Boolean, clickedPoints: MutableList>, textMeasurer: TextMeasurer, - xRegionWidth: Dp + xRegionWidth: Dp, + xAxisData: List = emptyList(), ) = Path().apply { var medX: Float val height = size.height.toDp() @@ -121,14 +123,15 @@ fun DrawScope.drawLineAsQuadratic( lastClickedPoint = null } else { lastClickedPoint = Pair(xFirstPoint.toPx(), yFirstPoint.toFloat()) - circleWithRectAndText( + drawTooltipWithMarker( x = xFirstPoint, y = yFirstPoint, - textMeasure = textMeasurer, - info = info, - stroke = Stroke(width = 2.dp.toPx()), + textMeasurer = textMeasurer, + xIndex = index, + yValue = info, line = line, - animatedProgress = animatedProgress + animatedProgress = animatedProgress, + xAxisData = xAxisData ) } diff --git a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/TooltipWithMarker.kt b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/TooltipWithMarker.kt new file mode 100644 index 00000000..64a3f8ee --- /dev/null +++ b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/components/TooltipWithMarker.kt @@ -0,0 +1,333 @@ +package com.aay.compose.lineChart.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.aay.compose.lineChart.model.LineParameters +import com.aay.compose.lineChart.model.MarkerStyle +import com.aay.compose.lineChart.model.TooltipConfig +import com.aay.compose.lineChart.model.TooltipContent +import com.aay.compose.lineChart.model.TooltipCornerRadii +import com.aay.compose.lineChart.model.TooltipPosition +import com.aay.compose.lineChart.model.TooltipSize + +/** + * Draws a tooltip with a circular marker at the clicked point on the line chart. + * + * This is the main entry point for rendering tooltips when a user clicks on a data point. + * It draws both the marker circle and the tooltip box with customizable content and styling. + * + * @param animatedProgress Animation state for the chart + * @param textMeasurer Text measurer for calculating text dimensions + * @param xIndex Index of the clicked point in the data array + * @param yValue Y-axis value of the clicked point + * @param line Line parameters containing tooltip configuration + * @param x X-coordinate of the point + * @param y Y-coordinate of the point + * @param xAxisData List of X-axis labels for displaying X values in tooltip + */ +@OptIn(ExperimentalTextApi::class) +internal fun DrawScope.drawTooltipWithMarker( + animatedProgress: Animatable, + textMeasurer: TextMeasurer, + xIndex: Int, + yValue: Double, + line: LineParameters, + x: Dp, + y: Double, + xAxisData: List = emptyList(), +) { + val config = line.tooltipConfig + + if (!config.enabled) return + + // Draw marker circle at the point + drawMarkerCircle( + x = x.toPx(), + y = y.toFloat(), + color = line.lineColor, + animatedProgress = animatedProgress, + markerStyle = config.markerStyle + ) + + // Draw tooltip box with content + drawTooltipBox( + x = x, + y = y, + color = line.lineColor, + textMeasurer = textMeasurer, + xIndex = xIndex, + yValue = yValue, + xAxisData = xAxisData, + config = config + ) +} + +/** + * Draws the circular marker at the clicked point. + * + * @param x X-coordinate of the marker center + * @param y Y-coordinate of the marker center + * @param color Color of the marker + * @param animatedProgress Animation state + * @param markerStyle Style of the marker (Stroke, Solid, or None) + */ +private fun DrawScope.drawMarkerCircle( + x: Float, + y: Float, + color: Color, + animatedProgress: Animatable, + markerStyle: MarkerStyle, +) { + when (markerStyle) { + is MarkerStyle.Stroke -> { + chartCircle( + x = x, + y = y, + color = color, + animatedProgress = animatedProgress, + stroke = Stroke(width = markerStyle.strokeWidth.toPx()) + ) + } + + is MarkerStyle.Solid -> { + chartCircle( + x = x, + y = y, + color = color, + animatedProgress = animatedProgress, + stroke = null // null means filled circle + ) + } + + is MarkerStyle.None -> { + // Don't draw any marker + } + } +} + +/** + * Draws the tooltip box with formatted content. + * + * @param x X-coordinate of the point + * @param y Y-coordinate of the point + * @param color Default color (line color) used if borderColor is not specified + * @param textMeasurer Text measurer for calculating text dimensions + * @param xIndex Index of the point in the data array + * @param yValue Y-axis value of the point + * @param xAxisData List of X-axis labels + * @param config Tooltip configuration + */ +@OptIn(ExperimentalTextApi::class) +private fun DrawScope.drawTooltipBox( + x: Dp, + y: Double, + color: Color, + textMeasurer: TextMeasurer, + xIndex: Int, + yValue: Double, + xAxisData: List, + config: TooltipConfig, +) { + // Generate tooltip text based on content configuration + val tooltipText = generateTooltipText( + content = config.content, + xIndex = xIndex, + yValue = yValue, + xAxisData = xAxisData + ) + + // Create text style + val textStyle = TextStyle( + fontSize = config.textSize, + color = config.textColor + ) + + // Measure text to determine tooltip size + val textLayoutResult = textMeasurer.measure( + text = AnnotatedString(tooltipText), + style = textStyle + ) + + // Calculate tooltip box size + val tooltipSize = when (config.size) { + is TooltipSize.Auto -> { + // Auto-size based on text with padding + Size( + width = textLayoutResult.size.width + (config.padding.toPx() * 2), + height = textLayoutResult.size.height + (config.padding.toPx() * 2) + ) + } + + is TooltipSize.Fixed -> { + Size( + width = config.size.width.toPx(), + height = config.size.height.toPx() + ) + } + } + + // Calculate tooltip position based on alignment + val tooltipX = when (config.position) { + is TooltipPosition.Center -> x.toPx() - tooltipSize.width / 2f + is TooltipPosition.Left -> x.toPx() - tooltipSize.width + is TooltipPosition.Right -> x.toPx() + } + + val tooltipTopLeft = Offset( + x = tooltipX, + y = y.toFloat() - tooltipSize.height - 10.dp.toPx() + ) + + val borderColor = config.borderColor ?: color + + // Draw tooltip with appropriate corner handling + val radii = config.cornerRadii + if (radii != null) { + // Use individual corner radii with Path + drawTooltipWithIndividualCorners( + topLeft = tooltipTopLeft, + size = tooltipSize, + radii = radii, + backgroundColor = config.backgroundColor, + borderColor = borderColor + ) + } else { + // Use uniform corner radius + val tooltipBounds = Rect(tooltipTopLeft, tooltipSize) + drawRoundRect( + color = config.backgroundColor, + topLeft = tooltipBounds.topLeft, + size = tooltipBounds.size, + cornerRadius = CornerRadius(config.cornerRadius.toPx()), + style = Fill + ) + drawRoundRect( + color = borderColor, + topLeft = tooltipBounds.topLeft, + size = tooltipBounds.size, + cornerRadius = CornerRadius(config.cornerRadius.toPx()), + style = Stroke(width = 1.dp.toPx()) + ) + } + + // Calculate text position (centered in tooltip) + val textOffset = Offset( + x = tooltipTopLeft.x + (tooltipSize.width - textLayoutResult.size.width) / 2f, + y = tooltipTopLeft.y + (tooltipSize.height - textLayoutResult.size.height) / 2f + ) + + // Draw text + drawContext.canvas.nativeCanvas.apply { + drawText( + textMeasurer = textMeasurer, + text = tooltipText, + style = textStyle, + topLeft = textOffset + ) + } +} + +/** + * Generates the tooltip text content based on the configuration. + * + * @param content Tooltip content configuration + * @param xIndex Index of the point + * @param yValue Y-axis value + * @param xAxisData List of X-axis labels + * @return Formatted tooltip text + */ +private fun generateTooltipText( + content: TooltipContent, + xIndex: Int, + yValue: Double, + xAxisData: List, +): String { + return when (content) { + is TooltipContent.YValue -> { + val formattedValue = content.formatter(yValue) + "${content.label}: $formattedValue" + } + + is TooltipContent.XValue -> { + val xValue = content.xAxisData.getOrElse(xIndex) { + xAxisData.getOrElse(xIndex) { xIndex.toString() } + } + "${content.label}: $xValue" + } + + is TooltipContent.XYValue -> { + val xValue = content.xAxisData.getOrElse(xIndex) { + xAxisData.getOrElse(xIndex) { xIndex.toString() } + } + val formattedYValue = content.yFormatter(yValue) + "${content.xLabel}: $xValue\n${content.yLabel}: $formattedYValue" + } + + is TooltipContent.Custom -> { + content.formatter(xIndex, yValue, xAxisData) + } + } +} + +/** + * Draws a rounded rectangle with individual corner radii using Path. + * + * @param topLeft Top-left position of the rectangle + * @param size Size of the rectangle + * @param radii Individual corner radii + * @param backgroundColor Fill color + * @param borderColor Border color + */ +private fun DrawScope.drawTooltipWithIndividualCorners( + topLeft: Offset, + size: Size, + radii: TooltipCornerRadii, + backgroundColor: Color, + borderColor: Color, +) { + val rect = Rect(topLeft, size) + + val roundRect = RoundRect( + rect = rect, + topLeft = CornerRadius(radii.topLeft.toPx()), + topRight = CornerRadius(radii.topRight.toPx()), + bottomLeft = CornerRadius(radii.bottomLeft.toPx()), + bottomRight = CornerRadius(radii.bottomRight.toPx()) + ) + + val path = Path().apply { + addRoundRect(roundRect) + } + + // Draw fill + drawPath( + path = path, + color = backgroundColor, + style = Fill + ) + + // Draw border + drawPath( + path = path, + color = borderColor, + style = Stroke(width = 1.dp.toPx()) + ) +} \ No newline at end of file diff --git a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/model/LineParameters.kt b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/model/LineParameters.kt index e0f37dfa..08e2ece8 100644 --- a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/model/LineParameters.kt +++ b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/model/LineParameters.kt @@ -8,5 +8,6 @@ data class LineParameters( val lineColor: Color, val lineType: LineType, val lineShadow: Boolean, + val tooltipConfig: TooltipConfig = TooltipConfig(), ) diff --git a/chart/src/commonMain/kotlin/com/aay/compose/lineChart/model/TooltipConfig.kt b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/model/TooltipConfig.kt new file mode 100644 index 00000000..223723e7 --- /dev/null +++ b/chart/src/commonMain/kotlin/com/aay/compose/lineChart/model/TooltipConfig.kt @@ -0,0 +1,159 @@ +package com.aay.compose.lineChart.model + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.aay.compose.utils.formatToThousandsMillionsBillions + +/** + * Configuration for customizing the tooltip appearance and content when clicking on line chart points. + * + * @param enabled Whether to show the tooltip when clicking on points + * @param backgroundColor Background color of the tooltip box + * @param borderColor Border color of the tooltip box. If null, uses the line color + * @param textColor Color of the text inside the tooltip + * @param textSize Font size of the tooltip text + * @param cornerRadius Corner radius for all corners (used when cornerRadii is null) + * @param cornerRadii Individual corner radii. If provided, overrides cornerRadius + * @param size Size configuration for the tooltip box + * @param content Content configuration defining what to display in the tooltip + * @param padding Internal padding of the tooltip box + * @param markerStyle Style configuration for the circular marker at the clicked point + * @param position Horizontal position alignment of the tooltip relative to the point + */ +data class TooltipConfig( + val enabled: Boolean = true, + val backgroundColor: Color = Color.White, + val borderColor: Color? = null, + val textColor: Color = Color.Black, + val textSize: TextUnit = 8.sp, + val cornerRadius: Dp = 16.dp, + val cornerRadii: TooltipCornerRadii? = null, + val size: TooltipSize = TooltipSize.Auto, + val content: TooltipContent = TooltipContent.YValue(), + val padding: Dp = 8.dp, + val markerStyle: MarkerStyle = MarkerStyle.Stroke(), + val position: TooltipPosition = TooltipPosition.Center, +) + +/** + * Individual corner radii for the tooltip box. + * + * @param topLeft Top-left corner radius + * @param topRight Top-right corner radius + * @param bottomLeft Bottom-left corner radius + * @param bottomRight Bottom-right corner radius + */ +data class TooltipCornerRadii( + val topLeft: Dp = 16.dp, + val topRight: Dp = 16.dp, + val bottomLeft: Dp = 16.dp, + val bottomRight: Dp = 16.dp, +) + +/** + * Defines the horizontal position of the tooltip relative to the clicked point. + */ +sealed class TooltipPosition { + /** + * Tooltip is centered horizontally above the point + */ + object Center : TooltipPosition() + + /** + * Tooltip is aligned to the left of the point + */ + object Left : TooltipPosition() + + /** + * Tooltip is aligned to the right of the point + */ + object Right : TooltipPosition() +} + +/** + * Defines the size of the tooltip box. + */ +sealed class TooltipSize { + /** + * Automatically calculates size based on text content + */ + object Auto : TooltipSize() + + /** + * Fixed size for the tooltip box + * @param width Width of the tooltip + * @param height Height of the tooltip + */ + data class Fixed(val width: Dp, val height: Dp) : TooltipSize() +} + +/** + * Defines the content to display in the tooltip. + */ +sealed class TooltipContent { + /** + * Display only the Y value (data point value) + * @param label Label text to show before the value (e.g., "Value", "القيمة") + * @param formatter Function to format the Y value for display + */ + data class YValue( + val label: String = "Value", + val formatter: (Double) -> String = { it.toFloat().formatToThousandsMillionsBillions() } + ) : TooltipContent() + + /** + * Display only the X value (axis label) + * @param label Label text to show before the X value + * @param xAxisData List of X-axis labels to use for display + */ + data class XValue( + val label: String = "X", + val xAxisData: List = emptyList() + ) : TooltipContent() + + /** + * Display both X and Y values + * @param xLabel Label for the X value + * @param yLabel Label for the Y value + * @param xAxisData List of X-axis labels to use for X value display + * @param yFormatter Function to format the Y value for display + */ + data class XYValue( + val xLabel: String = "X", + val yLabel: String = "Y", + val xAxisData: List = emptyList(), + val yFormatter: (Double) -> String = { it.toFloat().formatToThousandsMillionsBillions() } + ) : TooltipContent() + + /** + * Custom tooltip content with full control over formatting + * @param formatter Function that receives x index, y value, and x-axis data, and returns formatted string + */ + data class Custom( + val formatter: (xIndex: Int, yValue: Double, xAxisData: List) -> String + ) : TooltipContent() +} + +/** + * Defines the style of the circular marker shown at the clicked point. + */ +sealed class MarkerStyle { + /** + * Stroke (outline) circle marker + * @param strokeWidth Width of the circle outline + */ + data class Stroke(val strokeWidth: Dp = 2.dp) : MarkerStyle() + + /** + * Solid (filled) circle marker + */ + object Solid : MarkerStyle() + + /** + * No marker circle (only show tooltip) + */ + object None : MarkerStyle() +} diff --git a/common/src/commonMain/kotlin/com/aay/common/LineChartTooltipExamples.kt b/common/src/commonMain/kotlin/com/aay/common/LineChartTooltipExamples.kt new file mode 100644 index 00000000..54b44e6e --- /dev/null +++ b/common/src/commonMain/kotlin/com/aay/common/LineChartTooltipExamples.kt @@ -0,0 +1,364 @@ +package com.aay.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.aay.compose.lineChart.model.LineParameters +import com.aay.compose.lineChart.LineChart +import com.aay.compose.lineChart.model.* + +/** + * Example 1: Default tooltip (backward compatibility) + * Shows the default "Value: X" tooltip with stroke circle marker + */ +@Composable +fun LineChartWithDefaultTooltip() { + val lineParameters = listOf( + LineParameters( + label = "Revenue", + data = listOf(70.0, 80.0, 50.33, 40.0, 100.500, 50.0), + lineColor = Color(0xFF6C3428), + lineType = LineType.CURVED_LINE, + lineShadow = true, + // No tooltipConfig specified - uses default + ) + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun"), + animateChart = true, + yAxisRange = 14, + ) + } +} + +/** + * Example 2: Localized tooltip with custom label + * Shows how to localize the tooltip label (e.g., Arabic) + */ +@Composable +fun LineChartWithLocalizedTooltip() { + val lineParameters = listOf( + LineParameters( + label = "الإيرادات", + data = listOf(70.0, 80.0, 50.33, 40.0, 100.500, 50.0), + lineColor = Color(0xFF6C3428), + lineType = LineType.CURVED_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + content = TooltipContent.YValue( + label = "القيمة", // "Value" in Arabic + ) + ) + ) + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو"), + animateChart = true, + yAxisRange = 14, + ) + } +} + +/** + * Example 3: Tooltip showing both X and Y coordinates + * Demonstrates how to display both axis values in the tooltip + */ +@Composable +fun LineChartWithXYTooltip() { + val lineParameters = listOf( + LineParameters( + label = "Sales", + data = listOf(70.0, 80.0, 50.33, 40.0, 100.500, 50.0), + lineColor = Color(0xFF2196F3), + lineType = LineType.DEFAULT_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + content = TooltipContent.XYValue( + xLabel = "Month", + yLabel = "Sales", + xAxisData = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun") + ), + backgroundColor = Color.White, + textColor = Color.Black, + textSize = 10.sp, + ) + ) + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun"), + animateChart = true, + yAxisRange = 14, + ) + } +} + +/** + * Example 4: Customized tooltip appearance + * Shows how to customize background color, size, corner radius, and text style + */ +@Composable +fun LineChartWithCustomStyledTooltip() { + val lineParameters = listOf( + LineParameters( + label = "Revenue", + data = listOf(70.0, 80.0, 50.33, 40.0, 100.500, 50.0), + lineColor = Color(0xFF4CAF50), + lineType = LineType.CURVED_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + backgroundColor = Color(0xFF1E1E1E), // Dark background + borderColor = Color(0xFF4CAF50), // Green border + textColor = Color.White, + textSize = 12.sp, + cornerRadius = 8.dp, + size = TooltipSize.Fixed(width = 120.dp, height = 40.dp), + padding = 12.dp, + content = TooltipContent.YValue( + label = "Revenue", + formatter = { value -> "$${value.toInt()}" } // Custom formatter + ) + ) + ) + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("Q1", "Q2", "Q3", "Q4", "Q5", "Q6"), + animateChart = true, + yAxisRange = 14, + ) + } +} + +/** + * Example 5: Solid circle marker instead of stroke + * Demonstrates how to use a filled circle marker + */ +@Composable +fun LineChartWithSolidMarker() { + val lineParameters = listOf( + LineParameters( + label = "Temperature", + data = listOf(20.0, 22.5, 25.0, 23.5, 21.0, 19.5), + lineColor = Color(0xFFFF5722), + lineType = LineType.CURVED_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + markerStyle = MarkerStyle.Solid, // Solid filled circle + backgroundColor = Color(0xFFF8F8F8), + //borderColor = Color(0xFFFF5722), + textColor = Color(0xFF5D4037), + content = TooltipContent.YValue( + label = "Temp", + formatter = { value -> "${value.toInt()}°C" } + ), + position = TooltipPosition.Left, + cornerRadii = TooltipCornerRadii( + topLeft = 8.dp, + topRight = 8.dp, + bottomLeft = 8.dp, + bottomRight = 2.dp + ) + ) + ) + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat"), + animateChart = true, + yAxisRange = 10, + ) + } +} + +/** + * Example 6: No marker circle (tooltip only) + * Shows tooltip without the circular marker + */ +@Composable +fun LineChartWithTooltipNoMarker() { + val lineParameters = listOf( + LineParameters( + label = "Progress", + data = listOf(10.0, 25.0, 45.0, 60.0, 80.0, 95.0), + lineColor = Color(0xFF9C27B0), + lineType = LineType.DEFAULT_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + markerStyle = MarkerStyle.None, // No marker circle + backgroundColor = Color(0xFFF3E5F5), + textColor = Color(0xFF4A148C), + content = TooltipContent.YValue( + label = "Progress", + formatter = { value -> "${value.toInt()}%" } + ) + ) + ) + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("Week 1", "Week 2", "Week 3", "Week 4", "Week 5", "Week 6"), + animateChart = true, + yAxisRange = 14, + ) + } +} + +/** + * Example 7: Custom tooltip content with formatter + * Demonstrates complete control over tooltip content + */ +@Composable +fun LineChartWithCustomTooltipContent() { + val lineParameters = listOf( + LineParameters( + label = "Users", + data = listOf(1200.0, 1850.0, 2100.0, 1950.0, 2400.0, 2800.0), + lineColor = Color(0xFF00BCD4), + lineType = LineType.CURVED_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + markerStyle = MarkerStyle.Stroke(strokeWidth = 3.dp), + backgroundColor = Color(0xFFE0F7FA), + borderColor = Color(0xFF00BCD4), + textColor = Color(0xFF006064), + textSize = 11.sp, + content = TooltipContent.Custom { xIndex, yValue, xAxisData -> + val month = xAxisData.getOrNull(xIndex) ?: "N/A" + val users = yValue.toInt() + "$month\n$users users" + } + ) + ) + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun"), + animateChart = true, + yAxisRange = 14, + ) + } +} + +/** + * Example 8: Multi-line chart with different tooltip configs + * Shows how each line can have its own tooltip configuration + */ +@Composable +fun MultiLineChartWithDifferentTooltips() { + val lineParameters = listOf( + LineParameters( + label = "Revenue", + data = listOf(70.0, 80.0, 50.33, 40.0, 100.500, 50.0), + lineColor = Color(0xFF4CAF50), + lineType = LineType.CURVED_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + markerStyle = MarkerStyle.Solid, + backgroundColor = Color(0xFFE8F5E9), + textColor = Color(0xFF1B5E20), + content = TooltipContent.YValue(label = "Revenue") + ) + ), + LineParameters( + label = "Expenses", + data = listOf(60.0, 70.6, 40.33, 86.232, 88.0, 90.0), + lineColor = Color(0xFFFF5722), + lineType = LineType.DEFAULT_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + markerStyle = MarkerStyle.Stroke(strokeWidth = 2.dp), + backgroundColor = Color(0xFFFBE9E7), + textColor = Color(0xFFBF360C), + content = TooltipContent.YValue(label = "Expenses") + ) + ), + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun"), + animateChart = true, + yAxisRange = 14, + oneLineChart = false, + ) + } +} + +/** + * Example 9: Disabled tooltip + * Shows how to disable the tooltip completely + */ +@Composable +fun LineChartWithDisabledTooltip() { + val lineParameters = listOf( + LineParameters( + label = "Data", + data = listOf(70.0, 80.0, 50.33, 40.0, 100.500, 50.0), + lineColor = Color(0xFF607D8B), + lineType = LineType.CURVED_LINE, + lineShadow = true, + tooltipConfig = TooltipConfig( + enabled = false // Disable tooltip + ) + ) + ) + + Box(Modifier.fillMaxSize()) { + LineChart( + modifier = Modifier.fillMaxSize(), + linesParameters = lineParameters, + isGrid = true, + gridColor = Color.LightGray, + xAxisData = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun"), + animateChart = true, + yAxisRange = 14, + ) + } +}