Skip to content

Commit

Permalink
Redesign pie chart (#36)
Browse files Browse the repository at this point in the history
* [Redesign] Pie Chart
  • Loading branch information
hi-manshu authored Sep 13, 2022
1 parent eba0483 commit 24d23a2
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 177 deletions.
49 changes: 9 additions & 40 deletions app/src/main/java/com/himanshoe/charty/ui/PieChartDemo.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.himanshoe.charty.ui

import android.util.Log
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand All @@ -25,26 +23,20 @@ fun PieChartDemo() {
) {

val pieData = listOf(
PieData(20F),
PieData(50F),
PieData(100F),
PieData(70F),
PieData(20F),
PieData(50F),
PieData(100F),
PieData(20F)
PieData(3F),
PieData(1F),
PieData(1F),
)
val pieDataWithCustomColors =
listOf(PieData(20F, Color(0xFFfafa6e)), PieData(50F, Color(0xFFc4ec74)))

item {
PieChart(
modifier = Modifier
.scale(1f)
.size(400.dp),
.fillMaxSize(),
pieData = pieData,
config = PieConfig(isDonut = true, expandDonutOnClick = true),
onSectionClicked = { percent, value ->
Log.d("DSfdsfdsf", percent.toString())
Log.d("DSfdsfdsf", value.toString())
}
)
}
Expand All @@ -58,35 +50,12 @@ fun PieChartDemo() {
textAlign = TextAlign.Center
)
}

item {
PieChart(
modifier = Modifier
.scale(1f)
.size(400.dp),
pieData = pieDataWithCustomColors,
config = PieConfig(isDonut = true, expandDonutOnClick = false),
onSectionClicked = { percent, value ->
}
)
}
item {
Text(
text = "Donut Chart custom colors, no expand",
fontSize = 16.sp,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
textAlign = TextAlign.Center
)
}
item {
PieChart(
modifier = Modifier
.scale(1f)
.size(400.dp),
.fillMaxSize(),
pieData = pieData,
config = PieConfig(false),
config = PieConfig(isDonut = false, expandDonutOnClick = true),
onSectionClicked = { percent, value ->
}
)
Expand Down
229 changes: 92 additions & 137 deletions charty/src/main/java/com/himanshoe/charty/pie/PieChart.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
package com.himanshoe.charty.pie

import android.annotation.SuppressLint
import android.graphics.Paint
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
Expand All @@ -42,82 +38,115 @@ fun PieChart(
) {

if (pieData.isEmpty()) return
val data = pieData.map { it.data }
val total = data.sum()
val proportions = data.map { it.times(TotalProgress).div(total) }
val angleProgress = proportions.map { TotalAngle.times(it).div(TotalProgress) }

val currentProgressSize = mutableListOf<Float>().apply {
add(angleProgress.first())
}

(1 until angleProgress.size).forEach { angle ->
currentProgressSize.add(angleProgress[angle].plus(currentProgressSize[angle.minus(1)]))
}

val currentPie = remember { mutableStateOf(-1) }

val pieChartData = pieData.map { it.data }
val total = pieChartData.sum()
var startAngle = ReflexAnge.toFloat()

BoxWithConstraints(modifier = modifier) {

val sideSize = min(constraints.maxWidth, constraints.maxHeight)
val padding = (sideSize.times(if (config.isDonut) 30 else 5)).div(80f)

val pathPortion = remember { Animatable(initialValue = 0f) }
LaunchedEffect(key1 = true) {
pathPortion.animateTo(
1f, animationSpec = tween(1000)
)
val pieChartPortion = pieChartData.map { it.times(TotalProgress).div(total) }
val angleProgress = pieChartPortion.map { TotalAngle.times(it).div(TotalProgress) }
val padding = if (config.isDonut) 40.dp else 0.dp
val currentProgressSize = buildList {
add(angleProgress.first())
(1 until angleProgress.size).forEach { angle ->
val newAngle: Float = this@buildList[angle.minus(1)]
add(angleProgress[angle].plus(newAngle))
}
}.toList()
val currentPie = remember {
mutableStateOf(-1)
}

val size = Size(sideSize.toFloat().minus(padding), sideSize.toFloat().minus(padding))
BoxWithConstraints(
modifier = modifier
.padding(padding)
.aspectRatio(1F)
) {

val minSide = min(constraints.minWidth, constraints.maxHeight)
Canvas(
modifier = Modifier
.width(sideSize.dp)
.height(sideSize.dp)
.handlePiePortionClick(
sideSize,
config.expandDonutOnClick,
currentProgressSize,
currentPie
) {
onSectionClicked(proportions[it], data[it])
.height(minSide.dp)
.width(minSide.dp)
.pointerInput(true) {
detectTapGestures { offset ->
val clickedAngle = convertTouchEventPointToAngle(
minSide.toFloat(),
minSide.toFloat(),
offset.x,
offset.y
)
currentProgressSize.forEachIndexed { index, item ->
if (clickedAngle <= item) {
if (currentPie.value != index) {
currentPie.value = index
}
onSectionClicked(pieChartPortion[index], pieChartData[index])
return@detectTapGestures
}
}
}
}
) {
angleProgress.forEachIndexed { index, individualAngle ->
if (!config.isDonut) {
drawArc(
color = pieData[index].color,
startAngle = startAngle,
sweepAngle = individualAngle,
useCenter = true,
)
} else {
val isClicked = currentPie.value != -1 && currentPie.value == index
if (isClicked) {
drawPieSection(
pieChartPortion[currentPie.value],
config.textColor,
minSide
)
}
drawArc(
color = pieData[index].color,
startAngle = startAngle,
sweepAngle = individualAngle,
useCenter = false,
style = Stroke(
width = if (isClicked) size.width.div(4.5F) else size.width.div(
5
)
)
)
}

angleProgress.forEachIndexed { index, arcProgress ->
drawPie(
pieData[index].color,
startAngle,
arcProgress * pathPortion.value,
size,
padding = padding,
isDonut = config.isDonut,
isActive = currentPie.value == index
)
startAngle += arcProgress
}

if (currentPie.value != -1 && config.isDonut) {
drawPieSection(proportions, currentPie.value, config.textColor, sideSize)
startAngle += individualAngle
}
}
}
}

private fun convertTouchEventPointToAngle(
width: Float,
height: Float,
xPos: Float,
yPos: Float
): Double {
val x = xPos.minus(width.times(0.5f))
val y = yPos.minus(height.times(0.5f))

var angle =
Math.toDegrees(kotlin.math.atan2(y.toDouble(), x.toDouble()).plus(Math.PI.div(2)))
angle = if (angle < 0) angle.plus(TotalAngle) else angle
return angle
}

fun DrawScope.drawPieSection(
proportions: List<Float>,
currentPieValue: Int,
value: Float,
percentColor: Color,
sideSize: Int
) {
drawContext.canvas.nativeCanvas.apply {
val fontSize = size.width.div(20).toDp().toPx()

drawText(
"${proportions[currentPieValue].roundToInt()}",
"${value.roundToInt()} %",
(sideSize.div(2)).plus(fontSize.div(4)), (sideSize.div(2)).plus(fontSize.div(3)),
Paint().apply {
color = percentColor.toArgb()
Expand All @@ -127,77 +156,3 @@ fun DrawScope.drawPieSection(
)
}
}

@SuppressLint("UnnecessaryComposedModifier")
private fun Modifier.handlePiePortionClick(
sideSize: Int,
isExpandable: Boolean,
currentProgressSize: MutableList<Float>,
currentPie: MutableState<Int>,
onIndexSelected: (Int) -> Unit
): Modifier = composed {
if (isExpandable) {
pointerInput(true) {
detectTapGestures { offset ->
val clickedAngle = convertTouchEventPointToAngle(
sideSize.toFloat(),
sideSize.toFloat(),
offset.x,
offset.y
)
currentProgressSize.forEachIndexed { index, item ->
if (clickedAngle <= item) {
if (currentPie.value != index) {
currentPie.value = index
}
onIndexSelected(currentPie.value)
return@detectTapGestures
}
}
}
}
} else {
Modifier
}
}

private fun DrawScope.drawPie(
color: Color,
startAngle: Float,
arcProgress: Float,
size: Size,
padding: Float,
isDonut: Boolean = false,
isActive: Boolean = false
): Path {

return Path().apply {
drawArc(
color = color,
startAngle = startAngle,
sweepAngle = arcProgress,
useCenter = !isDonut,
size = size,
style = if (isDonut)
Stroke(
width = if (isActive) size.width.div(1.5F) else size.width.div(2),
) else Fill,

topLeft = Offset(padding.div(2), padding.div(2))
)
}
}

private fun convertTouchEventPointToAngle(
width: Float,
height: Float,
xPos: Float,
yPos: Float
): Double {
val x = xPos.minus(width.times(0.5f))
val y = yPos.minus(height.times(0.5f))

var angle = Math.toDegrees(kotlin.math.atan2(y.toDouble(), x.toDouble()).plus(Math.PI.div(2)))
angle = if (angle < 0) angle.plus(TotalAngle) else angle
return angle
}

0 comments on commit 24d23a2

Please sign in to comment.