diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/bar/Bar.kt b/src/commonMain/kotlin/io/github/koalaplot/core/bar/Bar.kt index 1564863da..b92b4bbec 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/bar/Bar.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/bar/Bar.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor +import io.github.koalaplot.core.xygraph.XYGraphScope /** * A default implementation of a bar for bar charts. @@ -54,10 +55,96 @@ public fun BarScope.DefaultBar( /** * Factory function to create a Composable that emits a solid colored bar. */ -public fun solidBar( +@Deprecated( + message = "Delegates to vertical solid bar. Use explicitly dedicated factory function.", + replaceWith = ReplaceWith("verticalSolidBar(color, shape, border)") +) +public fun solidBar( color: Color, shape: Shape = RectangleShape, border: BorderStroke? = null, -): @Composable BarScope.() -> Unit = { +): DefaultVerticalBarComposable = verticalSolidBar(color, shape, border) + +/** + * Factory function to create a Composable that emits a solid colored bar. + */ +public fun verticalSolidBar( + color: Color, + shape: Shape = RectangleShape, + border: BorderStroke? = null, +): DefaultVerticalBarComposable = { _, _, _ -> + DefaultBar(SolidColor(color), shape = shape, border = border) +} + +/** + * Factory function to create a Composable that emits a solid colored bar. + */ +public fun horizontalSolidBar( + color: Color, + shape: Shape = RectangleShape, + border: BorderStroke? = null, +): DefaultHorizontalBarComposable = { _, _, _ -> DefaultBar(SolidColor(color), shape = shape, border = border) } + +/** + * Factory function to create a Composable that emits a solid colored bar. + * Each bar features a planar starting side and a convex ending side. + */ +public fun XYGraphScope.verticalPlanoConvexBar( + color: Color, + border: BorderStroke? = null, +): DefaultVerticalBarComposable = { _, index, value -> + DefaultBar( + brush = SolidColor(color), + shape = VerticalPlanoConvexShape(this@verticalPlanoConvexBar, index, value), + border = border + ) +} + +/** + * Factory function to create a Composable that emits a solid colored bar. + * Each bar features a convex shape on both its starting and ending sides. + * There's an additional convex cutout at the bottom of the bar. + */ +public fun XYGraphScope.verticalBiConvexBar( + color: Color, + border: BorderStroke? = null, +): DefaultVerticalBarComposable = { _, index, value -> + DefaultBar( + brush = SolidColor(color), + shape = VerticalBiConvexShape(this@verticalBiConvexBar, index, value), + border = border + ) +} + +/** + * Factory function to create a Composable that emits a solid colored bar. + * Each bar features a planar shape at one end and a convex shape at the other. + */ +public fun XYGraphScope.horizontalPlanoConvexBar( + color: Color, + border: BorderStroke? = null, +): DefaultHorizontalBarComposable = { _, index, value -> + DefaultBar( + brush = SolidColor(color), + shape = HorizontalPlanoConvexShape(this@horizontalPlanoConvexBar, index, value), + border = border + ) +} + +/** + * Factory function to create a Composable that emits a solid colored bar. + * Each bar features a convex shape on both its starting and ending sides. + * There's an additional convex cutout at the bottom of the bar. + */ +public fun XYGraphScope.horizontalBiConvexBar( + color: Color, + border: BorderStroke? = null, +): DefaultHorizontalBarComposable = { _, index, value -> + DefaultBar( + brush = SolidColor(color), + shape = HorizontalBiConvexShape(this@horizontalBiConvexBar, index, value), + border = border + ) +} diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/bar/BarPlotShapes.kt b/src/commonMain/kotlin/io/github/koalaplot/core/bar/BarPlotShapes.kt new file mode 100644 index 000000000..794cfc95d --- /dev/null +++ b/src/commonMain/kotlin/io/github/koalaplot/core/bar/BarPlotShapes.kt @@ -0,0 +1,660 @@ +package io.github.koalaplot.core.bar + +import androidx.compose.runtime.Stable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import io.github.koalaplot.core.util.rad +import io.github.koalaplot.core.util.toDegrees +import io.github.koalaplot.core.xygraph.AxisModel +import io.github.koalaplot.core.xygraph.XYGraphScope +import kotlin.math.asin +import kotlin.math.max + +/** + * Rectangle shape with convex shaped side. + * Useful for Single Vertical Bar Plot rendering. + * Use in Stacked Bars is discouraged. + */ +@Stable +private val DefaultVerticalPlanoConvexShape: Shape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val shapeWidth = size.width + val shapeHeight = size.height + val arcRadius = shapeWidth / 2 + + return Path().apply { + val rectHeight = max((shapeHeight - arcRadius), 0F) + addRect( + rect = Rect( + offset = Offset(0F, arcRadius), + size = Size(shapeWidth, rectHeight) + ) + ) + + val heightRadiusOffset = max((arcRadius - shapeHeight), 0F) + val heightRadiusOffsetDegrees = + asin(heightRadiusOffset / arcRadius).rad.toDegrees().value.toFloat() + addArc( + oval = Size(shapeWidth, shapeWidth).toRect(), + startAngleDegrees = 180F + heightRadiusOffsetDegrees, + sweepAngleDegrees = 180F - 2 * heightRadiusOffsetDegrees + ) + }.let(Outline::Generic) + } +} + +/** + * Rectangle shape with convex shaped side. + * Useful for Single Horizontal Bar Plot rendering. + * Use in Stacked Bars is discouraged. + */ +@Stable +private val DefaultHorizontalPlanoConvexShape: Shape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val shapeWidth = size.width + val shapeHeight = size.height + val arcRadius = shapeHeight / 2 + + return Path().apply { + val rectWidth = max((shapeWidth - arcRadius), 0F) + addRect(Size(rectWidth, shapeHeight).toRect()) + + val widthRadiusOffset = max((arcRadius - shapeWidth), 0F) + val widthRadiusOffsetDegrees = + asin(widthRadiusOffset / arcRadius).rad.toDegrees().value.toFloat() + addArc( + oval = Rect( + offset = Offset(rectWidth - arcRadius - widthRadiusOffset, 0F), + size = Size(shapeHeight, shapeHeight) + ), + startAngleDegrees = 270F + widthRadiusOffsetDegrees, + sweepAngleDegrees = 180F - 2 * widthRadiusOffsetDegrees + ) + }.let(Outline::Generic) + } +} + +/** + * Rectangle shape with convex shaped sides. + * Useful for Single Vertical Bar Plot rendering. + * Use in Stacked Bars is discouraged. + */ +@Stable +private val DefaultVerticalBiConvexShape: Shape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val outline = DefaultVerticalPlanoConvexShape.createOutline(size, layoutDirection, density) as Outline.Generic + + val shapeWidth = size.width + val shapeHeight = size.height + val arcRadius = shapeWidth / 2 + + val cutoutRect = Path().apply { + addRect( + rect = Rect( + offset = Offset(0F, shapeHeight - arcRadius), + size = Size(shapeWidth, shapeWidth) + ) + ) + } + val cutoutArc = Path().apply { + addArc( + oval = Rect( + offset = Offset(0F, shapeHeight - shapeWidth), + size = Size(shapeWidth, shapeWidth) + ), + startAngleDegrees = 0F, + sweepAngleDegrees = 180F + ) + } + val cutout = (cutoutRect - cutoutArc) + return (outline.path - cutout).let(Outline::Generic) + } +} + +/** + * Rectangle shape with convex shaped sides. + * Useful for Single Horizontal Bar Plot rendering. + * Use in Stacked Bars is discouraged. + */ +@Stable +private val DefaultHorizontalBiConvexShape: Shape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val outline = DefaultHorizontalPlanoConvexShape + .createOutline(size, layoutDirection, density) as Outline.Generic + + val shapeHeight = size.height + val arcRadius = shapeHeight / 2 + + val cutoutRect = Path().apply { + addRect( + rect = Rect( + offset = Offset(-arcRadius, 0F), + size = Size(shapeHeight, shapeHeight) + ) + ) + } + val cutoutArc = Path().apply { + addArc( + oval = Size(shapeHeight, shapeHeight).toRect(), + startAngleDegrees = 90F, + sweepAngleDegrees = 180F + ) + } + val cutout = (cutoutRect - cutoutArc) + return (outline.path - cutout).let(Outline::Generic) + } +} + +/** + * Rectangle shape with planar/convex shaped sides. + * Useful for Single Vertical Bar and Stacked Bars Plot rendering. + * + * @param xyGraphScope Provides access to [yAxisModel] and acts as an implementation of [XYGraphScope]. + * @param index Represents the element index within the series. + * @param value The [VerticalBarPlotEntry] that defines the cutouts for the [VerticalPlanoConvexShape]. + */ +@Stable +public class VerticalPlanoConvexShape>( + private val xyGraphScope: XYGraphScope, + private val index: Int, + private val value: E +) : Shape, XYGraphScope by xyGraphScope { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val shapeWidth = size.width + val shapeHeight = size.height + val arcRadius = shapeWidth / 2 + + // Rendering negative values + val isInverted = value.y.end < value.y.start + // Required for proper bar rendering in waterfall charts + if (index == 0) { + val outline = + DefaultVerticalPlanoConvexShape.createOutline(size, layoutDirection, density) as Outline.Generic + + outline.path.apply { + // Rendering bar in negative direction + if (isInverted) { + inverted(pivotX = shapeWidth / 2F, pivotY = shapeHeight / 2F) + } + } + return outline + } + + val (yZeroOffset, yMinOffset, yMaxOffset) = yAxisModel.yOffsets(value.y.start, value.y.end) + + // Prevent division by zero + return if (yMaxOffset == yMinOffset) { + Path().let(Outline::Generic) + } else { + // AxisModel's `computeOffset` method provides relative values between 0 and 1 + // Mapping offset values to pixel values + val heightOffsetRatio = size.height / (yMaxOffset - yMinOffset) + val offsetToHeight = { offset: Float -> offset * heightOffsetRatio } + + // Bars start with a concave path which might go below zero; the respective shape must be cut appropriately + // Calculating aforementioned offset values for a bar's max and min value relative to the axis zero line + val yMinZeroOffset = yMinOffset - yZeroOffset + val yMaxZeroOffset = yMaxOffset - yZeroOffset + + // Getting screen's height pixel values from offsets + val yMinZeroHeight = offsetToHeight(yMinZeroOffset) + val yMaxZeroHeight = offsetToHeight(yMaxZeroOffset) + + // If min and max values are greater than arcRadius, aforementioned below zero compensation is not required + // and therefore becomes ineffective + val yMinZeroArcHeight = max((arcRadius - yMinZeroHeight), 0F) + val yMaxZeroArcHeight = max((arcRadius - yMaxZeroHeight), 0F) + + // Prevent arc from being drawn below zero by subtracting value in degrees + val yMaxZeroArcHeightDegrees = + asin(yMaxZeroArcHeight / arcRadius).rad.toDegrees().value.toFloat() + + Path().apply { + ( + Path().apply { + addArc( + oval = Size(shapeWidth, shapeWidth).toRect(), + startAngleDegrees = 180F + yMaxZeroArcHeightDegrees, + sweepAngleDegrees = 180F - 2 * yMaxZeroArcHeightDegrees + ) + } - Path().apply { + addArc( + oval = Rect( + offset = Offset(0F, shapeHeight), + size = Size(shapeWidth, shapeWidth) + ), + startAngleDegrees = 180F, + sweepAngleDegrees = 180F + ) + } + ).let(::addPath) + + ( + Path().apply { + addRect( + rect = Rect( + offset = Offset(0F, arcRadius), + size = Size(shapeWidth, max(shapeHeight - yMinZeroArcHeight, 0F)) + ) + ) + } - Path().apply { + addArc( + oval = Rect( + offset = Offset(0F, shapeHeight), + size = Size(shapeWidth, shapeWidth) + ), + startAngleDegrees = 180F, + sweepAngleDegrees = 180F + ) + } + ).let(::addPath) + // Rendering bar in negative direction + if (isInverted) { + inverted(pivotX = shapeWidth / 2F, pivotY = shapeHeight / 2F) + } + }.let(Outline::Generic) + } + } +} + +/** + * Rectangle shape with planar/convex shaped sides. + * Useful for Single Horizontal Bar and Stacked Bars Plot rendering. + * + * @param xyGraphScope Provides access to [yAxisModel] and acts as an implementation of [XYGraphScope]. + * @param index Represents the element index within the series. + * @param value The [HorizontalBarPlotEntry] that defines the cutouts for the [HorizontalPlanoConvexShape]. + */ +@Stable +public class HorizontalPlanoConvexShape>( + private val xyGraphScope: XYGraphScope, + private val index: Int, + private val value: E +) : Shape, XYGraphScope by xyGraphScope { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val shapeWidth = size.width + val shapeHeight = size.height + val arcRadius = shapeHeight / 2 + + // Rendering negative values + val isInverted = value.x.end < value.x.start + // Required for proper bar rendering in waterfall charts + if (index == 0) { + val outline = + DefaultHorizontalPlanoConvexShape.createOutline(size, layoutDirection, density) as Outline.Generic + + outline.path.apply { + // Rendering bar in negative direction + if (isInverted) { + inverted(pivotX = shapeWidth / 2F, pivotY = shapeHeight / 2F) + } + } + return outline + } + + val (xZeroOffset, xMinOffset, xMaxOffset) = xAxisModel.xOffsets(value.x.start, value.x.end) + + // Prevent division by zero + return if (xMaxOffset == xMinOffset) { + Path().let(Outline::Generic) + } else { + // AxisModel's `computeOffset` method provides relative values between 0 and 1 + // Mapping offset values to pixel values + val widthOffsetRatio = size.width / (xMaxOffset - xMinOffset) + val offsetToWidth = { offset: Float -> offset * widthOffsetRatio } + + // Bars start with a concave path which might go below zero; the respective shape must be cut appropriately + // Calculating aforementioned offset values for a bar's max and min value relative to the axis zero line + val xMinZeroOffset = xMinOffset - xZeroOffset + val xMaxZeroOffset = xMaxOffset - xZeroOffset + + // Getting screen's width pixel values from offsets + val xMinZeroWidth = offsetToWidth(xMinZeroOffset) + val xMaxZeroWidth = offsetToWidth(xMaxZeroOffset) + + // If min and max values are greater than arcRadius, aforementioned below zero compensation is not required + // and therefore becomes ineffective + val xMinZeroArcWidth = max((arcRadius - xMinZeroWidth), 0F) + val xMaxZeroArcWidth = max((arcRadius - xMaxZeroWidth), 0F) + + // Prevent arc from being drawn below zero by subtracting value in degrees + val xMaxZeroArcWidthDegrees = + asin(xMaxZeroArcWidth / arcRadius).rad.toDegrees().value.toFloat() + + Path().apply { + ( + Path().apply { + val rectWidth = max((shapeWidth - arcRadius), 0F) + val widthRadiusOffset = max((arcRadius - shapeWidth), 0F) + addArc( + oval = Rect( + offset = Offset(rectWidth - arcRadius - widthRadiusOffset, 0F), + size = Size(shapeHeight, shapeHeight) + ), + startAngleDegrees = 270F + xMaxZeroArcWidthDegrees, + sweepAngleDegrees = 180F - 2 * xMaxZeroArcWidthDegrees + ) + } - Path().apply { + addArc( + oval = Rect( + offset = Offset(-shapeHeight, 0F), + size = Size(shapeHeight, shapeHeight) + ), + startAngleDegrees = 270F, + sweepAngleDegrees = 180F + ) + } + ).let(::addPath) + + ( + Path().apply { + addRect( + rect = Rect( + offset = Offset(-arcRadius + xMinZeroArcWidth, 0F), + size = Size(max(shapeWidth - xMinZeroArcWidth, 0F), shapeHeight) + ) + ) + } - Path().apply { + addArc( + oval = Rect( + offset = Offset(-shapeHeight, 0F), + size = Size(shapeHeight, shapeHeight) + ), + startAngleDegrees = 270F, + sweepAngleDegrees = 180F + ) + } + ).let(::addPath) + // Rendering bar in negative direction + if (isInverted) { + inverted(pivotX = shapeWidth / 2F, pivotY = shapeHeight / 2F) + } + }.let(Outline::Generic) + } + } +} + +/** + * Rectangle shape with convex shaped sides and an additional convex cutout at the bottom. + * Useful for Single Vertical Bar and Stacked Bars Plot rendering. + * + * Primary constructor: + * @param planoConvexShape The internal shape logic used for rendering. + * @param index Represents the element index within the series. + * @param value The [VerticalBarPlotEntry] that defines the cutouts for the [VerticalPlanoConvexShape]. + * + * Secondary constructor: + * @param xyGraphScope Provides access to [yAxisModel] and acts as an implementation of [XYGraphScope]. + * @param index Represents the element index within the series. + * @param value The [VerticalBarPlotEntry] used to construct the internal shape as well as the additional convex cutout. + */ +@Stable +public class VerticalBiConvexShape> private constructor( + private val planoConvexShape: VerticalPlanoConvexShape, + private val index: Int, + private val value: E +) : Shape, XYGraphScope by planoConvexShape { + + public constructor( + xyGraphScope: XYGraphScope, + index: Int, + value: E + ) : this( + VerticalPlanoConvexShape(xyGraphScope, index, value), + index, + value + ) + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val shapeWidth = size.width + val shapeHeight = size.height + val arcRadius = shapeWidth / 2 + + // Rendering negative values + val isInverted = value.y.end < value.y.start + // Required for proper bar rendering in waterfall charts + if (index == 0) { + val outline = DefaultVerticalBiConvexShape.createOutline(size, layoutDirection, density) as Outline.Generic + + outline.path.apply { + // Rendering bar in negative direction + if (isInverted) { + inverted(pivotX = shapeWidth / 2F, pivotY = shapeHeight / 2F) + } + } + return outline + } + + val (yZeroOffset, yMinOffset, yMaxOffset) = yAxisModel.yOffsets(value.y.start, value.y.end) + + // Prevent division by zero + return if (yMaxOffset == yMinOffset) { + Path().let(Outline::Generic) + } else { + // AxisModel's `computeOffset` method provides relative values between 0 and 1 + // Mapping offset values to pixel values + val heightOffsetRatio = size.height / (yMaxOffset - yMinOffset) + val offsetToHeight = { offset: Float -> offset * heightOffsetRatio } + + // Bars start with a concave path which might go below zero; the respective shape must be cut appropriately + // Calculating aforementioned offset values for a bar's max and min value relative to the axis zero line + val yMaxZeroOffset = yMaxOffset - yZeroOffset + // Getting screen's height pixel values from offsets + val yMaxZeroHeight = offsetToHeight(yMaxZeroOffset) + + val outline = planoConvexShape.createOutline(size, layoutDirection, density) as Outline.Generic + + val cutoutRect = Path().apply { + addRect( + rect = Rect( + offset = Offset(0F, yMaxZeroHeight - arcRadius), + size = Size(shapeWidth, shapeWidth) + ) + ) + } + val cutoutArc = Path().apply { + addArc( + oval = Rect( + offset = Offset(0F, yMaxZeroHeight - shapeWidth), + size = Size(shapeWidth, shapeWidth) + ), + startAngleDegrees = 0F, + sweepAngleDegrees = 180F + ) + } + val cutout = (cutoutRect - cutoutArc).apply { + // Rendering bar in negative direction + if (isInverted) { + inverted(pivotX = shapeWidth / 2F, pivotY = shapeHeight / 2F) + } + } + (outline.path - cutout).let(Outline::Generic) + } + } +} + +/** + * Rectangle shape with convex shaped sides and an additional convex cutout at the bottom. + * Useful for Single Horizontal Bar and Stacked Bars Plot rendering. + * + * Primary constructor: + * @param planoConvexShape The internal shape logic used for rendering. + * @param index Represents the element index within the series. + * @param value The [HorizontalBarPlotEntry] that defines the cutouts for the [HorizontalPlanoConvexShape]. + * + * Secondary constructor: + * @param xyGraphScope Provides access to [yAxisModel] and acts as an implementation of [XYGraphScope]. + * @param index Represents the element index within the series. + * @param value The [HorizontalBarPlotEntry] used to construct the internal shape + * as well as the additional convex cutout. + */ +@Stable +public class HorizontalBiConvexShape> private constructor( + private val planoConvexShape: HorizontalPlanoConvexShape, + private val index: Int, + private val value: E +) : Shape, XYGraphScope by planoConvexShape { + + public constructor( + xyGraphScope: XYGraphScope, + index: Int, + value: E + ) : this( + HorizontalPlanoConvexShape(xyGraphScope, index, value), + index, + value + ) + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val shapeWidth = size.width + val shapeHeight = size.height + val arcRadius = shapeHeight / 2 + + // Rendering negative values + val isInverted = value.x.end < value.x.start + // Required for proper bar rendering in waterfall charts + if (index == 0) { + val outline = + DefaultHorizontalBiConvexShape.createOutline(size, layoutDirection, density) as Outline.Generic + + outline.path.apply { + // Rendering bar in negative direction + if (isInverted) { + inverted(pivotX = shapeWidth / 2F, pivotY = shapeHeight / 2F) + } + } + return outline + } + + val (xZeroOffset, xMinOffset, xMaxOffset) = xAxisModel.xOffsets(value.x.start, value.x.end) + + // Prevent division by zero + return if (xMaxOffset == xMinOffset) { + Path().let(Outline::Generic) + } else { + // AxisModel's `computeOffset` method provides relative values between 0 and 1 + // Mapping offset values to pixel values + val widthOffsetRatio = size.width / (xMaxOffset - xMinOffset) + val offsetToWidth = { offset: Float -> offset * widthOffsetRatio } + + // Bars start with a concave path which might go below zero; the respective shape must be cut appropriately + // Calculating aforementioned offset values for a bar's max and min value relative to the axis zero line + val xMinZeroOffset = xMinOffset - xZeroOffset + // Getting screen's width pixel values from offsets + val xMinZeroWidth = offsetToWidth(xMinZeroOffset) + + val outline = planoConvexShape.createOutline(size, layoutDirection, density) as Outline.Generic + + val cutoutRect = Path().apply { + addRect( + rect = Rect( + offset = Offset(-xMinZeroWidth, 0F), + size = Size(arcRadius, shapeHeight) + ) + ) + } + val cutoutArc = Path().apply { + addArc( + oval = Rect( + offset = Offset(-xMinZeroWidth, 0F), + size = Size(shapeHeight, shapeHeight) + ), + startAngleDegrees = 90F, + sweepAngleDegrees = 180F + ) + } + val cutout = (cutoutRect - cutoutArc).apply { + // Rendering bar in negative direction + if (isInverted) { + inverted(pivotX = shapeWidth / 2F, pivotY = shapeHeight / 2F) + } + } + (outline.path - cutout).let(Outline::Generic) + } + } +} + +private fun Path.inverted(pivotX: Float, pivotY: Float): Path { + Matrix().apply { + resetToPivotedTransform( + pivotX = pivotX, + pivotY = pivotY, + rotationZ = 180F + ) + }.let(::transform) + return this +} + +private fun AxisModel.xOffsets(xMin: Float, xMax: Float): XOffsets { + val xZeroOffset = computeOffset(0F).coerceIn(0F, 1F) + val xMinOffset = computeOffset(xMin).coerceIn(0f, 1f) + val xMaxOffset = computeOffset(xMax).coerceIn(0f, 1f) + return XOffsets( + xZeroOffset = xZeroOffset, + xMinOffset = xMinOffset, + xMaxOffset = xMaxOffset + ) +} + +private fun AxisModel.yOffsets(yMin: Float, yMax: Float): YOffsets { + val yZeroOffset = computeOffset(0F).coerceIn(0F, 1F) + val yMinOffset = computeOffset(yMin).coerceIn(0f, 1f) + val yMaxOffset = computeOffset(yMax).coerceIn(0f, 1f) + return YOffsets( + yZeroOffset = yZeroOffset, + yMinOffset = yMinOffset, + yMaxOffset = yMaxOffset + ) +} + +private data class XOffsets( + val xZeroOffset: Float, + val xMinOffset: Float, + val xMaxOffset: Float +) + +private data class YOffsets( + val yZeroOffset: Float, + val yMinOffset: Float, + val yMaxOffset: Float +) diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/bar/GroupedHorizontalBarPlot.kt b/src/commonMain/kotlin/io/github/koalaplot/core/bar/GroupedHorizontalBarPlot.kt index 027db73ae..839bbd243 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/bar/GroupedHorizontalBarPlot.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/bar/GroupedHorizontalBarPlot.kt @@ -30,7 +30,7 @@ import io.github.koalaplot.core.xygraph.XYGraphScope public fun > XYGraphScope.GroupedHorizontalBarPlot( data: List, modifier: Modifier = Modifier, - bar: @Composable BarScope.(dataIndex: Int, groupIndex: Int, entry: E) -> Unit = { i, g, _ -> + bar: HorizontalBarComposable = { i, g, _ -> val colors = remember(data) { generateHueColorPalette(data.maxOf { it.d.size }) } @@ -97,7 +97,7 @@ public fun XYGraphScope.GroupedHorizontalBarPlot( data class EntryWithBars( override val i: Y, - val xb: List, @Composable BarScope.() -> Unit>> + val xb: List, DefaultHorizontalBarComposable>> ) : BarPlotGroupedPointEntry { override val d: List> = object : AbstractList>() { override val size: Int = xb.size @@ -131,8 +131,13 @@ public fun XYGraphScope.GroupedHorizontalBarPlot( GroupedHorizontalBarPlot( data, modifier, - { xIndex, seriesIndex, _ -> - data.data[xIndex].xb[seriesIndex].second.invoke(this) + { xIndex, seriesIndex, value -> + data.data[xIndex].xb[seriesIndex].second.invoke( + this, + xIndex, + seriesIndex, + GroupedEntryToHorizontalEntryAdapter(value) + ) }, maxBarGroupWidth, startAnimationUseCase = startAnimationUseCase, @@ -180,15 +185,18 @@ public interface GroupedHorizontalBarPlotScope { * bars in this series. */ public fun series( - defaultBar: @Composable BarScope.() -> Unit = solidBar(Color.Blue), + defaultBar: DefaultHorizontalBarComposable = horizontalSolidBar(Color.Blue), content: HorizontalBarPlotScope.() -> Unit ) } private class GroupedHorizontalBarPlotScopeImpl : GroupedHorizontalBarPlotScope { val series: MutableList> = mutableListOf() - override fun series(defaultBar: @Composable BarScope.() -> Unit, content: HorizontalBarPlotScope.() -> Unit) { - val scope = HorizontalBarPlotScopeImpl(defaultBar) + override fun series( + defaultBar: DefaultHorizontalBarComposable, + content: HorizontalBarPlotScope.() -> Unit + ) { + val scope = HorizontalBarPlotScopeImpl(defaultBar) series.add(scope) scope.content() } diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/bar/GroupedVerticalBarPlot.kt b/src/commonMain/kotlin/io/github/koalaplot/core/bar/GroupedVerticalBarPlot.kt index 77313705b..d9c4b562d 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/bar/GroupedVerticalBarPlot.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/bar/GroupedVerticalBarPlot.kt @@ -30,7 +30,7 @@ import io.github.koalaplot.core.xygraph.XYGraphScope public fun > XYGraphScope.GroupedVerticalBarPlot( data: List, modifier: Modifier = Modifier, - bar: @Composable BarScope.(dataIndex: Int, groupIndex: Int, entry: E) -> Unit = { i, g, _ -> + bar: VerticalBarComposable = { i, g, _ -> val colors = remember(data) { generateHueColorPalette(data.maxOf { it.d.size }) } @@ -97,7 +97,7 @@ public fun XYGraphScope.GroupedVerticalBarPlot( data class EntryWithBars( override val i: X, - val yb: List, @Composable BarScope.() -> Unit>> + val yb: List, DefaultVerticalBarComposable>> ) : BarPlotGroupedPointEntry { override val d: List> = object : AbstractList>() { override val size: Int = yb.size @@ -131,8 +131,13 @@ public fun XYGraphScope.GroupedVerticalBarPlot( GroupedVerticalBarPlot( data, modifier, - { xIndex, seriesIndex, _ -> - data.data[xIndex].yb[seriesIndex].second.invoke(this) + { xIndex, seriesIndex, value -> + data.data[xIndex].yb[seriesIndex].second.invoke( + this, + xIndex, + seriesIndex, + GroupedEntryToVerticalEntryAdapter(value) + ) }, maxBarGroupWidth, startAnimationUseCase = startAnimationUseCase, @@ -177,15 +182,18 @@ public interface GroupedVerticalBarPlotScope { * bars in this series. */ public fun series( - defaultBar: @Composable BarScope.() -> Unit = solidBar(Color.Blue), + defaultBar: DefaultVerticalBarComposable = verticalSolidBar(Color.Blue), content: VerticalBarPlotScope.() -> Unit ) } private class GroupedVerticalBarPlotScopeImpl : GroupedVerticalBarPlotScope { val series: MutableList> = mutableListOf() - override fun series(defaultBar: @Composable BarScope.() -> Unit, content: VerticalBarPlotScope.() -> Unit) { - val scope = VerticalBarPlotScopeImpl(defaultBar) + override fun series( + defaultBar: DefaultVerticalBarComposable, + content: VerticalBarPlotScope.() -> Unit + ) { + val scope = VerticalBarPlotScopeImpl(defaultBar) series.add(scope) scope.content() } diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/bar/HorizontalBarPlot.kt b/src/commonMain/kotlin/io/github/koalaplot/core/bar/HorizontalBarPlot.kt index aa7239be5..30e561323 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/bar/HorizontalBarPlot.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/bar/HorizontalBarPlot.kt @@ -54,6 +54,14 @@ public fun horizontalBarPlotEntry(y: Y, xMin: X, xMax: X): HorizontalBarP */ public typealias HorizontalBarComposable = @Composable BarScope.(series: Int, index: Int, value: E) -> Unit +/** + * Defines a Composable function used to emit a horizontal bar for [HorizontalBarPlotEntry] values. + * Delegates to [HorizontalBarComposable] with [HorizontalBarPlotEntry] as type parameter. + * @param X The type of the x-axis values + * @param Y The type of the y-axis values + */ +public typealias DefaultHorizontalBarComposable = HorizontalBarComposable> + /** * A HorizontalBarPlot to be used in an XYGraph and that plots a single series of data points as horizontal bars. * @@ -70,7 +78,7 @@ public fun XYGraphScope.HorizontalBarPlot( xData: List, yData: List, modifier: Modifier = Modifier, - bar: @Composable BarScope.(index: Int) -> Unit, + bar: DefaultHorizontalBarComposable, barWidth: Float = 0.9f, startAnimationUseCase: StartAnimationUseCase = StartAnimationUseCase( @@ -106,7 +114,7 @@ public fun XYGraphScope.HorizontalBarPlot( public fun > XYGraphScope.HorizontalBarPlot( data: List, modifier: Modifier = Modifier, - bar: @Composable BarScope.(index: Int) -> Unit, + bar: DefaultHorizontalBarComposable, barWidth: Float = 0.9f, startAnimationUseCase: StartAnimationUseCase = StartAnimationUseCase( @@ -122,8 +130,8 @@ public fun > XYGraphScope.Horizonta GroupedHorizontalBarPlot( dataAdapter, modifier = modifier, - bar = { dataIndex, _, _ -> - bar(dataIndex) + bar = { series, index, value -> + bar(series, index, GroupedEntryToHorizontalEntryAdapter(value)) }, maxBarGroupWidth = barWidth, startAnimationUseCase = startAnimationUseCase @@ -151,6 +159,15 @@ private class HorizontalEntryToGroupedEntryAdapter(val entry: HorizontalBa } } +internal class GroupedEntryToHorizontalEntryAdapter( + private val entry: BarPlotGroupedPointEntry +) : HorizontalBarPlotEntry { + override val y: Y + get() = entry.i + override val x: BarPosition + get() = entry.d.first() +} + /** * Creates a Horizontal Bar Plot. * @@ -161,7 +178,7 @@ private class HorizontalEntryToGroupedEntryAdapter(val entry: HorizontalBa */ @Composable public fun XYGraphScope.HorizontalBarPlot( - defaultBar: @Composable BarScope.() -> Unit = solidBar(Color.Blue), + defaultBar: DefaultHorizontalBarComposable = horizontalSolidBar(Color.Blue), modifier: Modifier = Modifier, barWidth: Float = 0.9f, startAnimationUseCase: StartAnimationUseCase = @@ -172,7 +189,7 @@ public fun XYGraphScope.HorizontalBarPlot( ), content: HorizontalBarPlotScope.() -> Unit ) { - val scope = remember(content, defaultBar) { HorizontalBarPlotScopeImpl(defaultBar) } + val scope = remember(content, defaultBar) { HorizontalBarPlotScopeImpl(defaultBar) } val data = remember(scope) { scope.content() scope.data.values.toList() @@ -181,8 +198,8 @@ public fun XYGraphScope.HorizontalBarPlot( HorizontalBarPlot( data.map { it.first }, modifier, - { - data[it].second.invoke(this) + { series, index, value -> + data[index].second.invoke(this, series, index, value) }, barWidth, startAnimationUseCase @@ -198,15 +215,15 @@ public interface HorizontalBarPlotScope { * [xMin] to [xMax]. An optional [bar] can be provided to customize the Composable used to * generate the bar for this specific item. */ - public fun item(y: Y, xMin: X, xMax: X, bar: (@Composable BarScope.() -> Unit)? = null) + public fun item(y: Y, xMin: X, xMax: X, bar: (DefaultHorizontalBarComposable)? = null) } -internal class HorizontalBarPlotScopeImpl(private val defaultBar: @Composable BarScope.() -> Unit) : +internal class HorizontalBarPlotScopeImpl(private val defaultBar: DefaultHorizontalBarComposable) : HorizontalBarPlotScope { - val data: MutableMap, @Composable BarScope.() -> Unit>> = + val data: MutableMap, DefaultHorizontalBarComposable>> = mutableMapOf() - override fun item(y: Y, xMin: X, xMax: X, bar: (@Composable BarScope.() -> Unit)?) { + override fun item(y: Y, xMin: X, xMax: X, bar: DefaultHorizontalBarComposable?) { data[y] = Pair(horizontalBarPlotEntry(y, xMin, xMax), bar ?: defaultBar) } } diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/bar/StackedHorizontalBarPlot.kt b/src/commonMain/kotlin/io/github/koalaplot/core/bar/StackedHorizontalBarPlot.kt index 1ef8574ba..6d9cfcffc 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/bar/StackedHorizontalBarPlot.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/bar/StackedHorizontalBarPlot.kt @@ -47,7 +47,7 @@ public data class DefaultHorizontalBarPlotStackedPointEntry( public fun > XYGraphScope.StackedHorizontalBarPlot( data: List, modifier: Modifier = Modifier, - bar: @Composable BarScope.(yIndex: Int, barIndex: Int) -> Unit, + bar: DefaultHorizontalBarComposable, barWidth: Float = 0.9f, startAnimationUseCase: StartAnimationUseCase = StartAnimationUseCase( @@ -63,8 +63,8 @@ public fun > XYGraphScope - bar(index, barIndex) + { series, index, value -> + bar(index, barIndex, value) }, barWidth, startAnimationUseCase @@ -119,17 +119,17 @@ public fun XYGraphScope.StackedHorizontalBarPlot( /* chart animation */ KoalaPlotTheme.animationSpec, ), - content: StackedHorizontalBarPlotScope.() -> Unit + content: StackedHorizontalBarPlotScope.() -> Unit ) { val scope = remember(content) { - val scope = StackedHorizontalBarPlotScopeImpl() + val scope = StackedHorizontalBarPlotScopeImpl() scope.content() scope } data class EntryWithBars( override val y: Y, - val xb: List Unit>> + val xb: List>> ) : HorizontalBarPlotStackedPointEntry { override val xOrigin = 0f @@ -167,8 +167,8 @@ public fun XYGraphScope.StackedHorizontalBarPlot( StackedHorizontalBarPlot( data, modifier, - { yIndex, seriesIndex -> - data.data[yIndex].xb[seriesIndex].second.invoke(this) + { yIndex, seriesIndex, value -> + data.data[yIndex].xb[seriesIndex].second.invoke(this, yIndex, seriesIndex, value) }, barWidth, startAnimationUseCase @@ -178,24 +178,24 @@ public fun XYGraphScope.StackedHorizontalBarPlot( /** * Receiver scope used by [StackedHorizontalBarPlot]. */ -public interface StackedHorizontalBarPlotScope { +public interface StackedHorizontalBarPlotScope { /** * Starts a new series of bars to be plotted, with a [defaultBar] to use for rendering all * bars in this series. */ public fun series( - defaultBar: @Composable BarScope.() -> Unit = solidBar(Color.Blue), - content: StackedHorizontalBarPlotSeriesScope.() -> Unit + defaultBar: DefaultHorizontalBarComposable = horizontalSolidBar(Color.Blue), + content: StackedHorizontalBarPlotSeriesScope.() -> Unit ) } -private class StackedHorizontalBarPlotScopeImpl : StackedHorizontalBarPlotScope { - val series: MutableList> = mutableListOf() +private class StackedHorizontalBarPlotScopeImpl : StackedHorizontalBarPlotScope { + val series: MutableList> = mutableListOf() override fun series( - defaultBar: @Composable BarScope.() -> Unit, - content: StackedHorizontalBarPlotSeriesScope.() -> Unit + defaultBar: DefaultHorizontalBarComposable, + content: StackedHorizontalBarPlotSeriesScope.() -> Unit ) { - val scope = StackedHorizontalBarPlotSeriesScopeImpl(defaultBar) + val scope = StackedHorizontalBarPlotSeriesScopeImpl(defaultBar) series.add(scope) scope.content() } @@ -204,21 +204,21 @@ private class StackedHorizontalBarPlotScopeImpl : StackedHorizontalBarPlotSco /** * Scope item to allow adding items to a [StackedHorizontalBarPlot]. */ -public interface StackedHorizontalBarPlotSeriesScope { +public interface StackedHorizontalBarPlotSeriesScope { /** * Adds an item at the specified [y] axis coordinate, with a horizontal extent [x], which will * be added to series elements at the same y-axis coordinate already added to the plot. * An optional [bar] can be provided to customize the Composable used to * generate the bar for this specific item. */ - public fun item(x: Float, y: Y, bar: (@Composable BarScope.() -> Unit)? = null) + public fun item(x: Float, y: Y, bar: (DefaultHorizontalBarComposable)? = null) } -private class StackedHorizontalBarPlotSeriesScopeImpl(val defaultBar: @Composable BarScope.() -> Unit) : - StackedHorizontalBarPlotSeriesScope { - val data: MutableMap Unit>> = mutableMapOf() +private class StackedHorizontalBarPlotSeriesScopeImpl(val defaultBar: DefaultHorizontalBarComposable) : + StackedHorizontalBarPlotSeriesScope { + val data: MutableMap>> = mutableMapOf() - override fun item(x: Float, y: Y, bar: (@Composable BarScope.() -> Unit)?) { + override fun item(x: Float, y: Y, bar: (DefaultHorizontalBarComposable)?) { data[y] = Pair(x, bar ?: defaultBar) } } diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/bar/StackedVerticalBarPlot.kt b/src/commonMain/kotlin/io/github/koalaplot/core/bar/StackedVerticalBarPlot.kt index faf1c859e..38bf7bc30 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/bar/StackedVerticalBarPlot.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/bar/StackedVerticalBarPlot.kt @@ -47,7 +47,7 @@ public data class DefaultVerticalBarPlotStackedPointEntry( public fun > XYGraphScope.StackedVerticalBarPlot( data: List, modifier: Modifier = Modifier, - bar: @Composable BarScope.(xIndex: Int, barIndex: Int) -> Unit, + bar: DefaultVerticalBarComposable, barWidth: Float = 0.9f, startAnimationUseCase: StartAnimationUseCase = StartAnimationUseCase( @@ -63,8 +63,8 @@ public fun > XYGraphScope VerticalBarPlot( layerData, modifier, - { index -> - bar(index, barIndex) + { series, index, value -> + bar(index, barIndex, value) }, barWidth, startAnimationUseCase @@ -119,17 +119,17 @@ public fun XYGraphScope.StackedVerticalBarPlot( /* chart animation */ KoalaPlotTheme.animationSpec, ), - content: StackedVerticalBarPlotScope.() -> Unit + content: StackedVerticalBarPlotScope.() -> Unit ) { val scope = remember(content) { - val scope = StackedVerticalBarPlotScopeImpl() + val scope = StackedVerticalBarPlotScopeImpl() scope.content() scope } data class EntryWithBars( override val x: X, - val yb: List Unit>> + val yb: List>> ) : VerticalBarPlotStackedPointEntry { override val yOrigin = 0f @@ -167,8 +167,8 @@ public fun XYGraphScope.StackedVerticalBarPlot( StackedVerticalBarPlot( data, modifier, - { xIndex, seriesIndex -> - data.data[xIndex].yb[seriesIndex].second.invoke(this) + { xIndex, seriesIndex, value -> + data.data[xIndex].yb[seriesIndex].second.invoke(this, xIndex, seriesIndex, value) }, barWidth, startAnimationUseCase @@ -178,24 +178,24 @@ public fun XYGraphScope.StackedVerticalBarPlot( /** * Receiver scope used by [StackedVerticalBarPlot]. */ -public interface StackedVerticalBarPlotScope { +public interface StackedVerticalBarPlotScope { /** * Starts a new series of bars to be plotted, with a [defaultBar] to use for rendering all * bars in this series. */ public fun series( - defaultBar: @Composable BarScope.() -> Unit = solidBar(Color.Blue), - content: StackedVerticalBarPlotSeriesScope.() -> Unit + defaultBar: DefaultVerticalBarComposable = verticalSolidBar(Color.Blue), + content: StackedVerticalBarPlotSeriesScope.() -> Unit ) } -private class StackedVerticalBarPlotScopeImpl : StackedVerticalBarPlotScope { - val series: MutableList> = mutableListOf() +private class StackedVerticalBarPlotScopeImpl : StackedVerticalBarPlotScope { + val series: MutableList> = mutableListOf() override fun series( - defaultBar: @Composable BarScope.() -> Unit, - content: StackedVerticalBarPlotSeriesScope.() -> Unit + defaultBar: DefaultVerticalBarComposable, + content: StackedVerticalBarPlotSeriesScope.() -> Unit ) { - val scope = StackedVerticalBarPlotSeriesScopeImpl(defaultBar) + val scope = StackedVerticalBarPlotSeriesScopeImpl(defaultBar) series.add(scope) scope.content() } @@ -204,21 +204,21 @@ private class StackedVerticalBarPlotScopeImpl : StackedVerticalBarPlotScope { +public interface StackedVerticalBarPlotSeriesScope { /** * Adds an item at the specified [x] axis coordinate, with a vertical extent [y], which will * be added to series elements at the same x-axis coordinate already added to the plot. * An optional [bar] can be provided to customize the Composable used to * generate the bar for this specific item. */ - public fun item(x: X, y: Float, bar: (@Composable BarScope.() -> Unit)? = null) + public fun item(x: X, y: Float, bar: (DefaultVerticalBarComposable)? = null) } -private class StackedVerticalBarPlotSeriesScopeImpl(val defaultBar: @Composable BarScope.() -> Unit) : - StackedVerticalBarPlotSeriesScope { - val data: MutableMap Unit>> = mutableMapOf() +private class StackedVerticalBarPlotSeriesScopeImpl(val defaultBar: DefaultVerticalBarComposable) : + StackedVerticalBarPlotSeriesScope { + val data: MutableMap>> = mutableMapOf() - override fun item(x: X, y: Float, bar: (@Composable BarScope.() -> Unit)?) { + override fun item(x: X, y: Float, bar: (DefaultVerticalBarComposable)?) { data[x] = Pair(y, bar ?: defaultBar) } } diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/bar/VerticalBarPlot.kt b/src/commonMain/kotlin/io/github/koalaplot/core/bar/VerticalBarPlot.kt index 2819c35ca..96b0a21c4 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/bar/VerticalBarPlot.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/bar/VerticalBarPlot.kt @@ -54,6 +54,14 @@ public fun verticalBarPlotEntry(x: X, yMin: Y, yMax: Y): VerticalBarPlotE */ public typealias VerticalBarComposable = @Composable BarScope.(series: Int, index: Int, value: E) -> Unit +/** + * Defines a Composable function used to emit a vertical bar for [VerticalBarPlotEntry] values. + * Delegates to [VerticalBarComposable] with [VerticalBarPlotEntry] as type parameter. + * @param X The type of the x-axis values + * @param Y The type of the y-axis values + */ +public typealias DefaultVerticalBarComposable = VerticalBarComposable> + /** * A VerticalBarPlot to be used in an XYGraph and that plots a single series of data points as vertical bars. * @@ -70,7 +78,7 @@ public fun XYGraphScope.VerticalBarPlot( xData: List, yData: List, modifier: Modifier = Modifier, - bar: @Composable BarScope.(index: Int) -> Unit, + bar: DefaultVerticalBarComposable, barWidth: Float = 0.9f, startAnimationUseCase: StartAnimationUseCase = StartAnimationUseCase( @@ -106,7 +114,7 @@ public fun XYGraphScope.VerticalBarPlot( public fun > XYGraphScope.VerticalBarPlot( data: List, modifier: Modifier = Modifier, - bar: @Composable BarScope.(index: Int) -> Unit, + bar: DefaultVerticalBarComposable, barWidth: Float = 0.9f, startAnimationUseCase: StartAnimationUseCase = StartAnimationUseCase( @@ -123,8 +131,8 @@ public fun > XYGraphScope.VerticalBar GroupedVerticalBarPlot( dataAdapter, modifier = modifier, - bar = { dataIndex, _, _ -> - bar(dataIndex) + bar = { series, index, value -> + bar(series, index, GroupedEntryToVerticalEntryAdapter(value)) }, maxBarGroupWidth = barWidth, startAnimationUseCase = startAnimationUseCase @@ -152,6 +160,15 @@ private class VerticalEntryToGroupedEntryAdapter(val entry: VerticalBarPlo } } +internal class GroupedEntryToVerticalEntryAdapter( + private val entry: BarPlotGroupedPointEntry +) : VerticalBarPlotEntry { + override val x: X + get() = entry.i + override val y: BarPosition + get() = entry.d.first() +} + /** * Creates a Vertical Bar Plot. * @@ -162,7 +179,7 @@ private class VerticalEntryToGroupedEntryAdapter(val entry: VerticalBarPlo */ @Composable public fun XYGraphScope.VerticalBarPlot( - defaultBar: @Composable BarScope.() -> Unit = solidBar(Color.Blue), + defaultBar: DefaultVerticalBarComposable = verticalSolidBar(Color.Blue), modifier: Modifier = Modifier, barWidth: Float = 0.9f, startAnimationUseCase: StartAnimationUseCase = @@ -173,7 +190,7 @@ public fun XYGraphScope.VerticalBarPlot( ), content: VerticalBarPlotScope.() -> Unit ) { - val scope = remember(content, defaultBar) { VerticalBarPlotScopeImpl(defaultBar) } + val scope = remember(content, defaultBar) { VerticalBarPlotScopeImpl(defaultBar) } val data = remember(scope) { scope.content() scope.data.values.toList() @@ -182,8 +199,8 @@ public fun XYGraphScope.VerticalBarPlot( VerticalBarPlot( data.map { it.first }, modifier, - { - data[it].second.invoke(this) + { series, index, value -> + data[index].second.invoke(this, series, index, value) }, barWidth, startAnimationUseCase @@ -199,15 +216,15 @@ public interface VerticalBarPlotScope { * [yMin] to [yMax]. An optional [bar] can be provided to customize the Composable used to * generate the bar for this specific item. */ - public fun item(x: X, yMin: Y, yMax: Y, bar: (@Composable BarScope.() -> Unit)? = null) + public fun item(x: X, yMin: Y, yMax: Y, bar: (DefaultVerticalBarComposable)? = null) } -internal class VerticalBarPlotScopeImpl(private val defaultBar: @Composable BarScope.() -> Unit) : +internal class VerticalBarPlotScopeImpl(private val defaultBar: DefaultVerticalBarComposable) : VerticalBarPlotScope { - val data: MutableMap, @Composable BarScope.() -> Unit>> = + val data: MutableMap, DefaultVerticalBarComposable>> = mutableMapOf() - override fun item(x: X, yMin: Y, yMax: Y, bar: (@Composable BarScope.() -> Unit)?) { + override fun item(x: X, yMin: Y, yMax: Y, bar: (DefaultVerticalBarComposable)?) { data[x] = Pair(verticalBarPlotEntry(x, yMin, yMax), bar ?: defaultBar) } } diff --git a/src/desktopTest/kotlin/io/github/koalaplot/core/bar/VerticalBarChartTest.kt b/src/desktopTest/kotlin/io/github/koalaplot/core/bar/VerticalBarChartTest.kt index 5a8e2d196..c380ce378 100644 --- a/src/desktopTest/kotlin/io/github/koalaplot/core/bar/VerticalBarChartTest.kt +++ b/src/desktopTest/kotlin/io/github/koalaplot/core/bar/VerticalBarChartTest.kt @@ -29,7 +29,7 @@ class VerticalBarChartTest { DefaultBarPosition(0f, 10f) ) ), - bar = {} + bar = { _, _, _ -> } ) } }