From 171833dbd62ef36c63b838687d489b925794794c Mon Sep 17 00:00:00 2001 From: Peter Getek Date: Thu, 21 Aug 2025 08:00:47 +0200 Subject: [PATCH 1/4] Adding piechart shape implementation: - Concave/convex semicircle shaped endings - Nearly exact API as DefaultSlice except doesn't accept BorderStroke - Gaps can lead to negative sweep angles which cause rendering issues; only rendering positive angles --- .../koalaplot/core/pie/PieChartShapes.kt | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt b/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt new file mode 100644 index 000000000..2167a8269 --- /dev/null +++ b/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2025 Peter Artur Getek. + * All rights reserved. + */ + +package io.github.koalaplot.core.pie + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +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.Outline +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import io.github.koalaplot.core.util.ExperimentalKoalaPlotApi +import io.github.koalaplot.core.util.deg +import io.github.koalaplot.core.util.polarToCartesian +import io.github.koalaplot.core.util.rad +import io.github.koalaplot.core.util.toDegrees +import kotlin.math.abs +import kotlin.math.asin +import kotlin.math.max + +/** + * A semicircle shaped pie chart slice implementation that can form full slices as well as slices + * with a "hole" for donut charts. The endings of each slice consist of a concave and a convex shape. + * + * @receiver Provides drawing and interaction parameters for the slice scope + * @param color The Color of the Slice + * @param modifier The modifier to be applied to this item + * @param hoverExpandFactor Amount the slice expands when hovered. 1 is no expansion, values greater + * than 1 expand outward from the pie, and values less than 1 shrink. If expansion on hover is + * desired, a good starting value is 1.05. + * @param hoverElement Content to show when the mouse/pointer hovers over the slice + * @param clickable If clicking should be enabled. + * @param antiAlias Set to true if the slice should be drawn with anti-aliasing, false otherwise + * start/stop values the slice represents and where the slice is actually drawn. + * @param onClick handler of clicks on the slice + */ +@ExperimentalKoalaPlotApi +@Composable +public fun PieSliceScope.ConcaveConvexSlice( + color: Color, + modifier: Modifier = Modifier, + hoverExpandFactor: Float = 1.0f, + hoverElement: @Composable () -> Unit = {}, + clickable: Boolean = false, + antiAlias: Boolean = false, + gap: Float = 0.0f, + onClick: () -> Unit = {} +) { + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + val targetOuterRadius by animateFloatAsState(outerRadius * if (isHovered) hoverExpandFactor else 1f) + + val shape = ConcaveConvexSlice( + pieSliceData.startAngle.toDegrees().value.toFloat() + gap, + pieSliceData.angle.toDegrees().value.toFloat() - 2 * gap, + innerRadius, + targetOuterRadius + ) + + Box( + modifier = modifier.fillMaxSize() + .drawWithContent { + drawIntoCanvas { + val path = (shape.createOutline(size, layoutDirection, this) as Outline.Generic).path + + // draw slice + it.drawPath( + path, + Paint().apply { + isAntiAlias = antiAlias + this.color = color + } + ) + } + drawContent() + }.clip(shape) + .then( + if (clickable) { + Modifier.clickable( + enabled = true, + role = Role.Button, + onClick = onClick + ) + } else { + Modifier + } + ) + .hoverableElement(hoverElement) + .hoverable(interactionSource) + ) {} +} + +/** + * Creates a pie chart slice shape with a total angular extent of [angle] degrees with an + * optional holeSize that is specified as a percentage of the overall slice radius. + * The pie diameter is equal to the Shape's size width. The slice is positioned with its vertex + * at the center. + * + * The slice shape starts with a concave and ends with a convex shape. + */ +private class ConcaveConvexSlice( + private val startAngle: Float, + private val angle: Float, + private val innerRadius: Float = 0.5F, + private val outerRadius: Float = 1.0F +) : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val radius = size.width / 2F * outerRadius + val holeRadius = size.width / 2F * innerRadius + val center = Offset(size.width / 2F, size.width / 2F) + + val innerRect = Rect(center, holeRadius) + val outerRect = Rect(center, radius) + + // Gap can lead to negative sweep angle which causes rendering issues + val sweepAngle = max(0F,angle) + val innerCircleRadius = (radius - holeRadius) / 2F + val innerCircleCenterRadius = (radius + holeRadius) / 2F + + val innerCircleDegrees = + asin(innerCircleRadius / innerCircleCenterRadius).rad.toDegrees().value.toFloat() + + val concaveRingSlice = concaveRingSlice( + center = center, + innerRect = innerRect, + outerRect = outerRect, + startAngle = startAngle, + sweepAngle = sweepAngle, + innerCircleCenterRadius = innerCircleCenterRadius, + innerCircleDegrees = innerCircleDegrees, + innerCircleRadius = innerCircleRadius, + ) + + val ringSlice = ringSlice( + innerRect = innerRect, + outerRect = outerRect, + startAngle = startAngle, + sweepAngle = sweepAngle, + innerCircleDegrees = innerCircleDegrees + ) + + val convexRingSlice = convexRingSlice( + center = center, + startAngle = startAngle, + sweepAngle = sweepAngle, + innerCircleCenterRadius = innerCircleCenterRadius, + innerCircleDegrees = innerCircleDegrees, + innerCircleRadius = innerCircleRadius, + ) + + return Path().apply { + addPath(convexRingSlice) + addPath(ringSlice) + addPath(concaveRingSlice) + }.let(Outline::Generic) + } +} + +/** + * Path provider function for concave part of ring/donut slice. + * + * @param center The center of the pie chart. + * @param innerRect Rect corresponding to pie chart's hole. + * @param outerRect Rect corresponding to pie chart's outer radius. + * @param startAngle The start angle of the slice. + * @param sweepAngle The sweepAngle of the slice. + * @param innerCircleCenterRadius Radius pointing to average of outer and inner radius. + * @param innerCircleDegrees Angle from center which encompasses slice's inner circle. + * @param innerCircleRadius Radius of slice's inner circle. + */ +private fun concaveRingSlice( + center: Offset, + innerRect: Rect, + outerRect: Rect, + startAngle: Float, + sweepAngle: Float, + innerCircleCenterRadius: Float, + innerCircleDegrees: Float, + innerCircleRadius: Float, +): Path { + val deltaSmallSweepAngle = + if (sweepAngle < innerCircleDegrees) abs(sweepAngle - innerCircleDegrees) else 0F + + val toOuterStartAngleDegrees = startAngle - innerCircleDegrees / 2F + val toInnerStartAngleDegrees = startAngle + innerCircleDegrees / 2F - deltaSmallSweepAngle + val outerSweepAngleDegrees = innerCircleDegrees - deltaSmallSweepAngle + + val slice = Path().apply { + arcTo( + rect = outerRect, + startAngleDegrees = toOuterStartAngleDegrees, + sweepAngleDegrees = outerSweepAngleDegrees, + false + ) + arcTo( + rect = innerRect, + startAngleDegrees = toInnerStartAngleDegrees, + sweepAngleDegrees = -outerSweepAngleDegrees, + false + ) + } + + val toInnerCircleDegrees = startAngle - (innerCircleDegrees / 2F) + val innerCircleSweepAngleDegrees = 180F + + val convexSemicircle = Path().apply { + addArc( + oval = Rect( + center = center + polarToCartesian( + radius = innerCircleCenterRadius, + angle = toInnerCircleDegrees.deg + ), + radius = innerCircleRadius + ), + startAngleDegrees = toInnerCircleDegrees, + sweepAngleDegrees = innerCircleSweepAngleDegrees + ) + } + return slice - convexSemicircle +} + +/** + * Path provider function for ring part of ring/donut slice. + * Returns empty path if slice consists only of concave/convex pieces. + * + * @param innerRect Rect corresponding to pie chart's hole. + * @param outerRect Rect corresponding to pie chart's outer radius. + * @param startAngle The start angle of the slice. + * @param sweepAngle The sweepAngle of the slice. + * @param innerCircleDegrees Angle from center which encompasses slice's inner circle. + */ +private fun ringSlice( + innerRect: Rect, + outerRect: Rect, + startAngle: Float, + sweepAngle: Float, + innerCircleDegrees: Float +): Path { + if (sweepAngle <= innerCircleDegrees) return Path() + + val toOuterStartAngleDegrees = startAngle + (innerCircleDegrees / 2F) + val toInnerStartAngleDegrees = startAngle + sweepAngle - innerCircleDegrees / 2F + val outerSweepAngleDegrees = sweepAngle - innerCircleDegrees + + return Path().apply { + addArc( + oval = outerRect, + startAngleDegrees = toOuterStartAngleDegrees, + sweepAngleDegrees = outerSweepAngleDegrees + ) + arcTo( + rect = innerRect, + startAngleDegrees = toInnerStartAngleDegrees, + sweepAngleDegrees = -outerSweepAngleDegrees, + forceMoveTo = false + ) + } +} + +/** + * Path provider function for convex part of ring/donut slice. + * + * @param center The center of the pie chart. + * @param startAngle The start angle of the slice. + * @param sweepAngle The sweepAngle of the slice. + * @param innerCircleCenterRadius Radius pointing to average of outer and inner radius. + * @param innerCircleDegrees Angle from center which encompasses slice's inner circle. + * @param innerCircleRadius Radius of slice's inner circle. + */ +private fun convexRingSlice( + center: Offset, + startAngle: Float, + sweepAngle: Float, + innerCircleCenterRadius: Float, + innerCircleDegrees: Float, + innerCircleRadius: Float, +): Path { + val toInnerCircleDegrees = (startAngle + sweepAngle - innerCircleDegrees / 2F) + val innerCircleSweepAngleDegrees = 180F + + val convexSemicircle = Path().apply { + addArc( + oval = Rect( + center = center + polarToCartesian( + radius = innerCircleCenterRadius, + angle = toInnerCircleDegrees.deg + ), + radius = innerCircleRadius + ), + startAngleDegrees = toInnerCircleDegrees, + sweepAngleDegrees = innerCircleSweepAngleDegrees + ) + } + + if (sweepAngle < innerCircleDegrees) { + val toConcaveInnerCircleDegrees = startAngle - (innerCircleDegrees / 2F) + val concaveSemicircle = Path().apply { + addArc( + oval = Rect( + center = center + polarToCartesian( + radius = innerCircleCenterRadius, + angle = toConcaveInnerCircleDegrees.deg + ), + radius = innerCircleRadius + ), + startAngleDegrees = toConcaveInnerCircleDegrees, + sweepAngleDegrees = innerCircleSweepAngleDegrees + ) + } + + return convexSemicircle - concaveSemicircle + } + return convexSemicircle +} From 67c2f7a9dd54d5d6816afffd58231f8bbdd6104a Mon Sep 17 00:00:00 2001 From: Peter Getek Date: Thu, 21 Aug 2025 13:47:06 +0200 Subject: [PATCH 2/4] removing copyright tag --- .../kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt b/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt index 2167a8269..8b16c52fa 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt @@ -1,8 +1,3 @@ -/* - * Copyright (c) 2025 Peter Artur Getek. - * All rights reserved. - */ - package io.github.koalaplot.core.pie import androidx.compose.animation.core.animateFloatAsState From 3108590eba00be3093447191477367d39e8b9198 Mon Sep 17 00:00:00 2001 From: Peter Getek Date: Fri, 22 Aug 2025 07:17:54 +0200 Subject: [PATCH 3/4] fixing minor formatting issue --- .../kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt b/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt index 8b16c52fa..f85d642b5 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt @@ -133,7 +133,7 @@ private class ConcaveConvexSlice( val outerRect = Rect(center, radius) // Gap can lead to negative sweep angle which causes rendering issues - val sweepAngle = max(0F,angle) + val sweepAngle = max(0F, angle) val innerCircleRadius = (radius - holeRadius) / 2F val innerCircleCenterRadius = (radius + holeRadius) / 2F From 58213aec6ac509e9665821b657b382209bf3e401 Mon Sep 17 00:00:00 2001 From: Peter Getek Date: Sat, 30 Aug 2025 15:19:59 +0200 Subject: [PATCH 4/4] Commit fixes issues as specified in review comments: - adding gap argument comment - extracting inner circle sweep angle constant - slices don't expand on hover event - fixing detekt's argument number constraint by introducing parameter data classes - gap cannot be negative `require` constraint --- .../koalaplot/core/pie/PieChartShapes.kt | 130 ++++++++++-------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt b/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt index f85d642b5..fdbda80f7 100644 --- a/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt +++ b/src/commonMain/kotlin/io/github/koalaplot/core/pie/PieChartShapes.kt @@ -1,15 +1,9 @@ package io.github.koalaplot.core.pie -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable -import androidx.compose.foundation.hoverable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent @@ -41,13 +35,11 @@ import kotlin.math.max * @receiver Provides drawing and interaction parameters for the slice scope * @param color The Color of the Slice * @param modifier The modifier to be applied to this item - * @param hoverExpandFactor Amount the slice expands when hovered. 1 is no expansion, values greater - * than 1 expand outward from the pie, and values less than 1 shrink. If expansion on hover is - * desired, a good starting value is 1.05. * @param hoverElement Content to show when the mouse/pointer hovers over the slice * @param clickable If clicking should be enabled. * @param antiAlias Set to true if the slice should be drawn with anti-aliasing, false otherwise - * start/stop values the slice represents and where the slice is actually drawn. + * @param gap Specifies the gap between slices. It is the angular distance, in degrees, between the + * start/stop values the slice represents and where the slice is actually drawn. Cannot be negative. * @param onClick handler of clicks on the slice */ @ExperimentalKoalaPlotApi @@ -55,22 +47,18 @@ import kotlin.math.max public fun PieSliceScope.ConcaveConvexSlice( color: Color, modifier: Modifier = Modifier, - hoverExpandFactor: Float = 1.0f, hoverElement: @Composable () -> Unit = {}, clickable: Boolean = false, antiAlias: Boolean = false, gap: Float = 0.0f, onClick: () -> Unit = {} ) { - val interactionSource = remember { MutableInteractionSource() } - val isHovered by interactionSource.collectIsHoveredAsState() - val targetOuterRadius by animateFloatAsState(outerRadius * if (isHovered) hoverExpandFactor else 1f) - + require(gap >= 0F) { "gap cannot be negative" } val shape = ConcaveConvexSlice( pieSliceData.startAngle.toDegrees().value.toFloat() + gap, pieSliceData.angle.toDegrees().value.toFloat() - 2 * gap, innerRadius, - targetOuterRadius + outerRadius ) Box( @@ -102,7 +90,6 @@ public fun PieSliceScope.ConcaveConvexSlice( } ) .hoverableElement(hoverElement) - .hoverable(interactionSource) ) {} } @@ -131,6 +118,11 @@ private class ConcaveConvexSlice( val innerRect = Rect(center, holeRadius) val outerRect = Rect(center, radius) + val layout = Layout( + center = center, + innerRect = innerRect, + outerRect = outerRect + ) // Gap can lead to negative sweep angle which causes rendering issues val sweepAngle = max(0F, angle) @@ -139,33 +131,31 @@ private class ConcaveConvexSlice( val innerCircleDegrees = asin(innerCircleRadius / innerCircleCenterRadius).rad.toDegrees().value.toFloat() + val innerCircle = InnerCircle( + innerCircleCenterRadius = innerCircleCenterRadius, + innerCircleDegrees = innerCircleDegrees, + innerCircleRadius = innerCircleRadius + ) val concaveRingSlice = concaveRingSlice( - center = center, - innerRect = innerRect, - outerRect = outerRect, + layout = layout, startAngle = startAngle, sweepAngle = sweepAngle, - innerCircleCenterRadius = innerCircleCenterRadius, - innerCircleDegrees = innerCircleDegrees, - innerCircleRadius = innerCircleRadius, + innerCircle = innerCircle ) val ringSlice = ringSlice( - innerRect = innerRect, - outerRect = outerRect, + layout = layout, startAngle = startAngle, sweepAngle = sweepAngle, - innerCircleDegrees = innerCircleDegrees + innerCircle = innerCircle ) val convexRingSlice = convexRingSlice( - center = center, + layout = layout, startAngle = startAngle, sweepAngle = sweepAngle, - innerCircleCenterRadius = innerCircleCenterRadius, - innerCircleDegrees = innerCircleDegrees, - innerCircleRadius = innerCircleRadius, + innerCircle = innerCircle ) return Path().apply { @@ -176,28 +166,24 @@ private class ConcaveConvexSlice( } } +private const val InnerCircleSweepAngleDegrees = 180F + /** * Path provider function for concave part of ring/donut slice. * - * @param center The center of the pie chart. - * @param innerRect Rect corresponding to pie chart's hole. - * @param outerRect Rect corresponding to pie chart's outer radius. + * @param layout Specifies layout of pie chart. * @param startAngle The start angle of the slice. * @param sweepAngle The sweepAngle of the slice. - * @param innerCircleCenterRadius Radius pointing to average of outer and inner radius. - * @param innerCircleDegrees Angle from center which encompasses slice's inner circle. - * @param innerCircleRadius Radius of slice's inner circle. + * @param innerCircle Specifies shape of concave/convex part of ring/donut slice. */ private fun concaveRingSlice( - center: Offset, - innerRect: Rect, - outerRect: Rect, + layout: Layout, startAngle: Float, sweepAngle: Float, - innerCircleCenterRadius: Float, - innerCircleDegrees: Float, - innerCircleRadius: Float, + innerCircle: InnerCircle ): Path { + val (center, innerRect, outerRect) = layout + val (innerCircleCenterRadius, innerCircleDegrees, innerCircleRadius) = innerCircle val deltaSmallSweepAngle = if (sweepAngle < innerCircleDegrees) abs(sweepAngle - innerCircleDegrees) else 0F @@ -221,7 +207,6 @@ private fun concaveRingSlice( } val toInnerCircleDegrees = startAngle - (innerCircleDegrees / 2F) - val innerCircleSweepAngleDegrees = 180F val convexSemicircle = Path().apply { addArc( @@ -233,7 +218,7 @@ private fun concaveRingSlice( radius = innerCircleRadius ), startAngleDegrees = toInnerCircleDegrees, - sweepAngleDegrees = innerCircleSweepAngleDegrees + sweepAngleDegrees = InnerCircleSweepAngleDegrees ) } return slice - convexSemicircle @@ -243,19 +228,19 @@ private fun concaveRingSlice( * Path provider function for ring part of ring/donut slice. * Returns empty path if slice consists only of concave/convex pieces. * - * @param innerRect Rect corresponding to pie chart's hole. - * @param outerRect Rect corresponding to pie chart's outer radius. + * @param layout Specifies layout of pie chart. * @param startAngle The start angle of the slice. * @param sweepAngle The sweepAngle of the slice. - * @param innerCircleDegrees Angle from center which encompasses slice's inner circle. + * @param innerCircle Specifies shape of concave/convex part of ring/donut slice. */ private fun ringSlice( - innerRect: Rect, - outerRect: Rect, + layout: Layout, startAngle: Float, sweepAngle: Float, - innerCircleDegrees: Float + innerCircle: InnerCircle ): Path { + val (_, innerRect, outerRect) = layout + val (_, innerCircleDegrees, _) = innerCircle if (sweepAngle <= innerCircleDegrees) return Path() val toOuterStartAngleDegrees = startAngle + (innerCircleDegrees / 2F) @@ -280,23 +265,20 @@ private fun ringSlice( /** * Path provider function for convex part of ring/donut slice. * - * @param center The center of the pie chart. + * @param layout Specifies layout of pie chart. * @param startAngle The start angle of the slice. * @param sweepAngle The sweepAngle of the slice. - * @param innerCircleCenterRadius Radius pointing to average of outer and inner radius. - * @param innerCircleDegrees Angle from center which encompasses slice's inner circle. - * @param innerCircleRadius Radius of slice's inner circle. + * @param innerCircle Specifies shape of concave/convex part of ring/donut slice. */ private fun convexRingSlice( - center: Offset, + layout: Layout, startAngle: Float, sweepAngle: Float, - innerCircleCenterRadius: Float, - innerCircleDegrees: Float, - innerCircleRadius: Float, + innerCircle: InnerCircle ): Path { + val (center, _, _) = layout + val (innerCircleCenterRadius, innerCircleDegrees, innerCircleRadius) = innerCircle val toInnerCircleDegrees = (startAngle + sweepAngle - innerCircleDegrees / 2F) - val innerCircleSweepAngleDegrees = 180F val convexSemicircle = Path().apply { addArc( @@ -308,7 +290,7 @@ private fun convexRingSlice( radius = innerCircleRadius ), startAngleDegrees = toInnerCircleDegrees, - sweepAngleDegrees = innerCircleSweepAngleDegrees + sweepAngleDegrees = InnerCircleSweepAngleDegrees ) } @@ -324,7 +306,7 @@ private fun convexRingSlice( radius = innerCircleRadius ), startAngleDegrees = toConcaveInnerCircleDegrees, - sweepAngleDegrees = innerCircleSweepAngleDegrees + sweepAngleDegrees = InnerCircleSweepAngleDegrees ) } @@ -332,3 +314,29 @@ private fun convexRingSlice( } return convexSemicircle } + +/** + * Parameter class specifying layout of concave/convex shaped pie chart slices. + * + * @param center The center of the pie chart. + * @param innerRect Rect corresponding to pie chart's hole. + * @param outerRect Rect corresponding to pie chart's outer radius. + */ +private data class Layout( + val center: Offset, + val innerRect: Rect, + val outerRect: Rect +) + +/** + * Parameter class providing inner circle values specifying shape of concave/convex part of ring/donut slice. + * + * @param innerCircleCenterRadius Radius pointing to average of outer and inner radius. + * @param innerCircleDegrees Angle from center which encompasses slice's inner circle. + * @param innerCircleRadius Radius of slice's inner circle. + */ +private data class InnerCircle( + val innerCircleCenterRadius: Float, + val innerCircleDegrees: Float, + val innerCircleRadius: Float, +)