Skip to content

Commit

Permalink
Rename PolarPlot to PolarGraph, add configurable angular axis directi…
Browse files Browse the repository at this point in the history
…on and origin.
  • Loading branch information
gsteckman committed Nov 26, 2023
1 parent 25faea6 commit 9e48a80
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.github.koalaplot.core.polar

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.github.koalaplot.core.util.AngularValue
import io.github.koalaplot.core.util.rad
import kotlin.math.PI
Expand All @@ -10,6 +12,26 @@ import kotlin.math.PI
* @param T The data type for the axis values.
*/
public interface AngularAxisModel<T> {
/**
* Defines the direction for incrementing angles on a [PolarGraph]. Counter clockwise is the regular convention for
* mathematics.
*/
public enum class AngleDirection {
CLOCKWISE,
COUNTERCLOCKWISE
}

/**
* Defines the orientation of the Zero angle on a [PolarGraph]. 3 O'Clock is the regular convention for
* mathematics, while 12 O'Clock is typical for category based charts and spider charts.
*/
public enum class AngleZero {
THREE_OCLOCK,
SIX_OCLOCK,
NINE_OCLOCK,
TWELVE_OCLOCK,
}

/**
* Gets the tick values for this axis model.
*/
Expand All @@ -22,6 +44,16 @@ public interface AngularAxisModel<T> {
* their angular position on the plot.
*/
public fun computeOffset(point: T): AngularValue

/**
* The [AngleDirection] for this axis.
*/
public val angleDirection: AngleDirection

/**
* The [AngleZero] for this axis.
*/
public val angleZero: AngleZero
}

/**
Expand All @@ -30,7 +62,11 @@ public interface AngularAxisModel<T> {
*
* @param categories The category values represented by this axis.
*/
public data class CategoryAngularAxisModel<T>(private val categories: List<T>) : AngularAxisModel<T> {
public data class CategoryAngularAxisModel<T>(
private val categories: List<T>,
public override val angleDirection: AngularAxisModel.AngleDirection = AngularAxisModel.AngleDirection.CLOCKWISE,
public override val angleZero: AngularAxisModel.AngleZero = AngularAxisModel.AngleZero.TWELVE_OCLOCK
) : AngularAxisModel<T> {
override fun getTickValues(): List<T> = categories

override fun computeOffset(point: T): AngularValue {
Expand All @@ -40,6 +76,19 @@ public data class CategoryAngularAxisModel<T>(private val categories: List<T>) :
}
}

/**
* Creates and remembers a [CategoryAngularAxisModel].
*/
@Composable
public fun <T> rememberCategoryAngularAxisModel(
categories: List<T>,
angleDirection: AngularAxisModel.AngleDirection = AngularAxisModel.AngleDirection.CLOCKWISE,
angleZero: AngularAxisModel.AngleZero = AngularAxisModel.AngleZero.TWELVE_OCLOCK
): CategoryAngularAxisModel<T> =
remember(categories, angleDirection, angleZero) {
CategoryAngularAxisModel(categories, angleDirection, angleZero)
}

/**
* An [AngularAxisModel] that uses [AngularValue]s of either Radians or Degrees.
*
Expand All @@ -51,9 +100,24 @@ public data class AngularValueAxisModel(
for (i in 0..<8) {
add((PI * i / 4.0).rad)
}
}
},
override val angleDirection: AngularAxisModel.AngleDirection = AngularAxisModel.AngleDirection.COUNTERCLOCKWISE,
override val angleZero: AngularAxisModel.AngleZero = AngularAxisModel.AngleZero.THREE_OCLOCK
) : AngularAxisModel<AngularValue> {
override fun getTickValues(): List<AngularValue> = tickValues

override fun computeOffset(point: AngularValue): AngularValue = point
}

@Composable
public fun rememberAngularValueAxisModel(
tickValues: List<AngularValue> = buildList {
@Suppress("MagicNumber")
for (i in 0..<8) {
add((PI * i / 4.0).rad)
}
},
angleDirection: AngularAxisModel.AngleDirection = AngularAxisModel.AngleDirection.COUNTERCLOCKWISE,
angleZero: AngularAxisModel.AngleZero = AngularAxisModel.AngleZero.THREE_OCLOCK
): AngularValueAxisModel =
remember(tickValues, angleDirection, angleZero) { AngularValueAxisModel(tickValues, angleDirection, angleZero) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.github.koalaplot.core.polar

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember

/**
* A model for a radial axis that transforms from axis space to drawing space.
*/
public interface RadialAxisModel<T> {
/**
* Provides a list of tick values that should be rendered for the axis.
*/
public val tickValues: List<T>

/**
* Transforms the provided [point] in this axis space to the fraction representing the point's distance
* from the origin relative to the overall axis length.
*/
public fun computeOffset(point: T): Float
}

/**
* A model for a radial axis that uses Float values and is linear.
*
* @param tickValues Values for each tick on the axis. Must have at least 2 values.
*/
public class FloatRadialAxisModel constructor(
override val tickValues: List<Float>,
) : RadialAxisModel<Float> {
init {
require(tickValues.size >= 2) { "tickValues must have at least 2 values " }
}

private val sortedTickValues = tickValues.sorted()
private val range = sortedTickValues.last() - sortedTickValues.first()

public override fun computeOffset(point: Float): Float {
return (point - sortedTickValues.first()) / range
}
}

@Composable
public fun rememberFloatRadialAxisModel(tickValues: List<Float>): FloatRadialAxisModel =
remember(tickValues) { FloatRadialAxisModel(tickValues) }
52 changes: 26 additions & 26 deletions src/commonMain/kotlin/io/github/koalaplot/core/polar/Grid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,32 @@ import kotlin.math.min
* restrict its size to the required diameter.
*/
@Composable
internal fun <T> PolarPlotScope<T>.Grid(
polarPlotProperties: PolarPlotProperties,
internal fun <T> PolarGraphScope<T>.Grid(
polarGraphProperties: PolarGraphProperties,
) {
Canvas(modifier = Modifier.fillMaxSize()) {
if (polarPlotProperties.background != null) {
val backgroundPath = generateGridBoundaryPath(size, polarPlotProperties.radialGridType)
if (polarGraphProperties.background != null) {
val backgroundPath = generateGridBoundaryPath(size, polarGraphProperties.radialGridType)

drawPath(
backgroundPath,
polarPlotProperties.background.brush,
polarPlotProperties.background.alpha,
polarGraphProperties.background.brush,
polarGraphProperties.background.alpha,
Fill,
polarPlotProperties.background.colorFilter,
polarPlotProperties.background.blendMode
polarGraphProperties.background.colorFilter,
polarGraphProperties.background.blendMode
)
}

drawRadialGridLines(
this@Grid,
polarPlotProperties.radialAxisGridLineStyle,
polarPlotProperties.radialGridType,
polarGraphProperties.radialAxisGridLineStyle,
polarGraphProperties.radialGridType,
)

drawAngularGridLines(
this@Grid,
polarPlotProperties.angularAxisGridLineStyle
polarGraphProperties.angularAxisGridLineStyle
)
}
}
Expand All @@ -55,35 +55,35 @@ internal fun <T> PolarPlotScope<T>.Grid(
* Draws radial grid lines/circles
*/
private fun <T> DrawScope.drawRadialGridLines(
polarPlotScope: PolarPlotScope<T>,
polarGraphScope: PolarGraphScope<T>,
style: LineStyle?,
radialGridType: RadialGridType
) {
val radii = polarPlotScope.radialAxisModel.tickValues.map { polarPlotScope.radialAxisModel.computeOffset(it) }
val radii = polarGraphScope.radialAxisModel.tickValues.map { polarGraphScope.radialAxisModel.computeOffset(it) }
if (radii.isEmpty()) return

if (style != null) {
if (radialGridType == RadialGridType.CIRCLES) {
drawCircularRadialGridLines(polarPlotScope.radialAxisModel, style)
drawCircularRadialGridLines(polarGraphScope.radialAxisModel, style)
} else {
drawStraightRadialGridLines(polarPlotScope, style)
drawStraightRadialGridLines(polarGraphScope, style)
}
}
}

private fun <T> DrawScope.drawStraightRadialGridLines(
polarPlotScope: PolarPlotScope<T>,
polarGraphScope: PolarGraphScope<T>,
style: LineStyle
) {
val angles = polarPlotScope.angularAxisModel.getTickValues()
val radii = polarPlotScope.radialAxisModel.tickValues
val angles = polarGraphScope.angularAxisModel.getTickValues()
val radii = polarGraphScope.radialAxisModel.tickValues

radii.forEach { radius ->
var startAngle = angles.last()
for (angleIndex in 0..angles.lastIndex) {
drawLine(
start = polarPlotScope.polarToCartesian(PolarPoint(radius, startAngle), size),
end = polarPlotScope.polarToCartesian(PolarPoint(radius, angles[angleIndex]), size),
start = polarGraphScope.polarToCartesian(PolarPoint(radius, startAngle), size),
end = polarGraphScope.polarToCartesian(PolarPoint(radius, angles[angleIndex]), size),
brush = style.brush,
strokeWidth = style.strokeWidth.toPx(),
pathEffect = style.pathEffect,
Expand All @@ -97,7 +97,7 @@ private fun <T> DrawScope.drawStraightRadialGridLines(
}

private fun DrawScope.drawCircularRadialGridLines(
radialAxisModel: RadialAxisModel,
radialAxisModel: FloatRadialAxisModel,
style: LineStyle,
) {
val radii = radialAxisModel.tickValues.map { radialAxisModel.computeOffset(it) }
Expand All @@ -116,18 +116,18 @@ private fun DrawScope.drawCircularRadialGridLines(
}

private fun <T> DrawScope.drawAngularGridLines(
polarPlotScope: PolarPlotScope<T>,
polarGraphScope: PolarGraphScope<T>,
style: LineStyle?
) {
if (style == null) return

val radius = polarPlotScope.radialAxisModel.tickValues.last()
val angles = polarPlotScope.angularAxisModel.getTickValues()
val radius = polarGraphScope.radialAxisModel.tickValues.last()
val angles = polarGraphScope.angularAxisModel.getTickValues()

angles.forEach { angle ->
drawLine(
start = Offset(0f, 0f),
end = polarPlotScope.polarToCartesian(PolarPoint(radius, angle), size),
end = polarGraphScope.polarToCartesian(PolarPoint(radius, angle), size),
brush = style.brush,
strokeWidth = style.strokeWidth.toPx(),
pathEffect = style.pathEffect,
Expand All @@ -142,7 +142,7 @@ private fun <T> DrawScope.drawAngularGridLines(
* Create a path that is the boundary of the grid, depending on the radial grid type. This path is used for
* setting the clip boundary at the edge of the grid, as well as for drawing the background.
*/
internal fun <T> PolarPlotScope<T>.generateGridBoundaryPath(
internal fun <T> PolarGraphScope<T>.generateGridBoundaryPath(
size: Size,
type: RadialGridType
): Path {
Expand Down
Loading

0 comments on commit 9e48a80

Please sign in to comment.