Skip to content

Instantly share code, notes, and snippets.

@L10n42
Created May 14, 2024 17:56
Show Gist options
  • Save L10n42/80a28255deb46e817d7fa7e4e05ff989 to your computer and use it in GitHub Desktop.
Save L10n42/80a28255deb46e817d7fa7e4e05ff989 to your computer and use it in GitHub Desktop.
3D Pie Chart in Jetpack Compose
import android.graphics.BlurMaskFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import kotlinx.coroutines.launch
/**
* Composable function to display a convex pie chart.
*
* This function renders a pie chart using a convex arc shape for each slice.
*
* @param modifier The modifier for layout customization.
* @param data The list of data points for the pie chart.
* @param startAngle The starting angle (in degrees) for the first slice of the pie chart.
* @param rotationsCount The number of complete rotations during animation.
* @param pieSliceStyle The style settings for the pie chart slices.
* @param animationSpec The animation specification for scaling and rotation.
*/
@Composable
fun ConvexPieChart(
modifier: Modifier,
data: List<PieChartData>,
startAngle: Float = -90f,
rotationsCount: Int = 4,
pieSliceStyle: ConvexStyle = ConvexStyle(),
animationSpec: AnimationSpec<Float> = tween(1_000, easing = LinearOutSlowInEasing)
) {
val totalValuesSum = remember(data) { data.sumOf(PieChartData::value) }
val pieChartScale = remember { Animatable(0f) }
val pieChartRotation = remember { Animatable(0f) }
LaunchedEffect(Unit) {
launch {
pieChartScale.animateTo(1f, animationSpec)
}
launch {
pieChartRotation.animateTo(360f * rotationsCount, animationSpec)
}
}
Canvas(
modifier
.aspectRatio(1f)
.scale(pieChartScale.value)
.rotate(pieChartRotation.value)
) {
var lastValue = startAngle
data.forEach { chartData ->
val pieSweepAngle = 360f * (chartData.value.toFloat() / totalValuesSum.toFloat())
drawConvexArc(
color = chartData.color,
startAngle = lastValue,
sweepAngle = pieSweepAngle,
style = pieSliceStyle,
useCenter = true
)
lastValue += pieSweepAngle
}
}
}
private fun DrawScope.drawConvexArc(
color: Color,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
style: ConvexStyle,
) = drawIntoCanvas { canvas ->
val rect = this.size.toRect()
val paint = Paint()
paint.color = color
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint)
fun drawShadowArc(offsetX: Float, offsetY: Float, shadowColor: Color) {
val shadowPaint = Paint()
shadowPaint.color = shadowColor
canvas.saveLayer(rect, shadowPaint)
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, shadowPaint)
shadowPaint.asFrameworkPaint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
maskFilter = BlurMaskFilter(style.blur.toPx(), BlurMaskFilter.Blur.NORMAL)
}
shadowPaint.color = Color.Black
canvas.translate(offsetX, offsetY)
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, shadowPaint)
canvas.restore()
}
val offsetPx = style.offset.toPx()
drawShadowArc(-offsetPx, -offsetPx, style.shadowColor)
drawShadowArc(offsetPx, offsetPx, style.glareColor)
}
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
data class ConvexStyle(
val blur: Dp = 5.dp,
val offset: Dp = 4.dp,
val glareColor: Color = Color.White.copy(0.48f),
val shadowColor: Color = Color.Black.copy(0.48f)
)
import androidx.compose.ui.graphics.Color
data class PieChartData(
val label: String,
val value: Int,
val color: Color
)
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun PieChartPanel(
modifier: Modifier,
platesColor: Color = Color(0xFFD5F3FF),
platesGap: Dp = 32.dp,
style: ConvexStyle = ConvexStyle(
blur = 12.dp,
offset = 8.dp,
glareColor = Color.White.copy(0.32f),
shadowColor = Color.Black.copy(0.32f)
),
content: @Composable BoxScope.() -> Unit
) {
Box(
modifier = modifier
.aspectRatio(1f)
.innerShadow(CircleShape, style.glareColor, style.blur, -style.offset, -style.offset)
.innerShadow(CircleShape, style.shadowColor, style.blur, style.offset, style.offset)
.dropShadow(CircleShape, style.glareColor, style.blur, -style.offset, -style.offset)
.dropShadow(CircleShape, style.shadowColor, style.blur, style.offset, style.offset)
.background(platesColor, CircleShape),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.matchParentSize()
.padding(platesGap)
.dropShadow(CircleShape, style.glareColor, style.blur, -style.offset, -style.offset)
.dropShadow(CircleShape, style.shadowColor, style.blur, style.offset, style.offset)
.background(platesColor, CircleShape),
contentAlignment = Alignment.Center,
content = content
)
}
}
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
@Composable
fun TotalView(
total: Int,
modifier: Modifier = Modifier,
animationSpec: AnimationSpec<Int> =
tween(1000, easing = FastOutSlowInEasing)
) {
val totalToDisplay = remember {
Animatable(initialValue = 0, typeConverter = Int.VectorConverter)
}
LaunchedEffect(total) {
totalToDisplay.animateTo(total, animationSpec)
}
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Total value",
fontSize = 14.sp,
color = Color(0xFF464646)
)
Text(
text = "${totalToDisplay.value}$",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF010203)
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment