diff --git a/Readme.md b/Readme.md index 6c3e634..6487108 100644 --- a/Readme.md +++ b/Readme.md @@ -27,6 +27,7 @@ You can find the detail implementation of the following: - [BarChart](docs/BarChart.md) - [CandleStickChart](docs/CandleStickChart.md) +- [StackedBarChart](docs/StackedBarChart.md) - [CombinedBarChart](docs/CombinedBarChart.md) - [HorizontalBarChart](docs/HorizontalBarChart.md) - [GroupedBarChart](docs/GroupedBarChart.md) diff --git a/app/src/main/java/com/himanshoe/charty/MainActivity.kt b/app/src/main/java/com/himanshoe/charty/MainActivity.kt index 788f457..294dc70 100644 --- a/app/src/main/java/com/himanshoe/charty/MainActivity.kt +++ b/app/src/main/java/com/himanshoe/charty/MainActivity.kt @@ -44,6 +44,8 @@ class MainActivity : ComponentActivity() { navigator.navigate("combinedBar") }, onCandleChartClicked = { navigator.navigate("candleChart") + }, onStackedBarClicked = { + navigator.navigate("stackedBar") } ) } @@ -63,11 +65,13 @@ fun MainApp( onGroupHorizontalClicked: () -> Unit, onCandleChartClicked: () -> Unit, onGroupBarClicked: () -> Unit, + onStackedBarClicked: () -> Unit, ) { val list: List Unit>> = listOf( "Bar Chart" to onBarChartClicked, + "Stack Bar Chart" to onStackedBarClicked, "Candle Chart" to onCandleChartClicked, "Combined Bar Chart" to onCombinedBarChartClicked, "Circle Chart" to onCircleChartClicked, diff --git a/app/src/main/java/com/himanshoe/charty/Navigator.kt b/app/src/main/java/com/himanshoe/charty/Navigator.kt index 06b4e82..00f7906 100644 --- a/app/src/main/java/com/himanshoe/charty/Navigator.kt +++ b/app/src/main/java/com/himanshoe/charty/Navigator.kt @@ -15,6 +15,7 @@ import com.himanshoe.charty.ui.HorizontalBarChartDemo import com.himanshoe.charty.ui.LineChartDemo import com.himanshoe.charty.ui.PieChartDemo import com.himanshoe.charty.ui.PointChartDemo +import com.himanshoe.charty.ui.StackedBarChartDemo @Composable fun RegisterNavigation( @@ -30,6 +31,7 @@ fun RegisterNavigation( onGroupBarClicked: () -> Unit, onCombinedBarChartClicked: () -> Unit, onCandleChartClicked: () -> Unit, + onStackedBarClicked: () -> Unit, ) { NavHost(navController = navigator, startDestination = "main") { composable("barchart") { BarChartDemo() } @@ -45,13 +47,15 @@ fun RegisterNavigation( onPieChartClicked = onPieChartClicked, onGroupHorizontalClicked = onGroupHorizontalClicked, onCombinedBarChartClicked = onCombinedBarChartClicked, - onCandleChartClicked = onCandleChartClicked + onCandleChartClicked = onCandleChartClicked, + onStackedBarClicked = onStackedBarClicked ) } composable("horizontalBarChartDemo") { HorizontalBarChartDemo() } composable("circlechart") { CircleChartDemo() } composable("linechart") { LineChartDemo() } composable("curvelinechart") { CurveLineChartDemo() } + composable("stackedBar") { StackedBarChartDemo() } composable("pointchart") { PointChartDemo() } composable("piechart") { PieChartDemo() } composable("grouphorizontalbar") { GroupedHorizontalBarChartDemo() } diff --git a/app/src/main/java/com/himanshoe/charty/ui/BarChart.kt b/app/src/main/java/com/himanshoe/charty/ui/BarChart.kt index db4f2aa..fec1816 100644 --- a/app/src/main/java/com/himanshoe/charty/ui/BarChart.kt +++ b/app/src/main/java/com/himanshoe/charty/ui/BarChart.kt @@ -31,22 +31,7 @@ fun BarChartDemo() { barData = listOf( BarData(10F, 35F), BarData(20F, 25F), - BarData(10F, 50F), - BarData(60F, 10F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(10F, 15F), - BarData(50F, 100F), - BarData(20F, 25F), + ) ) } diff --git a/app/src/main/java/com/himanshoe/charty/ui/StackBarChartDemo.kt b/app/src/main/java/com/himanshoe/charty/ui/StackBarChartDemo.kt new file mode 100644 index 0000000..03dcea4 --- /dev/null +++ b/app/src/main/java/com/himanshoe/charty/ui/StackBarChartDemo.kt @@ -0,0 +1,79 @@ +package com.himanshoe.charty.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.himanshoe.charty.bar.StackedBarChart +import com.himanshoe.charty.bar.model.StackedBarData + +private val stackedBarData = listOf( + StackedBarData(1F, listOf(40F, 50F, 10F)), + StackedBarData(2F, listOf(30F, 40F, 5F)), + StackedBarData(3F, listOf(90F, 40F, 71F)), + StackedBarData(4F, listOf(13F, 2F, 8F)), + StackedBarData(5F, listOf(30F, 40F, 20F)), + StackedBarData(6F, listOf(30F, 40F, 20F)), + StackedBarData(7F, listOf(40F, 50F, 10F)), + StackedBarData(8F, listOf(30F, 40F, 5F)), + StackedBarData(9F, listOf(90F, 40F, 71F)), + StackedBarData(10F, listOf(13F, 2F, 8F)), + StackedBarData(11F, listOf(30F, 40F, 20F)), + StackedBarData(1F, listOf(40F, 50F, 10F)), + StackedBarData(2F, listOf(30F, 40F, 5F)), + StackedBarData(3F, listOf(90F, 40F, 71F)), + StackedBarData(4F, listOf(13F, 2F, 8F)), + StackedBarData(5F, listOf(30F, 40F, 20F)), + StackedBarData(6F, listOf(30F, 40F, 20F)), + StackedBarData(7F, listOf(40F, 50F, 10F)), + StackedBarData(8F, listOf(30F, 40F, 5F)), + StackedBarData(9F, listOf(90F, 40F, 71F)), + StackedBarData(10F, listOf(13F, 2F, 8F)), + StackedBarData(11F, listOf(30F, 40F, 20F)), +) + +@Composable +fun StackedBarChartDemo() { + LazyColumn( + Modifier + .fillMaxSize() + ) { + item { + StackedBarChart( + modifier = Modifier + .fillMaxWidth() + .height(220.dp) + .padding(32.dp), + stackBarData = stackedBarData, + colors = pcolors + ) + } + item { + StackedBarChart( + modifier = Modifier + .fillMaxWidth() + .height(220.dp) + .padding(32.dp), + stackBarData = stackedBarData.takeLast(3), + onBarClick = { + }, + colors = pcolors + + ) + } + item { + StackedBarChart( + modifier = Modifier + .fillMaxWidth() + .height(220.dp) + .padding(32.dp), + colors = pcolors, + stackBarData = stackedBarData.takeLast(22) + ) + } + } +} diff --git a/charty/src/main/java/com/himanshoe/charty/bar/BarChart.kt b/charty/src/main/java/com/himanshoe/charty/bar/BarChart.kt index 7046c67..590390a 100644 --- a/charty/src/main/java/com/himanshoe/charty/bar/BarChart.kt +++ b/charty/src/main/java/com/himanshoe/charty/bar/BarChart.kt @@ -34,7 +34,7 @@ fun BarChart( color: Color, onBarClick: (BarData) -> Unit, modifier: Modifier = Modifier, - barDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), barConfig: BarConfig = BarConfigDefaults.barConfigDimesDefaults() ) { @@ -43,7 +43,7 @@ fun BarChart( colors = listOf(color, color), onBarClick = onBarClick, modifier = modifier, - barDimens = barDimens, + chartDimens = chartDimens, axisConfig = axisConfig, barConfig = barConfig ) @@ -55,7 +55,7 @@ fun BarChart( colors: List, onBarClick: (BarData) -> Unit, modifier: Modifier = Modifier, - barDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), barConfig: BarConfig = BarConfigDefaults.barConfigDimesDefaults() ) { @@ -75,7 +75,7 @@ fun BarChart( yAxis(axisConfig, maxYValue) } } - .padding(horizontal = barDimens.padding) + .padding(horizontal = chartDimens.padding) .pointerInput(Unit) { detectTapGestures(onPress = { offset -> clickedBar.value = offset @@ -86,8 +86,8 @@ fun BarChart( val yScalableFactor = size.height.div(maxYValue) barData.forEachIndexed { index, data -> - val topLeft = getTopLeft(index, barWidth, size, data, yScalableFactor) - val topRight = getTopRight(index, barWidth, size, data, yScalableFactor) + val topLeft = getTopLeft(index, barWidth.value, size, data.yValue, yScalableFactor) + val topRight = getTopRight(index, barWidth.value, size, data.yValue, yScalableFactor) val barHeight = data.yValue.times(yScalableFactor) if (clickedBar.value.x in (topLeft.x..topRight.x)) { @@ -101,7 +101,7 @@ fun BarChart( ) // draw label if (axisConfig.showXLabels) { - drawBarLabel(data, barWidth.value, barHeight, topLeft, barData.count()) + drawBarLabel(data.xValue, barWidth.value, barHeight, topLeft, barData.count()) } } } diff --git a/charty/src/main/java/com/himanshoe/charty/bar/GroupedBarChart.kt b/charty/src/main/java/com/himanshoe/charty/bar/GroupedBarChart.kt index bb57682..d5b7b54 100644 --- a/charty/src/main/java/com/himanshoe/charty/bar/GroupedBarChart.kt +++ b/charty/src/main/java/com/himanshoe/charty/bar/GroupedBarChart.kt @@ -69,8 +69,8 @@ fun GroupedBarChart( groupedBarData.flatMap { it.barData } .forEachIndexed { index, data -> - val topLeft = getTopLeft(index, barWidth, size, data, yScalableFactor) - val topRight = getTopRight(index, barWidth, size, data, yScalableFactor) + val topLeft = getTopLeft(index, barWidth.value, size, data.yValue, yScalableFactor) + val topRight = getTopRight(index, barWidth.value, size, data.yValue, yScalableFactor) val barHeight = data.yValue.times(yScalableFactor) if (clickedBar.value.x in (topLeft.x..topRight.x)) { @@ -84,7 +84,7 @@ fun GroupedBarChart( ) // draw label if (axisConfig.showXLabels) { - drawBarLabel(data, barWidth.value, barHeight, topLeft, groupedBarData.count()) + drawBarLabel(data.xValue, barWidth.value, barHeight, topLeft, groupedBarData.count()) } } } diff --git a/charty/src/main/java/com/himanshoe/charty/bar/StackedBarChart.kt b/charty/src/main/java/com/himanshoe/charty/bar/StackedBarChart.kt index 2a1ca9f..8481787 100644 --- a/charty/src/main/java/com/himanshoe/charty/bar/StackedBarChart.kt +++ b/charty/src/main/java/com/himanshoe/charty/bar/StackedBarChart.kt @@ -1,3 +1,158 @@ package com.himanshoe.charty.bar -class StackedBarChart +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.input.pointer.pointerInput +import com.himanshoe.charty.bar.common.calculations.getStackedTopLeft +import com.himanshoe.charty.bar.common.calculations.getTopLeft +import com.himanshoe.charty.bar.common.calculations.getTopRight +import com.himanshoe.charty.bar.common.component.drawBarLabel +import com.himanshoe.charty.bar.config.BarConfig +import com.himanshoe.charty.bar.config.BarConfigDefaults +import com.himanshoe.charty.bar.model.StackedBarData +import com.himanshoe.charty.bar.model.isValid +import com.himanshoe.charty.bar.model.maxYValue +import com.himanshoe.charty.common.axis.AxisConfig +import com.himanshoe.charty.common.axis.AxisConfigDefaults +import com.himanshoe.charty.common.axis.yAxis +import com.himanshoe.charty.common.dimens.ChartDimens +import com.himanshoe.charty.common.dimens.ChartDimensDefaults + +@Composable +fun StackedBarChart( + stackBarData: List, + colors: List, + modifier: Modifier = Modifier, + onBarClick: (StackedBarData) -> Unit = {}, + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + barConfig: BarConfig = BarConfigDefaults.barConfigDimesDefaults() +) { + if (stackBarData.isValid(colors.count()).not()) { + throw IllegalArgumentException("Colors count should be total to number of values in StackedBarData's yValue") + } + + val maxYValueState = rememberSaveable { mutableStateOf(stackBarData.maxYValue()) } + val maxYValue = maxYValueState.value + val barWidth = remember { mutableStateOf(0F) } + val clickedBar = remember { mutableStateOf(Offset(-10F, -10F)) } + + Canvas( + modifier = modifier + .drawBehind { + if (axisConfig.showAxis) { + yAxis(axisConfig, maxYValue) + } + } + .padding(horizontal = chartDimens.padding) + .pointerInput(Unit) { + detectTapGestures(onPress = { offset -> + clickedBar.value = offset + }) + } + ) { + barWidth.value = size.width.div(stackBarData.count().times(1.2F)) + val yScalableFactor = size.height.div(maxYValue) + + stackBarData.reversed().forEachIndexed { index, stackBarDataIndividual -> + drawIndividualStackedBar( + index, + stackBarDataIndividual, + barWidth.value, + yScalableFactor, + barConfig, + colors + ) + } + drawLabels( + stackBarData, + yScalableFactor, + barWidth.value, + axisConfig, + clickedBar.value, + onBarClick + ) + } +} + +private fun DrawScope.drawLabels( + stackBarData: List, + yScalableFactor: Float, + width: Float, + axisConfig: AxisConfig, + clickedBarValue: Offset, + onBarClick: (StackedBarData) -> Unit, +) { + stackBarData.forEachIndexed { index, stackBarDataIndividual -> + val barHeight = stackBarDataIndividual.yValue.sum().times(yScalableFactor) + val barTopLeft = getTopLeft( + index, + width, + size, + stackBarDataIndividual.yValue.sum(), + yScalableFactor + ) + val barTopRight = getTopRight( + index, + width, + size, + stackBarDataIndividual.yValue.sum(), + yScalableFactor + ) + if (clickedBarValue.x in (barTopLeft.x..barTopRight.x)) { + onBarClick(stackBarDataIndividual) + } + if (axisConfig.showXLabels) { + drawBarLabel( + stackBarDataIndividual.xValue, + width.times(1.4F), + barHeight, + barTopLeft, + stackBarData.count() + ) + } + } +} + +private fun DrawScope.drawIndividualStackedBar( + index: Int, + stackBarData: StackedBarData, + barWidth: Float, + yScalableFactor: Float, + barConfig: BarConfig, + colors: List, +) { + + var individualHeight = 0F + stackBarData.yValue.forEachIndexed { individualIndex, value -> + val individualBarHeight = value.times(yScalableFactor) + val topLeft = getStackedTopLeft( + index, + barWidth, + individualHeight, + ) + rotate(180F) { + drawRoundRect( + cornerRadius = CornerRadius(if (barConfig.hasRoundedCorner) individualBarHeight else 0F), + topLeft = topLeft, + color = colors[individualIndex], + size = Size(barWidth, individualBarHeight) + ) + } + + individualHeight += (individualBarHeight) + } +} diff --git a/charty/src/main/java/com/himanshoe/charty/bar/common/calculations/BarCalculations.kt b/charty/src/main/java/com/himanshoe/charty/bar/common/calculations/BarCalculations.kt index fe4e9a4..852372b 100644 --- a/charty/src/main/java/com/himanshoe/charty/bar/common/calculations/BarCalculations.kt +++ b/charty/src/main/java/com/himanshoe/charty/bar/common/calculations/BarCalculations.kt @@ -1,28 +1,35 @@ package com.himanshoe.charty.bar.common.calculations -import androidx.compose.runtime.MutableState import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size -import com.himanshoe.charty.bar.model.BarData internal fun getTopLeft( index: Int, - barWidth: MutableState, + barWidth: Float, size: Size, - barData: BarData, + yValue: Float, yScalableFactor: Float ) = Offset( - x = index.times(barWidth.value.times(1.2F)), - y = size.height.minus(barData.yValue.times(yScalableFactor)) + x = index.times(barWidth.times(1.2F)), + y = size.height.minus(yValue.times(yScalableFactor)) +) + +internal fun getStackedTopLeft( + index: Int, + barWidth: Float, + barHeight: Float, +) = Offset( + x = index.times(barWidth.times(1.2F)), + y = (barHeight) ) internal fun getTopRight( index: Int, - barWidth: MutableState, + barWidth: Float, size: Size, - barData: BarData, + yValue: Float, yScaleFactor: Float ) = Offset( - x = index.plus(1).times(barWidth.value.times(1.2F)), - y = size.height.minus(barData.yValue.times(yScaleFactor)) + x = index.plus(1).times(barWidth.times(1.2F)), + y = size.height.minus(yValue.times(yScaleFactor)) ) diff --git a/charty/src/main/java/com/himanshoe/charty/bar/common/component/Labels.kt b/charty/src/main/java/com/himanshoe/charty/bar/common/component/Labels.kt index f24c5d2..6313a28 100644 --- a/charty/src/main/java/com/himanshoe/charty/bar/common/component/Labels.kt +++ b/charty/src/main/java/com/himanshoe/charty/bar/common/component/Labels.kt @@ -5,23 +5,24 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.nativeCanvas -import com.himanshoe.charty.bar.model.BarData internal fun DrawScope.drawBarLabel( - data: BarData, + xValue: Any, barWidth: Float, barHeight: Float, topLeft: Offset, count: Int ) { + val heightDisplacement = if (count < 7) barWidth.div(4F) else barWidth.div(2) val divisibleFactor = if (count > 10) count else 1 - val textSizeFactor = if (count > 10) 3 else 30 + val textSizeFactor = if (count > 10) 3 else 28 + drawIntoCanvas { it.nativeCanvas.apply { drawText( - data.xValue.toString(), + xValue.toString(), topLeft.x.plus(barWidth.div(2)), - topLeft.y.plus(barHeight.plus(barWidth.div(2))), + topLeft.y.plus(barHeight.plus(heightDisplacement)), Paint().apply { textSize = size.width.div(textSizeFactor).div(divisibleFactor) textAlign = Paint.Align.CENTER diff --git a/charty/src/main/java/com/himanshoe/charty/bar/model/StackedBarData.kt b/charty/src/main/java/com/himanshoe/charty/bar/model/StackedBarData.kt index a14dfac..96ae9c9 100644 --- a/charty/src/main/java/com/himanshoe/charty/bar/model/StackedBarData.kt +++ b/charty/src/main/java/com/himanshoe/charty/bar/model/StackedBarData.kt @@ -1,3 +1,16 @@ package com.himanshoe.charty.bar.model -data class StackedBarData(val xValue: Any, val yValue: List) +data class StackedBarData( + val xValue: Any, + val yValue: List, +) + +internal fun List.isValid(count: Int) = totalItems() == count + +private fun List.totalItems(): Int = this.maxOf { + it.yValue.count() +} + +internal fun List.maxYValue() = maxOf { + it.yValue.sum() +} diff --git a/charty/src/main/java/com/himanshoe/charty/common/axis/AxisConfig.kt b/charty/src/main/java/com/himanshoe/charty/common/axis/AxisConfig.kt index 80c0d3c..6afa947 100644 --- a/charty/src/main/java/com/himanshoe/charty/common/axis/AxisConfig.kt +++ b/charty/src/main/java/com/himanshoe/charty/common/axis/AxisConfig.kt @@ -18,7 +18,7 @@ internal object AxisConfigDefaults { showAxis = true, isAxisDashed = false, showUnitLabels = true, - showXLabels = false, + showXLabels = true, yAxisColor = Color.LightGray, ) } diff --git a/charty/src/main/java/com/himanshoe/charty/pie/PieChart.kt b/charty/src/main/java/com/himanshoe/charty/pie/PieChart.kt index d5b6d58..12bf941 100644 --- a/charty/src/main/java/com/himanshoe/charty/pie/PieChart.kt +++ b/charty/src/main/java/com/himanshoe/charty/pie/PieChart.kt @@ -95,7 +95,7 @@ fun PieChart( useCenter = true, ) } else { - val isClickedAndEnabled = config.expandDonutOnClick && currentPie.value != -1 && currentPie.value == index + val isClickedAndEnabled = config.expandDonutOnClick && currentPie.value != -1 && currentPie.value == index if (isClickedAndEnabled) { drawPieSection( pieChartPortion[currentPie.value], diff --git a/docs/StackedBarChart.md b/docs/StackedBarChart.md new file mode 100644 index 0000000..6b0187c --- /dev/null +++ b/docs/StackedBarChart.md @@ -0,0 +1,29 @@ +## BarChart + +> The stacked bar chart (aka stacked bar graph) extends the standard bar chart from looking at numeric values across one categorical variable to two. + +### Using StackedBarChart in your project: + +```kotlin + StackedBarChart( + modifier = Modifier, + onBarClick = { // handle Click for individual bar} + colors = // colors, + stackBarData = // list of stackBarData + ) +``` + +### Creating Data Set: + +to create a data set you need to pass List of `StackedBarData`, where `StackedBarData` looks like: +```kotlin +data class StackedBarData(val xValue: Any, val yValue: List) +``` +Here, `xValue` will be used as Labels and `yValue` will represent the bars. + +Also, count of `yValue` and count of Colors should be same or it will throw an exception. + +### Additional Configuration (Optional) +- To add padding the the chart, you can also use `ChartDimens` +- To edit Config of the Axis, to suit your need to use `AxisConfig` +- To edit Individual Bar config of it having corner radius you need to use `BarConfig`