Skip to content

Commit

Permalink
Merge pull request #130 from hi-manshu/feat/barchart-v2
Browse files Browse the repository at this point in the history
Updated BarChart/HorizontalBar/LineBarChart
  • Loading branch information
hi-manshu authored Feb 13, 2025
2 parents 9233a9b + efe4b1f commit 6a9cd0d
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 33 deletions.
2 changes: 1 addition & 1 deletion charty/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ POM_DESCRIPTION=An Elementary Compose Multiplatform Chart library.
POM_PACKAGING=aar
POM_INCEPTION_YEAR=2025
GROUP=com.himanshoe
VERSION_NAME=2.1.0-beta02
VERSION_NAME=2.1.0-beta03
VERSION_CODE=2
POM_URL=/~https://github.com/hi-manshu
POM_LICENCE_NAME=The Apache Software License, Version 2.0
Expand Down
88 changes: 76 additions & 12 deletions charty/src/commonMain/kotlin/com/himanshoe/charty/bar/BarChart.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,21 +97,29 @@ private fun BarChartContent(
val textMeasurer = rememberTextMeasurer()
val bottomPadding = if (labelConfig.showXLabel && !hasNegativeValues) 8.dp else 0.dp
val leftPadding = if (labelConfig.showYLabel) 24.dp else 0.dp
val topPadding = if (labelConfig.showTooltip) 24.dp else 0.dp

var clickedOffset by mutableStateOf(Offset.Zero)
var clickedBarIndex by mutableIntStateOf(-1)

BarChartCanvasScaffold(
modifier = modifier.padding(bottom = bottomPadding, start = leftPadding),
modifier = modifier.padding(
bottom = bottomPadding,
start = leftPadding,
top = topPadding,
),
showAxisLines = barChartConfig.showAxisLines,
showRangeLines = barChartConfig.showGridLines,
axisLineColor = barChartColorConfig.axisLineColor,
rangeLineColor = barChartColorConfig.gridLineColor,
canDrawNegativeChart = canDrawNegativeChart,
labelConfig = labelConfig,
onClick = { clickedOffset = it },
onClick = { offset ->
clickedOffset = offset.copy(y = offset.y)
},
data = { displayData },
) { canvasHeight, gap, barWidth ->

target?.let {
require(it in minValue..maxValue) { "Target value should be between $minValue and $maxValue" }
val targetLineY = if (hasNegativeValues) canvasHeight / 2 else canvasHeight
Expand All @@ -133,7 +141,7 @@ private fun BarChartContent(
yAxis = yAxis,
height = height,
canvasHeight = canvasHeight,
maxHeight = maxHeight
maxHeight = maxHeight,
)
val color = getBarColor(
barData = barData,
Expand All @@ -150,24 +158,28 @@ private fun BarChartContent(
canDrawNegativeChart = canDrawNegativeChart
)

val textCharCount =
if (barData.xValue.toString().length >= 3) if (displayData.count() <= 7) 3 else 1 else 1
val textCharCount = getTextCharCount(labelConfig, barData, displayData)

val textSizeFactor = if (displayData.count() <= 13) 4 else 2
val textLayoutResult = textMeasurer.measure(
text = barData.xValue.toString().take(textCharCount),
style = TextStyle(fontSize = (barWidth / textSizeFactor).toSp()),
style = labelConfig.labelTextStyle ?: TextStyle(
fontSize = (barWidth / textSizeFactor).toSp(),
brush = Brush.linearGradient(labelConfig.textColor.value)
),
overflow = TextOverflow.Clip,
maxLines = 1,
)
val (textOffsetY, cornerRadius) = getTextYOffsetAndCornerRadius(

val (textOffsetY, calculatedCornerRadius) = getTextYOffsetAndCornerRadius(
barData = barData,
individualBarTopLeft = individualBarTopLeft,
textLayoutResult = textLayoutResult,
individualBarRectSize = individualBarRectSize,
barChartConfig = barChartConfig,
barWidth = barWidth
)

val cornerRadius = getCornerRadius(barChartConfig, calculatedCornerRadius)
if (isClickInsideBar(clickedOffset, individualBarTopLeft, individualBarRectSize)) {
clickedBarIndex = index
onBarClick(index, barData)
Expand Down Expand Up @@ -209,18 +221,69 @@ private fun BarChartContent(
) { "X value should not be empty" }
drawText(
textLayoutResult = textLayoutResult,
brush = Brush.linearGradient(labelConfig.textColor.value),
topLeft = Offset(
x = individualBarTopLeft.x + barWidth / 2 - textLayoutResult.size.width / 2,
y = textOffsetY,
),
)
}
if (labelConfig.showYLabel) {
val yLabelTextLayoutResult = textMeasurer.measure(
text = barData.yValue.toString(),
style = labelConfig.labelTextStyle ?: TextStyle(
fontSize = (barWidth / textSizeFactor).toSp(),
brush = Brush.linearGradient(labelConfig.textColor.value)
),
overflow = TextOverflow.Clip,
maxLines = 1,
)
drawText(
textLayoutResult = yLabelTextLayoutResult,
topLeft = Offset(
x = individualBarTopLeft.x + barWidth / 2 - yLabelTextLayoutResult.size.width / 2,
y = individualBarTopLeft.y - yLabelTextLayoutResult.size.height,
),
)
}

if (labelConfig.showTooltip) {
val yValueTextLayoutResult = textMeasurer.measure(
text = barData.yValue.toInt().toString(),
style = labelConfig.labelTextStyle ?: TextStyle(
fontSize = (barWidth / textSizeFactor).toSp(),
brush = Brush.linearGradient(labelConfig.textColor.value)
),
overflow = TextOverflow.Clip,
maxLines = 1,
)
drawText(
textLayoutResult = yValueTextLayoutResult,
topLeft = Offset(
x = individualBarTopLeft.x + barWidth / 2 - yValueTextLayoutResult.size.width / 2,
y = backgroundTopLeftY - yValueTextLayoutResult.size.height - gap,
),
)
}
}
}
}
}

private fun getCornerRadius(
barChartConfig: BarChartConfig,
cornerRadius: CornerRadius
) = barChartConfig.cornerRadius ?: cornerRadius

private fun getTextCharCount(
labelConfig: LabelConfig,
barData: BarData,
displayData: List<BarData>
) = labelConfig.xAxisCharCount ?: if (barData.xValue.toString().length >= 3) {
if (displayData.count() <= 7) 3 else 1
} else {
1
}

internal fun getTextYOffsetAndCornerRadius(
barData: BarData,
individualBarTopLeft: Offset,
Expand Down Expand Up @@ -260,8 +323,9 @@ private fun getBarTopLeftAndRectSize(
val heightAdjustment =
if (isClickedBar) height.absoluteValue * 0.02F / (if (canDrawNegativeChart) 2 else 1) else 0f

val xOffset = index * (barWidth + gap) - barWidthAdjustment / 2
val individualBarTopLeft = Offset(
x = index * (barWidth + gap) - barWidthAdjustment / 2,
x = xOffset,
y = if (barData.yValue < 0) {
topLeftY
} else {
Expand Down Expand Up @@ -290,7 +354,7 @@ internal fun getBarAndBackgroundBarTopLeft(
yAxis: Float,
height: Float,
canvasHeight: Float,
maxHeight: Float
maxHeight: Float,
): Pair<Float, Float> {
val topLeftY = if (canDrawNegativeChart) {
if (barData.yValue < 0) yAxis else yAxis - height / 2
Expand Down Expand Up @@ -361,7 +425,6 @@ internal fun isClickInsideBar(
* @param showAxisLines A boolean indicating whether to show axis lines.
* @param showRangeLines A boolean indicating whether to show range lines.
* @param canDrawNegativeChart A boolean indicating whether the chart can draw negative values.
* @param displayDataCount The number of data points to be displayed.
* @param axisLineColor The color of the axis lines.
* @param rangeLineColor The color of the range lines.
* @param onClick A lambda function to handle click events on the canvas.
Expand Down Expand Up @@ -402,6 +465,7 @@ internal fun BarChartCanvasScaffold(
minValue = minValue,
step = step,
maxValue = maxValue,
labelTextStyle = labelConfig.labelTextStyle,
labelColor = labelConfig.textColor,
textMeasurer = textMeasurer,
count = barData.count()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ fun HorizontalBarChart(
)
}


@Composable
private fun HorizontalBarChartContent(
data: () -> List<BarData>,
Expand Down Expand Up @@ -144,6 +145,7 @@ private fun HorizontalBarChartContent(
brush = brush,
showCurvedBar = barChartConfig.showCurvedBar,
isNegative = barData.yValue < 0,
barChartConfig = barChartConfig,
allNegativeValues = allNegativeValues,
allPositiveValues = allPositiveValues
)
Expand Down Expand Up @@ -183,15 +185,20 @@ private fun DrawScope.drawBar(
allNegativeValues: Boolean,
allPositiveValues: Boolean,
showCurvedBar: Boolean,
barChartConfig: BarChartConfig,
) {
val cornerRadius = if (showCurvedBar) {
val calculatedCornerRadius = if (showCurvedBar) {
CornerRadius(
x = size.height / 2,
y = size.height / 2
)
} else {
CornerRadius.Zero
}
val cornerRadius = getCornerRadius(
barChartConfig = barChartConfig,
cornerRadius = calculatedCornerRadius
)
val rightCornerRadius =
if (!isNegative || allPositiveValues || allNegativeValues) cornerRadius else CornerRadius.Zero
val leftCornerRadius =
Expand Down Expand Up @@ -297,6 +304,11 @@ private fun DrawScope.drawLabel(
}
}

private fun getCornerRadius(
barChartConfig: BarChartConfig,
cornerRadius: CornerRadius
) = barChartConfig.cornerRadius ?: cornerRadius

private data class BarDimensions(
val barWidth: Float,
val additionalWidth: Float,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,25 @@ private fun LineBarChartContent(
canDrawNegativeChart = canDrawNegativeChart
)

val textCharCount = if (displayData.count() <= 7) 3 else 1
val textCharCount = getTextCharCount(labelConfig, barData, displayData)
val textSizeFactor = if (displayData.count() <= 13) 4 else 2

val textLayoutResult = textMeasurer.measure(
text = barData.xValue.toString().take(textCharCount),
style = TextStyle(fontSize = (barWidth / textSizeFactor).toSp()),
overflow = TextOverflow.Clip,
maxLines = 1,
)
val (textOffsetY, cornerRadius) = getTextYOffsetAndCornerRadius(
val (textOffsetY, calculatedCornerRadius) = getTextYOffsetAndCornerRadius(
barData = barData,
individualBarTopLeft = singleBarTopLeft,
textLayoutResult = textLayoutResult,
individualBarRectSize = singleBarRectSize,
barChartConfig = barChartConfig,
barWidth = barWidth
)
val cornerRadius = getCornerRadius(barChartConfig, calculatedCornerRadius)


if (isClickInsideBar(clickedOffset, singleBarTopLeft, singleBarRectSize)) {
clickedBarIndex = index
Expand Down Expand Up @@ -196,6 +199,20 @@ private fun LineBarChartContent(
}
}
}
private fun getCornerRadius(
barChartConfig: BarChartConfig,
cornerRadius: CornerRadius
) = barChartConfig.cornerRadius ?: cornerRadius

private fun getTextCharCount(
labelConfig: LabelConfig,
barData: BarData,
displayData: List<BarData>
) = labelConfig.xAxisCharCount ?: if (barData.xValue.toString().length >= 3) {
if (displayData.count() <= 7) 3 else 1
} else {
1
}

private fun getBarTopLeftAndRectSize(
index: Int,
Expand All @@ -204,7 +221,7 @@ private fun getBarTopLeftAndRectSize(
topLeftY: Float,
clickedBarIndex: Int,
height: Float,
canDrawNegativeChart: Boolean
canDrawNegativeChart: Boolean,
): Pair<Offset, Size> {
val individualBarTopLeft = Offset(
x = index * barWidth + (barWidth - barWidth / 3) / 2,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.himanshoe.charty.bar.config

import androidx.compose.ui.geometry.CornerRadius

/**
* Configuration class for bar chart settings.
*
Expand All @@ -15,20 +17,21 @@ data class BarChartConfig(
val drawNegativeValueChart: Boolean,
val showCurvedBar: Boolean,
val minimumBarCount: Int,
val cornerRadius: CornerRadius?
) {
companion object {
/**
* Provides a default configuration for bar chart settings.
*
* @return A `BarChartConfig` object with default settings.
*/
fun default() =
BarChartConfig(
showAxisLines = true,
showGridLines = true,
drawNegativeValueChart = true,
showCurvedBar = false,
minimumBarCount = 7,
)
fun default() = BarChartConfig(
showAxisLines = true,
showGridLines = true,
drawNegativeValueChart = true,
showCurvedBar = false,
minimumBarCount = 7,
cornerRadius = null
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ internal fun Modifier.drawYAxisLabel(
textMeasurer: TextMeasurer,
count: Int,
labelColor: ChartColor = Color.Black.asSolidChartColor(),
labelTextStyle: TextStyle?,
): Modifier =
this.drawWithCache {
onDrawBehind {
Expand All @@ -155,7 +156,10 @@ internal fun Modifier.drawYAxisLabel(
val y = size.height - ((value - minValue) / (maxValue - minValue)) * size.height
val textLayoutResult = textMeasurer.measure(
text = displayValue,
style = TextStyle(fontSize = (size.width / count / 10).sp),
style = labelTextStyle ?: TextStyle(
brush = Brush.linearGradient(labelColor.value),
fontSize = (size.width / count / 10).sp
),
overflow = TextOverflow.Clip,
maxLines = 1,
)
Expand All @@ -165,8 +169,8 @@ internal fun Modifier.drawYAxisLabel(
-textLayoutResult.size.width - 8f,
y - textLayoutResult.size.height / 2
),
brush = Brush.linearGradient(labelColor.value)
)

)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.himanshoe.charty.common

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle

/**
* Data class representing the configuration for labels in a chart.
Expand All @@ -13,6 +14,9 @@ data class LabelConfig(
val textColor: ChartColor,
val showXLabel: Boolean,
val showYLabel: Boolean,
val showTooltip: Boolean,
val xAxisCharCount: Int?,
val labelTextStyle: TextStyle?,
) {
companion object {
/**
Expand All @@ -23,7 +27,10 @@ data class LabelConfig(
fun default() = LabelConfig(
textColor = Color.Black.asSolidChartColor(),
showXLabel = false,
xAxisCharCount = null,
labelTextStyle = null,
showYLabel = false,
showTooltip = false,
)
}
}
Loading

0 comments on commit 6a9cd0d

Please sign in to comment.