Building an Amazing 3D Pie Chart with Jetpack Compose

Kappdev
6 min readMay 14, 2024

--

Welcome 👋

Are you searching for a stunning Pie Chart to impress your users without investing more than 5 minutes in implementation?

If so, you’ve come to the right place. This article is exactly about that.

Stay tuned, and let’s dive in! 🚀

Convex Pie Chart

Let’s begin the journey from the most exciting part: crafting a Pie Chart with a convex effect to be applied to the slices.

PieChart data

Before we craft the composable function, we need to create a data class to represent the data for the pie chart.

data class PieChartData(
val label: String,
val value: Int,
val color: Color
)

ConvexStyle

We also require another supporting data class to represent the visual appearance of the convex effect to be applied to the slices.

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)
)

The composable

Now, we can define the composable that will draw the pie chart.

@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)
) {
/* Implementation */
}

⚒️ Parameters Breakdown

modifier ➜ Modifier applied to the layout.

data ➜ Data to be displayed on the pie chart.

startAngle ➜ Initial angle (in degrees) for the first slice (picture 1 👇).

rotationsCount ➜ Number of complete rotations during animation.

pieSliceStyle ➜ Defines the convex style for pie slices.

animationSpec ➜ Specifies animation behavior for scale and rotation.

Picture 1

Convex arc

Before drawing the pie chart itself, let’s create a support function called drawConvexArc to render our beautiful slices.

fun DrawScope.drawConvexArc(
color: Color,
startAngle: Float,
sweepAngle: Float,
useCenter: Boolean,
style: ConvexStyle,
) = drawIntoCanvas { canvas ->
val rect = this.size.toRect() // The bounds of the canvas

// Define paint object for drawing
val paint = Paint()
paint.color = color

// Draw the main arc on the canvas
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint)

// Function to draw shadow and glare arcs
fun drawShadowArc(offsetX: Float, offsetY: Float, shadowColor: Color) {
val shadowPaint = Paint() // Paint object for drawing shadows

shadowPaint.color = shadowColor // Set shadow color

// Save the current canvas layer
canvas.saveLayer(rect, shadowPaint)

// Draw the shadow arc
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, shadowPaint)

// Apply blending mode and blur effect for the shadow
shadowPaint.asFrameworkPaint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
maskFilter = BlurMaskFilter(style.blur.toPx(), BlurMaskFilter.Blur.NORMAL)
}

shadowPaint.color = Color.Black // Set the color for clipping

// Translate canvas and draw the clipping arc
canvas.translate(offsetX, offsetY)
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, shadowPaint)

// Restore canvas to its original state
canvas.restore()
}

// Calculate offset in pixels
val offsetPx = style.offset.toPx()

// Draw shadow arc with negative offset
drawShadowArc(-offsetPx, -offsetPx, style.shadowColor)

// Draw glare arc with positive offset
drawShadowArc(offsetPx, offsetPx, style.glareColor)
}

To understand better, take a look at the pictures below 👇

Shadow Arc Drawing
Convex Arc Drawing

Convex pie chart

Now that we have the drawConvexArc function, we can draw the pie chart.

@Composable
fun ConvexPieChart(
/* Parameters */
) {
// Sum of all data values
val totalValuesSum = remember(data) { data.sumOf(PieChartData::value) }

// Animatable values for scaling and rotating the pie chart
val pieChartScale = remember { Animatable(0f) }
val pieChartRotation = remember { Animatable(0f) }

// Launch animations for scaling and rotating the pie chart
LaunchedEffect(Unit) {
launch {
pieChartScale.animateTo(1f, animationSpec)
}
launch {
pieChartRotation.animateTo(360f * rotationsCount, animationSpec)
}
}

// Draw the pie chart using Canvas
Canvas(
modifier
.aspectRatio(1f) // Make sure the canvas is squared (1:1)
// Apply animation transitions
.scale(pieChartScale.value)
.rotate(pieChartRotation.value)
) {
// Initialize last value with the start angle
var lastValue = startAngle
// Iterate through each data point and draw corresponding pie slice
data.forEach { chartData ->
// Calculate sweep angle for the current data point
val pieSweepAngle = 360f * (chartData.value.toFloat() / totalValuesSum.toFloat())
// Draw convex arc representing the pie slice
drawConvexArc(
color = chartData.color,
startAngle = lastValue,
sweepAngle = pieSweepAngle,
style = pieSliceStyle,
useCenter = true
)
// Update last value for the next slice
lastValue += pieSweepAngle
}
}
}

Alright, here is what we’ve already achieved 😍

Pie chart panel

Now, let’s play around with shadows and craft a stunning panel for this pie chart.

In this Game of Shadows 🤹, we’ll utilize innerShadow and dropShadow modifiers. For a detailed explanation, refer to my related articles provided below 👇 or find the code on 👉 InnerShadow Gist, DropShadow Gist.

@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(alpha = 0.32f),
shadowColor = Color.Black.copy(alpha = 0.32f)
),
content: @Composable BoxScope.() -> Unit
) {
Box(
// Outer Box representing the entire panel
modifier = modifier
.aspectRatio(1f) // Ensure aspect ratio of 1:1
// Apply inner shadows to create depth effect
.innerShadow(CircleShape, style.glareColor, style.blur, -style.offset, -style.offset)
.innerShadow(CircleShape, style.shadowColor, style.blur, style.offset, style.offset)
// Apply drop shadows to create elevation effect
.dropShadow(CircleShape, style.glareColor, style.blur, -style.offset, -style.offset)
.dropShadow(CircleShape, style.shadowColor, style.blur, style.offset, style.offset)
// Draw the backgroud
.background(platesColor, CircleShape),
contentAlignment = Alignment.Center
) {
Box(
// Inner Box to contain the actual content
modifier = Modifier
.matchParentSize() // Occupy entire parent-size
.padding(platesGap) // Add gap between plates
// Apply drop shadows to create elevation effect
.dropShadow(CircleShape, style.glareColor, style.blur, -style.offset, -style.offset)
.dropShadow(CircleShape, style.shadowColor, style.blur, style.offset, style.offset)
// Draw the backgroud
.background(platesColor, CircleShape),
contentAlignment = Alignment.Center,
content = content // Paste the content
)
}
}

Here we go! The last piece we need is the content to display on the panel. Let’s craft some total-value text with a little animation.

@Composable
fun TotalView(
total: Int,
modifier: Modifier = Modifier,
animationSpec: AnimationSpec<Int> =
tween(1000, easing = FastOutSlowInEasing)
) {
val totalToDisplay = remember {
Animatable(initialValue = 0, typeConverter = Int.VectorConverter)
}

// Launch an effect to animate the total value when it changes
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)
)
}
}

Congratulations🥳! We’ve successfully built it👏. For the complete code implementation, you can access it on GitHub Gist🧑‍💻. Now, let’s put it all together and look at the final result!

Advertisement

Are you learning a foreign language and struggling with new vocabulary? Then, I strongly recommend you check out this words-learning app, which will make your journey easy and convenient!

WordBook

Final

Let’s create a list of data for the demonstration:

val pieChartData = remember {
listOf(
PieChartData("Item-1", 30, Color(0xFFE45C5C)),
PieChartData("Item-2", 45, Color(0xFF8FE25C)),
PieChartData("Item-3", 25, Color(0xFF4471E4)),
PieChartData("Item-4", 20, Color(0xFFEECE55)),
PieChartData("Item-5", 40, Color(0xFFBD68CB)),
)
}

And now, let’s wrap it up with the final touch 🔩

Box(contentAlignment = Alignment.Center) {
ConvexPieChart(
data = pieChartData,
modifier = Modifier.size(300.dp)
)
PieChartPanel(
Modifier.size(180.dp)
) {
TotalView(total = 23548)
}
}

The Result

You might also like 👇

Thank you for reading this article! ❤️ I hope you’ve found it enjoyable and valuable. Feel free to show your appreciation by hitting the clap 👏 if you liked it and follow Kappdev for more exciting articles 😊

Happy coding!

--

--

Kappdev

💡 Curious Explorer 🧭 Kotlin and Compose enthusiast 👨‍💻 Passionate about self-development and growth ❤️‍🔥 Push your boundaries 🚀