Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,34 @@ fun BarChartDefaultPreview() {
@Composable
fun BarChartCustomPreview() {
ScreenshotSurface {
val dataSet = SCREENSHOT_BAR_SAMPLE_USE_CASE.initialBarDataSet()
BarChart(
dataSet = SCREENSHOT_BAR_SAMPLE_USE_CASE.initialBarDataSet(),
dataSet = dataSet,
style = ChartTestStyleFixtures.barCustomStyle(chartViewStyle = ChartViewDefaults.style()),
animateOnStart = SCREENSHOT_ANIMATE_ON_START,
)
}
}

@PreviewTest
@ScreenshotPreview
@Composable
fun BarChartCustomBarColorsPreview() {
ScreenshotSurface {
val dataSet = SCREENSHOT_BAR_SAMPLE_USE_CASE.initialBarDataSet()
BarChart(
dataSet = dataSet,
style =
ChartTestStyleFixtures.barCustomStyle(
chartViewStyle = ChartViewDefaults.style(),
barCount = dataSet.data.item.points.size,
useBarColors = true,
),
animateOnStart = SCREENSHOT_ANIMATE_ON_START,
)
}
}

@PreviewTest
@ScreenshotPreview
@Composable
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,29 @@ object BarChartStyleItems {

@Composable
fun customStyle(
barCount: Int,
minValue: Float,
maxValue: Float,
aspectRatioPreset: ChartAspectRatioPreset = ChartAspectRatioPreset.Square,
) = ChartTestStyleFixtures.barCustomStyle(
chartViewStyle = chartViewStyle(aspectRatioPreset),
barCount = barCount,
useBarColors = true,
minValue = minValue,
maxValue = maxValue,
)

@Composable
fun custom(
barCount: Int,
minValue: Float,
maxValue: Float,
aspectRatioPreset: ChartAspectRatioPreset = ChartAspectRatioPreset.Square,
): StyleItems =
ChartStyleItems(
currentStyle =
customStyle(
barCount = barCount,
minValue = minValue,
maxValue = maxValue,
aspectRatioPreset = aspectRatioPreset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fun BarChartDemo(
ChartPreset.Default -> BarChartStyleItems.default(aspectRatioPreset)
ChartPreset.Custom ->
BarChartStyleItems.custom(
barCount = dataSet.data.item.points.size,
minValue = controlsState.minValue.toFloat(),
maxValue = controlsState.maxValue.toFloat(),
aspectRatioPreset = aspectRatioPreset,
Expand Down Expand Up @@ -120,6 +121,7 @@ fun BarChartDemo(
dataSet = dataSet,
style =
BarChartStyleItems.customStyle(
barCount = dataSet.data.item.points.size,
minValue = controlsState.minValue.toFloat(),
maxValue = controlsState.maxValue.toFloat(),
aspectRatioPreset = aspectRatioPreset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ fun BarChart(
selectedBarIndex: Int = NO_SELECTION,
) {
val errors =
remember(dataSet) {
remember(dataSet, style.barColors) {
validateBarData(
data = dataSet.data.item,
colorsSize = style.barColors.size,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.github.dautovicharis.charts.internal

import io.github.dautovicharis.charts.internal.ValidationErrors.MIN_REQUIRED_BAR
import io.github.dautovicharis.charts.internal.ValidationErrors.RULE_COLORS_SIZE_MISMATCH
import io.github.dautovicharis.charts.internal.common.model.ChartData

@InternalChartsApi
fun validateBarData(data: ChartData): List<String> {
fun validateBarData(
data: ChartData,
colorsSize: Int = 0,
): List<String> {
val validationErrors = mutableListOf<String>()
val pointsSize = data.points.size

Expand All @@ -15,6 +19,12 @@ fun validateBarData(data: ChartData): List<String> {
return validationErrors
}

if (colorsSize > 0 && colorsSize != pointsSize) {
val validationError =
RULE_COLORS_SIZE_MISMATCH.format(colorsSize, pointsSize)
validationErrors.add(validationError)
}

data.points.forEachIndexed { index, value ->
if (value.isNaN()) {
val validationError = ValidationErrors.RULE_DATA_POINT_NOT_NUMBER.format(index)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ internal fun BarChart(
selectedBarIndex: Int = NO_SELECTION,
onValueChanged: (Int) -> Unit = {},
) {
val barColor = style.barColor.copy(alpha = style.barAlpha)
val baseBarColor = style.barColor.copy(alpha = style.barAlpha)
val sourceBarColors =
remember(style.barColors, style.barAlpha) {
style.barColors.map { color -> color.copy(alpha = style.barAlpha) }
}
val isPreview = LocalInspectionMode.current
val sourceDataSize = chartData.points.size
BoxWithConstraints(modifier = style.modifier) {
Expand Down Expand Up @@ -87,6 +91,17 @@ internal fun BarChart(
}
var denseExpanded by rememberDenseExpandedState(isDenseModeAvailable = isDenseData)
val compactDenseMode = isDenseData && !denseExpanded
val compactBarCenterIndices =
remember(sourceDataSize, compactDenseMode, maxFitBars) {
if (compactDenseMode) {
compactDensityCenterIndices(
sourcePointsCount = sourceDataSize,
targetPoints = maxFitBars,
)
} else {
emptyList()
}
}
val renderData =
remember(chartData, compactDenseMode, maxFitBars) {
if (compactDenseMode) {
Expand All @@ -98,6 +113,17 @@ internal fun BarChart(
chartData
}
}
val renderBarColors =
remember(sourceBarColors, compactBarCenterIndices, compactDenseMode, baseBarColor) {
when {
sourceBarColors.isEmpty() -> emptyList()
!compactDenseMode -> sourceBarColors
else ->
compactBarCenterIndices.map { centerIndex ->
sourceBarColors.getOrElse(centerIndex) { baseBarColor }
}
}
}
val dataSize = renderData.points.size
val hasFixedRange = style.minValue != null || style.maxValue != null
val (fixedMin, fixedMax) =
Expand Down Expand Up @@ -214,7 +240,8 @@ internal fun BarChart(
interactionEnabled = interactionEnabled,
dragSelectionEnabled = !isScrollable,
animatedValues = animatedValues,
barColor = barColor,
barColors = renderBarColors,
defaultBarColor = baseBarColor,
fixedMin = fixedMin,
fixedMax = fixedMax,
isScrollable = isScrollable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ internal fun BarChartContent(
interactionEnabled: Boolean,
dragSelectionEnabled: Boolean,
animatedValues: List<Animatable<Float, AnimationVector1D>>,
barColor: Color,
barColors: List<Color>,
defaultBarColor: Color,
fixedMin: Double,
fixedMax: Double,
isScrollable: Boolean,
Expand Down Expand Up @@ -282,7 +283,8 @@ internal fun BarChartContent(
animatedValues = animatedValues,
visibleRange = visibleRange,
selectedIndex = selectedIndex,
barColor = barColor,
barColors = barColors,
defaultBarColor = defaultBarColor,
maxValue = fixedMax,
minValue = fixedMin,
barWidthPx = barWidthPx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ internal fun DrawScope.drawBars(
animatedValues: List<Animatable<Float, AnimationVector1D>>,
visibleRange: IntRange,
selectedIndex: Int,
barColor: Color,
barColors: List<Color>,
defaultBarColor: Color,
maxValue: Double,
minValue: Double,
barWidthPx: Float,
Expand Down Expand Up @@ -70,9 +71,10 @@ internal fun DrawScope.drawBars(
val barHeight = abs(value) * size.height
val top = if (value >= 0f) clampedBaselineY - barHeight else clampedBaselineY
val left = unitWidth * index
val resolvedBarColor = barColors.getOrNull(index) ?: defaultBarColor

drawRect(
color = barColor,
color = resolvedBarColor,
topLeft = Offset(x = left, y = top),
size =
androidx.compose.ui.geometry.Size(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ internal fun aggregateForCompactDensity(
return aggregatedPoints.toChartData(labels = aggregatedLabels)
}

internal fun compactDensityCenterIndices(
sourcePointsCount: Int,
targetPoints: Int = BAR_DENSE_THRESHOLD,
): List<Int> {
if (sourcePointsCount <= 0) return emptyList()
if (targetPoints <= 1 || sourcePointsCount <= targetPoints) {
return List(sourcePointsCount) { index -> index }
}

val bucketSize = bucketSizeForTargetCore(totalPoints = sourcePointsCount, targetPoints = targetPoints)
val bucketRanges = buildBucketRangesCore(totalPoints = sourcePointsCount, bucketSize = bucketSize)
return bucketRanges.map { range ->
range.first + ((range.last - range.first) / 2)
}
}

internal fun maxBarsThatFit(
viewportWidthPx: Float,
spacingPx: Float,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.sp
* @property modifier The modifier to be applied to the chart.
* @property chartViewStyle The style to be applied to the chart view.
* @property barColor The color to be used for the bars in the chart.
* @property barColors Optional explicit colors used for individual bars.
* @property barAlpha The alpha value applied to rendered bars.
* @property space The space between the bars in the chart.
* @property minValue Optional fixed minimum value for the chart scale.
Expand Down Expand Up @@ -46,6 +47,7 @@ class BarChartStyle(
val modifier: Modifier,
val chartViewStyle: ChartViewStyle,
val barColor: Color,
val barColors: List<Color>,
val barAlpha: Float,
val space: Dp,
val minValue: Float?,
Expand Down Expand Up @@ -77,6 +79,7 @@ class BarChartStyle(
override fun getProperties(): List<Pair<String, Any>> =
listOf(
BarChartStyle::barColor.name to barColor,
BarChartStyle::barColors.name to barColors,
BarChartStyle::barAlpha.name to barAlpha,
BarChartStyle::space.name to space,
BarChartStyle::minValue.name to (minValue ?: "auto"),
Expand Down Expand Up @@ -127,6 +130,7 @@ object BarChartDefaults {
* Returns a BarChartStyle with the provided parameters or their default values.
*
* @param barColor The color to be used for the bars in the chart. Defaults to the primary color of the MaterialTheme.
* @param barColors Optional explicit colors used for individual bars. Defaults to an empty list.
* @param barAlpha The alpha value applied to rendered bars. Defaults to 0.4f in light theme and 0.6f in dark theme.
* @param space The space between the bars in the chart. Defaults to 10.dp.
* @param minValue Optional fixed minimum value for the chart scale. Defaults to null.
Expand Down Expand Up @@ -156,6 +160,7 @@ object BarChartDefaults {
@Composable
fun style(
barColor: Color = MaterialTheme.colorScheme.primary,
barColors: List<Color> = emptyList(),
barAlpha: Float = defaultChartAlpha(),
space: Dp = 10.dp,
minValue: Float? = null,
Expand Down Expand Up @@ -187,6 +192,7 @@ object BarChartDefaults {
return BarChartStyle(
modifier = modifier,
barColor = barColor,
barColors = barColors,
barAlpha = barAlpha.coerceIn(0f, 1f),
space = space,
minValue = minValue,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.dautovicharis.charts.ui

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.isDisplayed
Expand All @@ -9,13 +10,15 @@ import androidx.compose.ui.test.runComposeUiTest
import io.github.dautovicharis.charts.BarChart
import io.github.dautovicharis.charts.internal.TestTags
import io.github.dautovicharis.charts.internal.ValidationErrors.MIN_REQUIRED_BAR
import io.github.dautovicharis.charts.internal.ValidationErrors.RULE_COLORS_SIZE_MISMATCH
import io.github.dautovicharis.charts.internal.ValidationErrors.RULE_DATA_POINTS_LESS_THAN_MIN
import io.github.dautovicharis.charts.internal.common.model.ChartDataType
import io.github.dautovicharis.charts.internal.format
import io.github.dautovicharis.charts.mock.MockTest.TITLE
import io.github.dautovicharis.charts.mock.MockTest.dataSet
import io.github.dautovicharis.charts.model.ChartDataSet
import io.github.dautovicharis.charts.model.toChartDataSet
import io.github.dautovicharis.charts.style.BarChartDefaults
import kotlin.test.Test
import kotlin.test.assertTrue

Expand Down Expand Up @@ -104,4 +107,47 @@ class BarChartTest {

assertTrue(lastLabelBounds.right <= axisBounds.right - 1f)
}

@OptIn(ExperimentalTestApi::class)
@Test
fun barChart_withMatchingBarColors_displaysChart() =
runComposeUiTest {
setContent {
val style =
BarChartDefaults.style(
barColors = listOf(Color.Red, Color.Green, Color.Blue, Color.Cyan),
)
BarChart(
dataSet = dataSet,
style = style,
)
}

onNodeWithTag(TestTags.BAR_CHART).isDisplayed()
}

@OptIn(ExperimentalTestApi::class)
@Test
fun barChart_withInvalidBarColors_displaysError() =
runComposeUiTest {
val expectedError =
RULE_COLORS_SIZE_MISMATCH.format(
2,
dataSet.data.item.points.size,
)

setContent {
val style =
BarChartDefaults.style(
barColors = listOf(Color.Red, Color.Green),
)
BarChart(
dataSet = dataSet,
style = style,
)
}

onNodeWithTag(TestTags.CHART_ERROR).isDisplayed()
onNodeWithText("${expectedError}\n").isDisplayed()
}
}
Loading
Loading