Jetpack Compose Doughnut Chart with Source Code

Jetpack Compose Doughnut Chart with Source Code

In this article, we’ll learn how to create a doughnut chart in Jetpack Compose. Our final output looks like this:

Jetpack Compose Doughnut Chart

Prerequisites:

Let’s get started.

First, create an empty Compose project and open MainActivity.kt. Create a DoughnutChart() composable and call it from the onCreate() method. We’ll write our code in it.

MainActivity for the Material 3 Jetpack Compose:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YourProjectNameTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column(
                        modifier = Modifier.fillMaxSize(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        DoughnutChart()
                    }
                }
            }
        }
    }
}

@Composable
fun DoughnutChart(){

}

MainActivity for the Material 2 version:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
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.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BlogPostsTheme(darkTheme = false) {
                Column(
                    modifier = Modifier.fillMaxSize(),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.Center
                ) {
                    DoughnutChart()
                }
            }
        }
    }
}

@Composable
fun DoughnutChart() {

}

Our doughnut chart needs arc values, colors, legend, and size. Let’s add them as parameters.

@Composable
fun DoughnutChart(
    values: List<Float> = listOf(65f, 40f, 25f, 20f),
    colors: List<Color> = listOf(
        Color(0xFFFF6384),
        Color(0xFFFFCE56),
        Color(0xFF36A2EB),
        Color(0xFF448AFF)
    ),
    legend: List<String> = listOf("Mango", "Banana", "Apple", "Melon"),
    size: Dp = 200.dp,
    thickness: Dp = 36.dp
) {

}

Here, the size represents the size of the chart, and thickness is the width of each arc.

We need to convert the values into equivalent angles.

@Composable
fun DoughnutChart(
    values: List<Float> = listOf(65f, 40f, 25f, 20f),
    colors: List<Color> = listOf(
        Color(0xFFFF6384),
        Color(0xFFFFCE56),
        Color(0xFF36A2EB),
        Color(0xFF448AFF)
    ),
    legend: List<String> = listOf("Mango", "Banana", "Apple", "Melon"),
    size: Dp = 200.dp,
    thickness: Dp = 36.dp
) {
    // Sum of all the values
    val sumOfValues = values.sum()

    // Calculate each proportion
    val proportions = values.map {
        it * 100 / sumOfValues
    }

    // Convert each proportion to angle
    val sweepAngles = proportions.map {
        360 * it / 100
    }
}

The proportions list contains the percentage of each value. In a circle, 100% is equivalent to 360 degrees. So, to convert each percentage into an angle (degree), we have multiplied it by the ratio of (360/100).

As our sweep angles are ready, let’s add the Canvas and draw the arcs.

@Composable
fun DoughnutChart(
    values: List<Float> = listOf(65f, 40f, 25f, 20f),
    colors: List<Color> = listOf(
        Color(0xFFFF6384),
        Color(0xFFFFCE56),
        Color(0xFF36A2EB),
        Color(0xFF448AFF)
    ),
    legend: List<String> = listOf("Mango", "Banana", "Apple", "Melon"),
    size: Dp = 200.dp,
    thickness: Dp = 36.dp
) {
    // Sum of all the values
    val sumOfValues = values.sum()

    // Calculate each proportion
    val proportions = values.map {
        it * 100 / sumOfValues
    }

    // Convert each proportion to angle
    val sweepAngles = proportions.map {
        360 * it / 100
    }

    Canvas(
        modifier = Modifier
            .size(size = size)
    ) {
        var startAngle = -90f

        for (i in values.indices) {
            drawArc(
                color = colors[i],
                startAngle = startAngle,
                sweepAngle = sweepAngles[i],
                useCenter = false,
                style = Stroke(width = thickness.toPx(), cap = StrokeCap.Butt)
            )
            startAngle += sweepAngles[i]
        }
    }
}

Now, it looks like this:

Chart using Canvas

First, we have set the startAngle = -90f. It represents the 12’0 clock and acts as a starting point. In each iteration, the startAngle is updated to the sum of its previous value and the current sweepAngle.

The style parameter helps us to set the width of each arc. Don’t set dp values directly. Convert them to pixels using the toPx() method. This will ensure that our UI looks consistent across all devices, regardless of their screen resolution.

We passed false to the useCenter parameter. As a result, the arcs are not touching the center.

Next, let’s display the legend. Create a separate composable called DisplayLegend(). We need color and legend values.

@Composable
fun DisplayLegend(color: Color, legend: String) {

}

Each part contains a circle and text. We can make a circle using the Box() and Shape APIs. For the text, use the Text() composable. Put the box and text in a Row layout.

@Composable
fun DisplayLegend(color: Color, legend: String) {
    Row(
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box(
            modifier = Modifier
                .size(10.dp)
                .background(color = color, shape = CircleShape)
        )

        Spacer(modifier = Modifier.width(4.dp))

        Text(
            text = legend,
            color = Color.Black
        )
    }
}

Call it from the DoughnutChart() method. Our final code looks like this:

@Composable
fun DoughnutChart(
    values: List<Float> = listOf(65f, 40f, 25f, 20f),
    colors: List<Color> = listOf(
        Color(0xFFFF6384),
        Color(0xFFFFCE56),
        Color(0xFF36A2EB),
        Color(0xFF448AFF)
    ),
    legend: List<String> = listOf("Mango", "Banana", "Apple", "Melon"),
    size: Dp = 200.dp,
    thickness: Dp = 36.dp
) {
    // Sum of all the values
    val sumOfValues = values.sum()

    // Calculate each proportion
    val proportions = values.map {
        it * 100 / sumOfValues
    }

    // Convert each proportion to angle
    val sweepAngles = proportions.map {
        360 * it / 100
    }

    Canvas(
        modifier = Modifier
            .size(size = size)
    ) {
        var startAngle = -90f

        for (i in values.indices) {
            drawArc(
                color = colors[i],
                startAngle = startAngle,
                sweepAngle = sweepAngles[i],
                useCenter = false,
                style = Stroke(width = thickness.toPx(), cap = StrokeCap.Butt)
            )
            startAngle += sweepAngles[i]
        }
    }

    Spacer(modifier = Modifier.height(32.dp))

    Column {
        for (i in values.indices) {
            DisplayLegend(color = colors[i], legend = legend[i])
        }
    }
}

@Composable
fun DisplayLegend(color: Color, legend: String) {
    Row(
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box(
            modifier = Modifier
                .size(10.dp)
                .background(color = color, shape = CircleShape)
        )

        Spacer(modifier = Modifier.width(4.dp))

        Text(
            text = legend,
            color = Color.Black
        )
    }
}

Output:

Jetpack Compose Doughnut Chart

This is all about the doughnut chart in Jetpack Compose. I hope you have learned something new. If you have any doubts, leave a comment below.

Related Articles:


References:

Leave a Comment