Last active
July 20, 2024 17:41
-
-
Save decodeandroid/cb91814fc2bf1dae5bb2b45359eeea06 to your computer and use it in GitHub Desktop.
PieChartCompose
This file contains hidden or 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
data class PieChartData( | |
val color: Color= getRandomColor(), | |
val value: Int, | |
val description: String, | |
val isTapped: Boolean = false | |
) | |
internal fun getRandomColor(): Color { | |
return Color( | |
red = (0..255).random(), | |
blue = (0..255).random(), | |
green = (0..255).random() | |
) | |
} | |
import android.graphics.Paint | |
import android.widget.Toast | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.gestures.detectTapGestures | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.padding | |
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.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.geometry.Size | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.StrokeCap | |
import androidx.compose.ui.graphics.drawscope.Stroke | |
import androidx.compose.ui.graphics.drawscope.rotate | |
import androidx.compose.ui.graphics.drawscope.scale | |
import androidx.compose.ui.graphics.nativeCanvas | |
import androidx.compose.ui.graphics.toArgb | |
import androidx.compose.ui.input.pointer.pointerInput | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.text.drawText | |
import androidx.compose.ui.text.rememberTextMeasurer | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import com.example.learngit.ui.theme.white | |
import kotlin.math.atan2 | |
import kotlin.math.cos | |
import kotlin.math.sin | |
@Preview(showBackground = true, showSystemUi = true) | |
@Composable | |
fun PreviewPieChart(modifier: Modifier = Modifier) { | |
val context = LocalContext.current | |
Column( | |
modifier = Modifier | |
.fillMaxSize() | |
.background(white) | |
.padding(5.dp), | |
verticalArrangement = Arrangement.Center, | |
horizontalAlignment = Alignment.CenterHorizontally | |
) { | |
PieChart( | |
dataPoints = listOf( | |
PieChartData( | |
value = 29, | |
description = "Data 1" | |
), | |
PieChartData( | |
value = 21, | |
description = "Data 2" | |
), | |
PieChartData( | |
value = 32, | |
description = "Data 3" | |
), | |
PieChartData( | |
value = 18, | |
description = "Data 4" | |
), | |
PieChartData( | |
value = 12, | |
description = "Data 5" | |
), | |
PieChartData( | |
value = 38, | |
description = "Data 6" | |
), | |
), | |
onSliceClick = { | |
Toast.makeText(context, it.description, Toast.LENGTH_SHORT).show() | |
} | |
) | |
} | |
} | |
@Composable | |
fun PieChart( | |
innerRadius: Float = 120f, | |
dataPoints: List<PieChartData>, | |
onSliceClick: ((PieChartData) -> Unit), | |
) { | |
var pieCenter by remember { | |
mutableStateOf(Offset.Zero) | |
} | |
var inputList by remember { | |
mutableStateOf(dataPoints) | |
} | |
var isCenterTapped by remember { | |
mutableStateOf(false) | |
} | |
val textMeasurer = rememberTextMeasurer() | |
val gapDegrees = 2f | |
val numberOfGaps = dataPoints.size | |
val remainingDegrees = 360f - (gapDegrees * numberOfGaps) | |
val totalValue = dataPoints.sumOf { | |
it.value | |
} | |
val anglePerValue = remainingDegrees / totalValue | |
androidx.compose.foundation.Canvas( | |
modifier = Modifier | |
.size(300.dp) | |
.pointerInput(true) { | |
detectTapGestures( | |
onTap = { offset -> //point where clicked | |
val angle = Math.toDegrees( | |
atan2( | |
offset.y - pieCenter.y, | |
offset.x - pieCenter.x | |
).toDouble() | |
) | |
//this angle can be +ve or -ve so we need to check both | |
val tapAngleInDegrees = if (angle < 0) angle + 360 else angle | |
val centerClicked = if (tapAngleInDegrees < 90) { | |
//means y -ve and x +ve region | |
offset.x < pieCenter.x + innerRadius && offset.y < pieCenter.y + innerRadius | |
} else if (tapAngleInDegrees < 180) { | |
//means y -ve and x -ve region | |
offset.x > pieCenter.x - innerRadius && offset.y < pieCenter.y + innerRadius | |
} else if (tapAngleInDegrees < 270) { | |
//means y +ve and x +ve region | |
offset.x > pieCenter.x - innerRadius && offset.y > pieCenter.y - innerRadius | |
} else { | |
//means y +ve and x +ve region | |
offset.x < pieCenter.x + innerRadius && offset.y > pieCenter.y - innerRadius | |
} | |
if (centerClicked) { | |
//make all slice isTapped value to true | |
inputList = inputList.map { | |
it.copy(isTapped = !isCenterTapped) | |
} | |
isCenterTapped = !isCenterTapped | |
} else { | |
//means we have clicked individual slice | |
var currAngle = 0f | |
inputList.forEach { pieChartInput -> | |
currAngle += pieChartInput.value * anglePerValue | |
if (tapAngleInDegrees < currAngle) { | |
val description = pieChartInput.description | |
inputList = inputList.map { | |
if (description == it.description) { | |
onSliceClick(it) | |
it.copy(isTapped = !it.isTapped) | |
} else { | |
it.copy(isTapped = false) | |
} | |
} | |
return@detectTapGestures | |
} | |
} | |
} | |
} | |
) | |
} | |
) { | |
val width = size.width | |
val height = size.height | |
val radius = width / 2 | |
pieCenter = Offset(x = width / 2f, y = height / 2f) | |
var currentStartAngle = 0f | |
val donutStyle = Stroke( | |
width = 100f, | |
cap = StrokeCap.Round | |
) | |
inputList.forEach { pieChartInput -> | |
val scale = if (pieChartInput.isTapped) 0.78f else 0.75f | |
val angleToDraw = pieChartInput.value * anglePerValue | |
scale(scale) { | |
drawArc( | |
color = pieChartInput.color, | |
startAngle = currentStartAngle, sweepAngle = angleToDraw, useCenter = true, | |
size = Size( | |
width = radius * 2f, | |
height = radius * 2f | |
), | |
topLeft = Offset( | |
(width - radius * 2f) / 2f, | |
(height - radius * 2f) / 2f | |
), | |
//style = donutStyle | |
) | |
currentStartAngle += angleToDraw + gapDegrees | |
} | |
//percentage value of each data point | |
val percentage = (pieChartInput.value / totalValue.toFloat() * 100).toInt() | |
//draw text inside the slice to show the percentage of data | |
drawContext.canvas.nativeCanvas.apply { | |
//only show data if > 5% | |
if (percentage > 5) { | |
val midAngle = currentStartAngle - gapDegrees - angleToDraw / 2f | |
val midOffSet = Offset( | |
x = (cos(Math.toRadians(midAngle.toDouble())) * radius + pieCenter.x).toFloat(), | |
y = (sin(Math.toRadians(midAngle.toDouble())) * radius + pieCenter.y).toFloat() | |
) | |
val xOffset = (midOffSet.x + pieCenter.x) / 2 | |
val yOffset = (midOffSet.y + pieCenter.y) / 2 | |
val centerOfSlice = Offset(xOffset, yOffset) | |
//measure text if it have any style like font, size, letter spacing | |
val textLayoutResult = textMeasurer.measure( | |
text = "$percentage %" | |
) | |
val textWidth = textLayoutResult.size.width | |
val textHeight = textLayoutResult.size.height | |
drawText( | |
textLayoutResult, color = Color.Black, | |
topLeft = Offset( | |
centerOfSlice.x - textWidth / 2, | |
centerOfSlice.y - textHeight / 2 | |
) | |
) | |
} | |
} | |
//angle for the text outside the slice | |
var rotateAngle = currentStartAngle - gapDegrees - angleToDraw / 2f - 90f | |
//how much distance from center you want to draw text outside the slice | |
var radiusFactor = .9f | |
if (rotateAngle > 90f) { | |
rotateAngle = (rotateAngle + 180).mod(360f) | |
//above 90 make text angle to negative | |
radiusFactor = -.9f | |
} | |
//show text outside the pie when tapped on a slice | |
if (pieChartInput.isTapped) { | |
rotate(rotateAngle) { | |
drawContext.canvas.nativeCanvas.apply { | |
drawText( | |
"${pieChartInput.description}: ${pieChartInput.value}", | |
pieCenter.x, | |
pieCenter.y + radius.times(radiusFactor), | |
Paint().apply { | |
textSize = 16.sp.toPx() | |
textAlign = Paint.Align.CENTER | |
color = Color.Black.toArgb() | |
} | |
) | |
} | |
} | |
} | |
//circle at the center of pie | |
drawCircle( | |
center = pieCenter, | |
color = Color.White, | |
radius = innerRadius | |
) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment