-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #99 from pakka-papad/feat/radar-chart
Added: Radar Chart
- Loading branch information
Showing
6 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
243 changes: 243 additions & 0 deletions
243
charty/src/main/java/com/himanshoe/charty/radar/RadarChart.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
package com.himanshoe.charty.radar | ||
|
||
import android.graphics.Paint | ||
import android.graphics.PointF | ||
import androidx.compose.foundation.Canvas | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.drawBehind | ||
import androidx.compose.ui.geometry.Offset | ||
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.nativeCanvas | ||
import androidx.compose.ui.graphics.toArgb | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.dp | ||
import androidx.compose.ui.util.fastForEach | ||
import androidx.compose.ui.util.fastForEachIndexed | ||
import com.himanshoe.charty.common.ChartDataCollection | ||
import com.himanshoe.charty.common.config.AxisConfig | ||
import com.himanshoe.charty.common.config.ChartDefaults | ||
import com.himanshoe.charty.common.maxYValue | ||
import com.himanshoe.charty.common.minYValue | ||
import com.himanshoe.charty.radar.config.RadarChartColors | ||
import com.himanshoe.charty.radar.config.RadarChartDefaults | ||
import com.himanshoe.charty.radar.config.RadarConfig | ||
import com.himanshoe.charty.radar.model.RadarData | ||
import kotlin.math.abs | ||
import kotlin.math.cos | ||
import kotlin.math.sin | ||
|
||
/** | ||
* A composable function that displays a radar chart. | ||
* | ||
* @param dataCollection The collection of chart data points. | ||
* @param modifier The modifier for the chart. | ||
* @param axisConfig The configuration for the chart's axes. | ||
* @param radarConfig The configuration for the polygon in the chart. | ||
* @param chartColors The colors used in the chart. | ||
* @param radiusScale The scale factor for the radius of the data points. | ||
*/ | ||
@Composable | ||
fun RadarChart( | ||
dataCollection: ChartDataCollection, | ||
modifier: Modifier = Modifier, | ||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(), | ||
radarConfig: RadarConfig = RadarChartDefaults.defaultConfig(), | ||
chartColors: RadarChartColors = RadarChartDefaults.defaultColor(), | ||
radiusScale: Float = 0.02f, | ||
) { | ||
val maxYValue by remember { mutableStateOf(dataCollection.maxYValue()) } | ||
val minYValue by remember { mutableStateOf(dataCollection.minYValue()) } | ||
val diff = abs(maxYValue * 0.1F) | ||
val dataRange = (minYValue - diff)..(maxYValue + diff) | ||
val partitionAngle = (2 * Math.PI / dataCollection.data.size).toFloat() | ||
|
||
Canvas( | ||
modifier = modifier | ||
.drawBehind { | ||
drawRadarAxis( | ||
dataCollection = dataCollection, | ||
dataRange = dataRange, | ||
radius = size.minDimension / 3, | ||
center = PointF(size.width / 2, size.height / 2), | ||
axisConfig = axisConfig, | ||
) | ||
} | ||
) { | ||
val radius = size.minDimension / 3 | ||
val centerX = size.width / 2 | ||
val centerY = size.height / 2 | ||
|
||
val scaleFactor = radius / (dataRange.endInclusive - dataRange.start) | ||
|
||
val lineColor = Brush.linearGradient(chartColors.lineColor) | ||
val dotColor = Brush.linearGradient(chartColors.dotColor) | ||
|
||
val radarPolygonVertices = mutableListOf<PointF>() | ||
|
||
Path().apply { | ||
dataCollection.data.fastForEachIndexed { index, chartData -> | ||
val angle = index * partitionAngle | ||
val x = centerX + scaleFactor * (chartData.yValue - dataRange.start) * cos(angle) | ||
val y = centerY + scaleFactor * (chartData.yValue - dataRange.start) * sin(angle) | ||
if (index == 0) { | ||
moveTo(x, y) | ||
} else { | ||
lineTo(x, y) | ||
} | ||
|
||
radarPolygonVertices.add(PointF(x, y)) | ||
} | ||
close() | ||
|
||
drawPath( | ||
path = this, | ||
brush = lineColor, | ||
style = Stroke(width = radarConfig.strokeSize) | ||
) | ||
if (radarConfig.fillPolygon) { | ||
drawPath( | ||
path = this, | ||
brush = Brush.linearGradient(chartColors.fillColor), | ||
) | ||
} | ||
} | ||
|
||
if (radarConfig.hasDotMarker) { | ||
radarPolygonVertices.fastForEach { | ||
drawCircle( | ||
brush = dotColor, | ||
radius = radiusScale * size.width, | ||
center = Offset(it.x, it.y) | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun DrawScope.drawRadarAxis( | ||
dataCollection: ChartDataCollection, | ||
dataRange: ClosedFloatingPointRange<Float>, | ||
radius: Float, | ||
center: PointF, | ||
axisConfig: AxisConfig, | ||
) { | ||
val partitionAngle = (2 * Math.PI / dataCollection.data.size).toFloat() | ||
val axisPolygons = if (axisConfig.showGridLines) { | ||
listOf(Path(), Path(), Path(), Path()) | ||
} else { | ||
emptyList() | ||
} | ||
|
||
for (spokeIndex in 0 until dataCollection.data.size) { | ||
val angle = spokeIndex * partitionAngle | ||
|
||
if (axisConfig.showAxes) { | ||
drawLine( | ||
start = Offset(center.x, center.y), | ||
end = Offset(center.x + radius * cos(angle), center.y + radius * sin(angle)), | ||
color = axisConfig.axisColor, | ||
strokeWidth = axisConfig.axisStroke | ||
) | ||
} | ||
|
||
drawContext.canvas.nativeCanvas.drawText( | ||
dataCollection.data[spokeIndex].xValue.toString(), | ||
center.x + (radius + 30F) * cos(angle), | ||
center.y + (radius + 30F) * sin(angle), | ||
Paint().apply { | ||
this.color = axisConfig.axisColor.toArgb() | ||
this.textSize = size.width / 30 | ||
this.textAlign = if (angle > Math.PI / 2 && angle < 3 * Math.PI / 2) { | ||
Paint.Align.RIGHT | ||
} else { | ||
Paint.Align.LEFT | ||
} | ||
} | ||
) | ||
|
||
axisPolygons.fastForEachIndexed { i, path -> | ||
val scale = 1F - 0.25F * i | ||
if (spokeIndex == 0) { | ||
path.moveTo(center.x + scale * radius * cos(angle), center.y + scale * radius * sin(angle)) | ||
} else { | ||
path.lineTo(center.x + scale * radius * cos(angle), center.y + scale * radius * sin(angle)) | ||
} | ||
} | ||
} | ||
|
||
if (!axisConfig.showGridLines) return | ||
|
||
axisPolygons.fastForEach { | ||
it.close() | ||
drawPath( | ||
path = it, | ||
color = axisConfig.gridColor, | ||
style = Stroke(width = axisConfig.axisStroke) | ||
) | ||
} | ||
|
||
if (!axisConfig.showGridLabel) return | ||
|
||
drawGridLabels( | ||
radius = radius, | ||
center = center, | ||
dataRange = dataRange, | ||
angle = (partitionAngle / 2), | ||
labelCount = axisPolygons.size, | ||
labelColor = axisConfig.gridColor | ||
) | ||
} | ||
|
||
private fun DrawScope.drawGridLabels( | ||
radius: Float, | ||
center: PointF, | ||
dataRange: ClosedFloatingPointRange<Float>, | ||
angle: Float, | ||
labelCount: Int, | ||
labelColor: Color | ||
) { | ||
val reduceBy = 1F / labelCount | ||
for (i in 0 until labelCount) { | ||
val scale = 1F - reduceBy * i | ||
drawContext.canvas.nativeCanvas.drawText( | ||
(dataRange.start + (dataRange.endInclusive - dataRange.start) * scale).toString(), | ||
center.x + scale * radius * cos(angle), | ||
center.y + scale * radius * sin(angle), | ||
Paint().apply { | ||
this.color = labelColor.toArgb() | ||
this.textSize = size.width / 30 | ||
this.textAlign = Paint.Align.CENTER | ||
} | ||
) | ||
} | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
private fun RadarChartPreview() { | ||
RadarChart( | ||
dataCollection = ChartDataCollection( | ||
listOf( | ||
RadarData(30f, "AAAAAA"), | ||
RadarData(25f, "BBBBBB"), | ||
RadarData(20f, "CCCCCC"), | ||
RadarData(15f, "DDDDDD"), | ||
RadarData(10f, "EEEEEE"), | ||
) | ||
), | ||
modifier = Modifier.size(350.dp), | ||
axisConfig = ChartDefaults.axisConfigDefaults(), | ||
radarConfig = RadarChartDefaults.defaultConfig().copy( | ||
fillPolygon = true | ||
) | ||
) | ||
} |
18 changes: 18 additions & 0 deletions
18
charty/src/main/java/com/himanshoe/charty/radar/config/RadarChartColors.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.himanshoe.charty.radar.config | ||
|
||
import androidx.compose.runtime.Immutable | ||
import androidx.compose.ui.graphics.Color | ||
|
||
/** | ||
* Represents the colors used in a radar chart. | ||
* | ||
* @property lineColor The colors of the lines in the chart. | ||
* @property dotColor The colors of the dots in the chart. | ||
* @property fillColor The fill color of the polygon in the chart. | ||
*/ | ||
@Immutable | ||
data class RadarChartColors( | ||
val lineColor: List<Color>, | ||
val dotColor: List<Color>, | ||
val fillColor: List<Color> = lineColor.map { it.copy(alpha = 0.5F) } | ||
) |
38 changes: 38 additions & 0 deletions
38
charty/src/main/java/com/himanshoe/charty/radar/config/RadarChartDefaults.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.himanshoe.charty.radar.config | ||
|
||
import androidx.compose.ui.graphics.Color | ||
|
||
/** | ||
* Default configurations and colors for a radar chart. | ||
*/ | ||
object RadarChartDefaults { | ||
|
||
/** | ||
* Returns the default configuration for a radar chart. | ||
* | ||
* @return The default radar chart configuration. | ||
*/ | ||
fun defaultConfig(): RadarConfig = | ||
RadarConfig( | ||
hasDotMarker = true, | ||
strokeSize = 5F, | ||
fillPolygon = true, | ||
) | ||
|
||
/** | ||
* Returns the default colors for a radar chart. | ||
* | ||
* @return The default radar chart colors. | ||
*/ | ||
fun defaultColor(): RadarChartColors = | ||
RadarChartColors( | ||
lineColor = listOf( | ||
Color(0xffed625d), | ||
Color(0xfff79f88) | ||
), | ||
dotColor = listOf( | ||
Color(0xff50c0a8), | ||
Color(0xff7a57e3), | ||
), | ||
) | ||
} |
17 changes: 17 additions & 0 deletions
17
charty/src/main/java/com/himanshoe/charty/radar/config/RadarConfig.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.himanshoe.charty.radar.config | ||
|
||
import androidx.compose.runtime.Immutable | ||
|
||
/** | ||
* Configuration options for a data polygon in a radar chart. | ||
* | ||
* @property hasDotMarker Whether the polygon should have a dot marker at each data point. | ||
* @property strokeSize The size of the line stroke. | ||
* @property fillPolygon Whether the polygon should be filled with a color. | ||
*/ | ||
@Immutable | ||
data class RadarConfig( | ||
val hasDotMarker: Boolean, | ||
val strokeSize: Float, | ||
val fillPolygon: Boolean, | ||
) |
19 changes: 19 additions & 0 deletions
19
charty/src/main/java/com/himanshoe/charty/radar/model/RadarData.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.himanshoe.charty.radar.model | ||
|
||
import androidx.compose.runtime.Immutable | ||
import com.himanshoe.charty.common.ChartData | ||
|
||
/** | ||
* Represents a data point for a radar chart. | ||
* | ||
* @property yValue The value of the data point. | ||
* @property xValue The label or identifier of the data point. | ||
*/ | ||
@Immutable | ||
data class RadarData( | ||
override val yValue: Float, | ||
override val xValue: Any, | ||
) : ChartData { | ||
override val chartString: String | ||
get() = "Radar Chart" | ||
} |