Skip to content

Commit 5485bdb

Browse files
authored
Merge pull request #79 from alexandre-thauvin/feature/donut_chart_more_customizable
[Donut Chart] [Customization] Add label when no slice is selected and enable/disable click on slice
2 parents 95a6f84 + 07e7991 commit 5485bdb

File tree

10 files changed

+234
-106
lines changed

10 files changed

+234
-106
lines changed

YChartsLib/src/androidTest/java/co/yml/charts/piechart/DonutPieChartTest.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ class DonutPieChartTest {
2323
val composeTestRule = createComposeRule()
2424

2525
private val pieChartConfig = PieChartConfig(
26-
percentVisible = false,
26+
labelVisible = false,
2727
strokeWidth = 120f,
28-
percentColor = Color.Black
28+
labelColor = Color.Black
2929
)
3030

3131
private val pieChartData = PieChartData(
@@ -70,4 +70,3 @@ class DonutPieChartTest {
7070
composeTestRule.onNodeWithText("C").assertDoesNotExist()
7171
}
7272
}
73-

YChartsLib/src/androidTest/java/co/yml/charts/piechart/PieChartTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ class PieChartTest {
2121
val composeTestRule = createComposeRule()
2222

2323
private val pieChartConfig = PieChartConfig(
24-
percentVisible = false,
24+
labelVisible = false,
2525
strokeWidth = 120f,
26-
percentColor = Color.Black
26+
labelColor = Color.Black
2727
)
2828
private val pieChartData = PieChartData(
2929
slices = listOf(

YChartsLib/src/main/java/co/yml/charts/ui/piechart/PieChartConstants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ object PieChartConstants {
1010
const val TOTAL_ANGLE = 360
1111
const val ONE_HUNDRED = 100
1212
const val DESCRIPTION = "Double tap to know the chart in detail"
13+
const val NO_SELECTED_SLICE = -1
1314
}

YChartsLib/src/main/java/co/yml/charts/ui/piechart/charts/DonutPieChart.kt

Lines changed: 113 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,27 @@ import androidx.compose.runtime.saveable.rememberSaveable
1919
import androidx.compose.ui.Modifier
2020
import androidx.compose.ui.geometry.Size
2121
import androidx.compose.ui.graphics.Color
22+
import androidx.compose.ui.graphics.NativeCanvas
2223
import androidx.compose.ui.graphics.nativeCanvas
2324
import androidx.compose.ui.graphics.toArgb
2425
import androidx.compose.ui.input.pointer.pointerInput
2526
import androidx.compose.ui.platform.LocalContext
2627
import androidx.compose.ui.semantics.contentDescription
2728
import androidx.compose.ui.semantics.semantics
2829
import androidx.compose.ui.unit.dp
30+
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
31+
import co.yml.charts.common.components.accessibility.SliceInfo
32+
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
33+
import co.yml.charts.common.model.PlotType
34+
import co.yml.charts.ui.piechart.PieChartConstants.NO_SELECTED_SLICE
2935
import co.yml.charts.ui.piechart.models.PieChartConfig
3036
import co.yml.charts.ui.piechart.models.PieChartData
3137
import co.yml.charts.ui.piechart.utils.convertTouchEventPointToAngle
3238
import co.yml.charts.ui.piechart.utils.proportion
3339
import co.yml.charts.ui.piechart.utils.sweepAngles
34-
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
35-
import co.yml.charts.common.components.accessibility.SliceInfo
36-
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
37-
import co.yml.charts.common.model.PlotType
3840
import kotlinx.coroutines.launch
3941
import kotlin.math.roundToInt
4042

41-
4243
/**
4344
* Compose function for Drawing Donut chart
4445
* @param modifier : All modifier related property
@@ -71,7 +72,7 @@ fun DonutPieChart(
7172
}
7273

7374
var activePie by rememberSaveable {
74-
mutableStateOf(-1)
75+
mutableStateOf(NO_SELECTED_SLICE)
7576
}
7677
val accessibilitySheetState =
7778
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
@@ -89,9 +90,9 @@ fun DonutPieChart(
8990
Surface(
9091
modifier = modifier
9192
) {
92-
BoxWithConstraints(
93-
modifier = modifier
94-
.aspectRatio(1f)
93+
val boxModifier = if (pieChartConfig.isClickOnSliceEnabled) {
94+
modifier
95+
.aspectRatio(ratio = 1f)
9596
.background(pieChartConfig.backgroundColor)
9697
.semantics {
9798
contentDescription = pieChartConfig.accessibilityConfig.chartDescription
@@ -102,7 +103,17 @@ fun DonutPieChart(
102103
accessibilitySheetState.show()
103104
}
104105
}
105-
}) {
106+
}
107+
} else {
108+
modifier
109+
.aspectRatio(1f)
110+
.semantics {
111+
contentDescription = pieChartConfig.accessibilityConfig.chartDescription
112+
}
113+
}
114+
BoxWithConstraints(
115+
modifier = boxModifier
116+
) {
106117

107118
val sideSize = Integer.min(constraints.maxWidth, constraints.maxHeight)
108119
val padding = (sideSize * pieChartConfig.chartPadding) / 100f
@@ -120,12 +131,11 @@ fun DonutPieChart(
120131
}
121132
}
122133

123-
Canvas(
124-
modifier = Modifier
134+
val canvasModifier = if (pieChartConfig.isClickOnSliceEnabled) {
135+
Modifier
125136
.width(sideSize.dp)
126137
.height(sideSize.dp)
127138
.pointerInput(true) {
128-
129139
detectTapGestures {
130140
val clickedAngle = convertTouchEventPointToAngle(
131141
sideSize.toFloat(),
@@ -135,14 +145,23 @@ fun DonutPieChart(
135145
)
136146
progressSize.forEachIndexed { index, item ->
137147
if (clickedAngle <= item) {
138-
if (activePie != index)
139-
activePie = index
148+
activePie = if (activePie != index)
149+
index
150+
else
151+
NO_SELECTED_SLICE
140152
onSliceClick(pieChartData.slices[index])
141153
return@detectTapGestures
142154
}
143155
}
144156
}
145157
}
158+
} else {
159+
Modifier
160+
.width(sideSize.dp)
161+
.height(sideSize.dp)
162+
}
163+
Canvas(
164+
modifier = canvasModifier
146165

147166
) {
148167

@@ -163,22 +182,54 @@ fun DonutPieChart(
163182
)
164183
sAngle += arcProgress
165184
}
166-
167-
if (activePie != -1 && pieChartConfig.percentVisible)
168-
drawContext.canvas.nativeCanvas.apply {
169-
val fontSize = pieChartConfig.percentageFontSize.toPx()
170-
drawText(
171-
"${proportions[activePie].roundToInt()}%",
172-
(sideSize / 2) + fontSize / 4, (sideSize / 2) + fontSize / 3,
173-
Paint().apply {
174-
color = pieChartConfig.percentColor.toArgb()
175-
textSize = fontSize
176-
textAlign = Paint.Align.CENTER
177-
typeface = pieChartConfig.percentageTypeface
178-
185+
when {
186+
activePie != -1 && pieChartConfig.labelVisible -> {
187+
val selectedSlice = pieChartData.slices[activePie]
188+
drawContext.canvas.nativeCanvas.apply {
189+
val fontSize = pieChartConfig.labelFontSize.toPx()
190+
var isValue = false
191+
val textToDraw = when (pieChartConfig.labelType) {
192+
PieChartConfig.LabelType.PERCENTAGE -> "${
193+
proportions[activePie].roundToInt()
194+
}%"
195+
PieChartConfig.LabelType.VALUE -> {
196+
isValue = true
197+
selectedSlice.value.toString()
198+
}
199+
}
200+
val labelColor = when (pieChartConfig.labelColorType) {
201+
PieChartConfig.LabelColorType.SPECIFIED_COLOR -> pieChartConfig.labelColor
202+
PieChartConfig.LabelColorType.SLICE_COLOR -> selectedSlice.color
179203
}
180-
)
204+
val shouldShowUnit = isValue && pieChartConfig.sumUnit.isNotEmpty()
205+
drawLabel(
206+
canvas = this,
207+
pieChartConfig = pieChartConfig,
208+
labelColor = labelColor,
209+
shouldShowUnit = shouldShowUnit,
210+
fontSize = fontSize,
211+
textToDraw = textToDraw,
212+
sideSize = sideSize
213+
)
214+
}
181215
}
216+
activePie == -1 && pieChartConfig.isSumVisible -> {
217+
drawContext.canvas.nativeCanvas.apply {
218+
val fontSize = pieChartConfig.labelFontSize.toPx()
219+
val textToDraw = "$sumOfValues"
220+
drawLabel(
221+
canvas = this,
222+
pieChartConfig = pieChartConfig,
223+
labelColor = pieChartConfig.labelColor,
224+
shouldShowUnit = pieChartConfig.sumUnit.isNotEmpty(),
225+
fontSize = fontSize,
226+
textToDraw = textToDraw,
227+
sideSize = sideSize
228+
)
229+
}
230+
}
231+
232+
}
182233
}
183234
}
184235
if (isTalkBackEnabled) {
@@ -204,3 +255,36 @@ fun DonutPieChart(
204255
}
205256
}
206257
}
258+
259+
private fun drawLabel(
260+
canvas: NativeCanvas,
261+
pieChartConfig: PieChartConfig,
262+
labelColor: Color,
263+
shouldShowUnit: Boolean,
264+
fontSize: Float,
265+
textToDraw: String,
266+
sideSize: Int
267+
) {
268+
val paint = Paint().apply {
269+
color = labelColor.toArgb()
270+
textSize = fontSize
271+
textAlign = Paint.Align.CENTER
272+
typeface = pieChartConfig.labelTypeface
273+
}
274+
val x = (sideSize / 2).toFloat()
275+
var y: Float = (sideSize / 2).toFloat() + fontSize / 3
276+
if (shouldShowUnit)
277+
y -= (paint.fontSpacing / 4)
278+
canvas.drawText(
279+
textToDraw,
280+
x, y,
281+
paint
282+
)
283+
y += paint.fontSpacing
284+
canvas.drawText(
285+
pieChartConfig.sumUnit,
286+
x,
287+
y,
288+
paint
289+
)
290+
}

YChartsLib/src/main/java/co/yml/charts/ui/piechart/charts/PieChart.kt

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,19 @@ import androidx.compose.ui.semantics.contentDescription
3232
import androidx.compose.ui.semantics.semantics
3333
import androidx.compose.ui.unit.dp
3434
import androidx.core.graphics.withRotation
35+
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
36+
import co.yml.charts.common.components.accessibility.SliceInfo
37+
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
38+
import co.yml.charts.common.extensions.getTextHeight
39+
import co.yml.charts.common.model.PlotType
3540
import co.yml.charts.ui.piechart.PieChartConstants.MINIMUM_PERCENTAGE_FOR_SLICE_LABELS
41+
import co.yml.charts.ui.piechart.PieChartConstants.NO_SELECTED_SLICE
3642
import co.yml.charts.ui.piechart.models.PieChartConfig
3743
import co.yml.charts.ui.piechart.models.PieChartData
3844
import co.yml.charts.ui.piechart.utils.convertTouchEventPointToAngle
3945
import co.yml.charts.ui.piechart.utils.getSliceCenterPoints
4046
import co.yml.charts.ui.piechart.utils.proportion
4147
import co.yml.charts.ui.piechart.utils.sweepAngles
42-
import co.yml.charts.common.components.accessibility.AccessibilityBottomSheetDialog
43-
import co.yml.charts.common.components.accessibility.SliceInfo
44-
import co.yml.charts.common.extensions.collectIsTalkbackEnabledAsState
45-
import co.yml.charts.common.extensions.getTextHeight
46-
import co.yml.charts.common.model.PlotType
4748
import kotlinx.coroutines.launch
4849
import kotlin.math.abs
4950
import kotlin.math.roundToInt
@@ -80,7 +81,7 @@ fun PieChart(
8081
}
8182

8283
var activePie by rememberSaveable {
83-
mutableStateOf(-1)
84+
mutableStateOf(NO_SELECTED_SLICE)
8485
}
8586
val accessibilitySheetState =
8687
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
@@ -96,9 +97,9 @@ fun PieChart(
9697
Surface(
9798
modifier = modifier.fillMaxWidth()
9899
) {
99-
BoxWithConstraints(
100-
modifier = modifier
101-
.aspectRatio(1f)
100+
val boxModifier = if (pieChartConfig.isClickOnSliceEnabled) {
101+
modifier
102+
.aspectRatio(ratio = 1f)
102103
.background(pieChartConfig.backgroundColor)
103104
.semantics {
104105
contentDescription = pieChartConfig.accessibilityConfig.chartDescription
@@ -109,7 +110,16 @@ fun PieChart(
109110
accessibilitySheetState.show()
110111
}
111112
}
112-
},
113+
}
114+
} else {
115+
modifier
116+
.aspectRatio(1f)
117+
.semantics {
118+
contentDescription = pieChartConfig.accessibilityConfig.chartDescription
119+
}
120+
}
121+
BoxWithConstraints(
122+
modifier = boxModifier
113123
) {
114124

115125
val sideSize = Integer.min(constraints.maxWidth, constraints.maxHeight)
@@ -126,24 +136,36 @@ fun PieChart(
126136
)
127137
}
128138
}
129-
Canvas(modifier = Modifier
130-
.width(sideSize.dp)
131-
.height(sideSize.dp)
132-
.pointerInput(true) {
133-
134-
detectTapGestures {
135-
val clickedAngle = convertTouchEventPointToAngle(
136-
sideSize.toFloat(), sideSize.toFloat(), it.x, it.y
137-
)
138-
progressSize.forEachIndexed { index, item ->
139-
if (clickedAngle <= item) {
140-
if (activePie != index) activePie = index
141-
onSliceClick(pieChartData.slices[index])
142-
return@detectTapGestures
139+
val canvasModifier = if (pieChartConfig.isClickOnSliceEnabled) {
140+
Modifier
141+
.width(sideSize.dp)
142+
.height(sideSize.dp)
143+
.pointerInput(true) {
144+
detectTapGestures {
145+
val clickedAngle = convertTouchEventPointToAngle(
146+
sideSize.toFloat(),
147+
sideSize.toFloat(),
148+
it.x,
149+
it.y
150+
)
151+
progressSize.forEachIndexed { index, item ->
152+
if (clickedAngle <= item) {
153+
activePie = if (activePie != index)
154+
index
155+
else
156+
NO_SELECTED_SLICE
157+
onSliceClick(pieChartData.slices[index])
158+
return@detectTapGestures
159+
}
143160
}
144161
}
145162
}
146-
}) {
163+
} else {
164+
Modifier
165+
.width(sideSize.dp)
166+
.height(sideSize.dp)
167+
}
168+
Canvas(modifier = canvasModifier) {
147169

148170
var sAngle = pieChartConfig.startAngle
149171

@@ -190,7 +212,7 @@ fun PieChart(
190212
it.nativeCanvas.withRotation(
191213
arcCenter, x, y
192214
) {
193-
if (pieChartConfig.percentVisible) {
215+
if (pieChartConfig.labelVisible) {
194216
label = "$label ${proportions[index].roundToInt()}%"
195217
}
196218
it.nativeCanvas.drawText(

0 commit comments

Comments
 (0)