Donut is a Compose Multiplatform library for creating beautiful doughnut-like charts. It also contains a legacy View-based implementation for Android only.
gradle/libs.versions.toml
:
[libraries]
donut-compose = { group = "app.futured.donut", name = "donut-compose", version.ref = "donut" }
module/build.gradle.kts
:
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.donut.compose)
}
}
}
}
module/build.gradle
:
dependencies {
implementation("app.futured.donut:donut:$version")
}
Snapshots are build from each commit to master branch.
Add new Maven repo in settings.gradle.kts
:
dependencyResolutionManagement {
repositories {
// ...
maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
}
}
Snapshots are grouped based on major version of library, so for version 2.x, use:
implementation("app.futured.donut:donut-compose:2.X.X-SNAPSHOT")
Donut provides configurable doughnut-like charts capable of displaying multiple sections with assignable colors. It supports animations and features a gap at the top, which makes it look like a gauge (or tasty bitten-off donut - that's why the name).
The charts automatically scale their sections proportionally to their values once they get filled up. This allows you to show your users their daily progresses, reached goals, etc.
@Composable
fun Sample() {
DonutProgress(
model = DonutModel(
cap = 2.2f,
masterProgress = 1f,
gapWidthDegrees = 25f,
gapAngleDegrees = 270f,
strokeWidth = 40f,
backgroundLineColor = Color(0xFFE7E8E9),
sections = listOf(
DonutSection(amount = 0.4f, color = Color(0xFFFB1D32)),
DonutSection(amount = 0.4f, color = Color(0xFFFFB98E)),
)
),
config = DonutConfig.create(
layoutAnimationSpec = tween(
durationMillis = 1000,
easing = CubicBezierEasing(0.18f, 0.7f, 0.16f, 1f),
),
colorAnimationSpec = tween(durationMillis = 1000),
)
)
}
You'll get something like this:
Once the sum of all section values exceeds the cap
property, the chart starts to scale its sections proportionally to their amounts along its length. E.g. if we set cap to 1f
and total amount > 1f, we would get something like this:
Place the view in your layout
<app.futured.donut.DonutProgressView
android:id="@+id/donut_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:donut_bgLineColor="@color/cloud"
app:donut_gapWidth="20"
app:donut_gapAngle="270"
app:donut_strokeWidth="16dp"/>
Submit data to the view
val section1 = DonutSection(
name = "section_1",
color = Color.parseColor("#FB1D32"),
amount = 1f
)
val section2 = DonutSection(
name = "section_2",
color = Color.parseColor("#FFB98E"),
amount = 1f
)
donut_view.cap = 5f
donut_view.submitData(listOf(section1, section2))
You'll get something like this:
Both implementations work differently when it comes to data management.
The Compose version uses simple DonutSection
objects with color and amount:
@Composable
fun MyDonutChart() {
DonutProgress(
model = DonutModel(
cap = 5f,
sections = listOf(
DonutSection(color = Color.Cyan, amount = 1.2f),
DonutSection(color = Color.Red, amount = 2f),
)
)
)
}
Important: The Compose version does not support adding or removing sections dynamically. The number of sections must remain the same throughout the component's lifecycle. To simulate adding/removing sections, initialize with empty sections and set their amounts to zero.
The View-based version requires unique names for each section and supports dynamic changes:
val waterAmount = DonutSection(
name = "drink_amount_water",
color = Color.parseColor("#03BFFA"),
amount = 1.2f
)
donut_view.submitData(listOf(waterAmount))
(Note: the view uses unique name for each section to resolve its internal state and animations, and throws IllegalStateException
if multiple sections with same name are submitted.)
The view also provides methods for more granular control over displayed data. You can use addAmount
, setAmount
and removeAmount
methods to add, set or remove specified amounts from displayed sections.
donut_view.addAmount(
sectionName = "drink_amount_water",
amount = 0.5f,
color = Color.parseColor("#03BFFA") // Optional, pass color if you want to create new section
)
The addAmount
adds specified amount to section with provided name. What if section does not yet exist? This method has one optional color
parameter (default value is null
) - when called, and there isn't already displayed any section with provided name and color
parameter was specified, the new DonutSection
with provided name, amount and color will be automatically created internally for you. If you leave the color
param null
while trying to add value to non-existent section, nothing happens.
donut_view.setAmount(
sectionName = "drink_amount_water",
amount = 2.5f
)
The setAmount
methods sets specified amount to section with provided name. If provided amount is equal or less than 0, section and corresponding progress line are automatically removed after animation. If view does not contain specified section, nothing happens.
donut_view.removeAmount(
sectionName = "drink_amount_water",
amount = 0.1f
)
The removeAmount
simply subtracts specified amount from any displayed section. If resulting amount is equal or less than 0, section and corresponding progress line are automatically removed after animation. If view does not contain specified section, nothing happens.
If you want to get currently displayed data, call getData()
method which returns immutable list of all displayed DonutSection
objects. To clear displayed data, call clear()
method.
Each call to a data method (submit, add, set, remove, clear) results in view automatically resolving and animating to the new state.
Both implementations provide extensive customization options to create unique donut chart styles. The Compose Multiplatform version offers more granular control over animations, while the View-based version uses traditional XML attributes.
This library is implemented as a standalone module also for Compose Multiplatform. It has (almost) the same features as the view implementation, but it supports a wider variety of animations.
@Composable
fun Sample() {
DonutProgress(
model = DonutModel(
cap = 8f,
masterProgress = 1f,
gapWidthDegrees = 270f,
gapAngleDegrees = 90f,
strokeWidth = 40f,
backgroundLineColor = Color.LightGray,
sections = listOf(
DonutSection(amount = 1f, color = Color.Cyan),
DonutSection(amount = 1f, color = Color.Red),
DonutSection(amount = 1f, color = Color.Green),
DonutSection(amount = 0f, color = Color.Blue)
)
),
config = DonutConfig(
gapAngleAnimationSpec = spring()
// ...
)
)
}
Name | Default value | Description |
---|---|---|
cap |
1.0f |
View's cap property |
donut_strokeWidth |
12dp |
Width of background and section lines in dp |
donut_strokeCap |
round |
The paint cap used for all lines. Can be either 'round' or 'butt' |
donut_bgLineColor |
#e7e8e9 |
Color of background line |
donut_gapWidth |
45° |
Width of the line gap in degrees |
donut_gapAngle |
90° |
Position of the line gap around the view in degrees |
donut_direction |
clockwise |
Progress lines direction (clockwise or anticlockwise ) |
donut_animateChanges |
true |
Animation enabled flag, if true , the view will animate it's state changes (enabled by default) |
donut_animationInterpolator |
DecelerateInterpolator |
Interpolator to be used in state change animations |
donut_animationDuration |
1000 ms |
Duration of state change animations in ms |
In addition to these XML attributes, the view features masterProgress
property (0f to 1f
) that can be changed programmatically. It controls percentual progress of all lines, including the background line, which allows you to get creative with startup animations, etc.
The quickest way to explore different styles and configurations is to try the unified sample app:
- sample-cmp - Compose Multiplatform app with both implementations
- Android: Run directly from Android Studio
- Choose between Compose Multiplatform (modern cross-platform) or Legacy Views (traditional Android) implementations
- Features an interactive playground with buttons and sliders to experiment with different configurations
- iOS: Open the iOS project in Xcode, fix signing errors regarding development team ID, and run on iOS devices or simulators
- btw: you can add your team ID in Signing.local.xcconfig.example. Remove
.example
from the filename and yourproject.pbxproj
won't change. - iOS version shows the Compose Multiplatform implementation
- btw: you can add your team ID in Signing.local.xcconfig.example. Remove
- Android: Run directly from Android Studio
Current maintainer and main contributor for the original version is Matej Semančík and for Jetpack Compose version is Martin Sumera. Check the PUBLISHING.md document for releasing and publishing guidelines.
Donut is available under MIT license. See LICENSE file for more information.