diff --git a/README.md b/README.md index 434fdf1b..4b6112b4 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ commonMain.dependencies { implementation("io.github.dautovicharis:charts-line:") implementation("io.github.dautovicharis:charts-pie:") implementation("io.github.dautovicharis:charts-bar:") + implementation("io.github.dautovicharis:charts-histogram:") implementation("io.github.dautovicharis:charts-stacked-bar:") implementation("io.github.dautovicharis:charts-stacked-area:") implementation("io.github.dautovicharis:charts-radar:") @@ -89,6 +90,7 @@ dependencies { implementation("io.github.dautovicharis:charts-line") implementation("io.github.dautovicharis:charts-pie") implementation("io.github.dautovicharis:charts-bar") + implementation("io.github.dautovicharis:charts-histogram") implementation("io.github.dautovicharis:charts-stacked-bar") implementation("io.github.dautovicharis:charts-stacked-area") implementation("io.github.dautovicharis:charts-radar") diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 072618d6..735c46f7 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -45,6 +45,16 @@ val hasReleaseSigningConfig = ).all { !it.isNullOrBlank() } val gifDocsVersion = providers.gradleProperty("gifDocsVersion").orElse("snapshot") +val gifContentRoot = + providers.gradleProperty("gifContentRoot").orElse( + providers.provider { + val migratedDocsContent = + rootProject.layout.projectDirectory + .dir("../charts-docs/content") + .asFile + if (migratedDocsContent.exists()) "../charts-docs/content" else "docs/content" + }, + ) val protobufSecurityVersion = libs.versions.protobuf.security .get() @@ -172,8 +182,8 @@ android { gifRecorder { applicationId.set(Config.DEMO_NAMESPACE) outputDir.set( - gifDocsVersion.map { docsVersion -> - rootProject.layout.projectDirectory.dir("docs/content/$docsVersion/wiki/assets") + gifContentRoot.zip(gifDocsVersion) { contentRoot, docsVersion -> + rootProject.layout.projectDirectory.dir("$contentRoot/$docsVersion/wiki/assets") }, ) } diff --git a/androidApp/src/main/kotlin/io/github/dautovicharis/charts/app/gif/DocsGifScenarios.kt b/androidApp/src/main/kotlin/io/github/dautovicharis/charts/app/gif/DocsGifScenarios.kt index 32c265e0..943a1f6f 100644 --- a/androidApp/src/main/kotlin/io/github/dautovicharis/charts/app/gif/DocsGifScenarios.kt +++ b/androidApp/src/main/kotlin/io/github/dautovicharis/charts/app/gif/DocsGifScenarios.kt @@ -9,12 +9,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import io.github.dautovicharis.charts.BarChart +import io.github.dautovicharis.charts.HistogramChart import io.github.dautovicharis.charts.LineChart import io.github.dautovicharis.charts.PieChart import io.github.dautovicharis.charts.RadarChart import io.github.dautovicharis.charts.StackedAreaChart import io.github.dautovicharis.charts.StackedBarChart import io.github.dautovicharis.charts.demoshared.data.barSampleUseCase +import io.github.dautovicharis.charts.demoshared.data.histogramSampleUseCase import io.github.dautovicharis.charts.demoshared.data.lineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.multiLineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.pieSampleUseCase @@ -107,6 +109,22 @@ fun BarDefaultGifScenario() { } } +@RecordGif( + name = "histogram_default", + interactionNodeTag = "HistogramChart", + interactions = [ + GifInteraction(type = GifInteractionType.TAP, target = GifInteractionTarget.LEFT, framesAfter = 14), + GifInteraction(type = GifInteractionType.TAP, target = GifInteractionTarget.TOP, framesAfter = 14), + GifInteraction(type = GifInteractionType.TAP, target = GifInteractionTarget.RIGHT, framesAfter = 14), + ], +) +@Composable +fun HistogramDefaultGifScenario() { + DocsGifScene { + HistogramChart(histogramSampleUseCase().initialHistogramDataSet()) + } +} + @RecordGif( name = "stacked_bar_default", interactionNodeTag = "StackedBarChartPlot", diff --git a/androidApp/src/screenshotTest/kotlin/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTest.kt b/androidApp/src/screenshotTest/kotlin/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTest.kt new file mode 100644 index 00000000..fe259d09 --- /dev/null +++ b/androidApp/src/screenshotTest/kotlin/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTest.kt @@ -0,0 +1,70 @@ +package io.github.dautovicharis.charts.app.screenshot + +import androidx.compose.runtime.Composable +import com.android.tools.screenshot.PreviewTest +import io.github.dautovicharis.charts.HistogramChart +import io.github.dautovicharis.charts.app.screenshot.shared.SCREENSHOT_ANIMATE_ON_START +import io.github.dautovicharis.charts.app.screenshot.shared.SCREENSHOT_HISTOGRAM_SAMPLE_USE_CASE +import io.github.dautovicharis.charts.app.screenshot.shared.ScreenshotPreview +import io.github.dautovicharis.charts.app.screenshot.shared.ScreenshotSurface +import io.github.dautovicharis.charts.demoshared.fixtures.ChartTestStyleFixtures +import io.github.dautovicharis.charts.style.ChartViewDefaults + +@PreviewTest +@ScreenshotPreview +@Composable +fun HistogramChartDefaultPreview() { + ScreenshotSurface { + HistogramChart( + dataSet = SCREENSHOT_HISTOGRAM_SAMPLE_USE_CASE.initialHistogramDataSet(), + animateOnStart = SCREENSHOT_ANIMATE_ON_START, + ) + } +} + +@PreviewTest +@ScreenshotPreview +@Composable +fun HistogramChartCustomPreview() { + ScreenshotSurface { + val dataSet = SCREENSHOT_HISTOGRAM_SAMPLE_USE_CASE.initialHistogramDataSet() + HistogramChart( + dataSet = dataSet, + style = ChartTestStyleFixtures.histogramCustomStyle(chartViewStyle = ChartViewDefaults.style()), + animateOnStart = SCREENSHOT_ANIMATE_ON_START, + ) + } +} + +@PreviewTest +@ScreenshotPreview +@Composable +fun HistogramChartCustomBarColorsPreview() { + ScreenshotSurface { + val dataSet = SCREENSHOT_HISTOGRAM_SAMPLE_USE_CASE.initialHistogramDataSet() + HistogramChart( + dataSet = dataSet, + style = + ChartTestStyleFixtures.histogramCustomStyle( + chartViewStyle = ChartViewDefaults.style(), + barCount = dataSet.data.item.points.size, + useBarColors = true, + ), + animateOnStart = SCREENSHOT_ANIMATE_ON_START, + ) + } +} + +@PreviewTest +@ScreenshotPreview +@Composable +fun HistogramChartSelectedBarPreview() { + ScreenshotSurface { + HistogramChart( + dataSet = SCREENSHOT_HISTOGRAM_SAMPLE_USE_CASE.initialHistogramDataSet(), + animateOnStart = SCREENSHOT_ANIMATE_ON_START, + interactionEnabled = false, + selectedBarIndex = 1, + ) + } +} diff --git a/androidApp/src/screenshotTest/kotlin/io/github/dautovicharis/charts/app/screenshot/shared/ScreenshotFixtures.kt b/androidApp/src/screenshotTest/kotlin/io/github/dautovicharis/charts/app/screenshot/shared/ScreenshotFixtures.kt index 841b2157..2a0a44ae 100644 --- a/androidApp/src/screenshotTest/kotlin/io/github/dautovicharis/charts/app/screenshot/shared/ScreenshotFixtures.kt +++ b/androidApp/src/screenshotTest/kotlin/io/github/dautovicharis/charts/app/screenshot/shared/ScreenshotFixtures.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import io.github.dautovicharis.charts.demoshared.data.barSampleUseCase +import io.github.dautovicharis.charts.demoshared.data.histogramSampleUseCase import io.github.dautovicharis.charts.demoshared.data.lineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.multiLineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.pieSampleUseCase @@ -24,6 +25,7 @@ internal val SCREENSHOT_PIE_SAMPLE_USE_CASE = pieSampleUseCase() internal val SCREENSHOT_LINE_SAMPLE_USE_CASE = lineSampleUseCase() internal val SCREENSHOT_MULTI_LINE_SAMPLE_USE_CASE = multiLineSampleUseCase() internal val SCREENSHOT_BAR_SAMPLE_USE_CASE = barSampleUseCase() +internal val SCREENSHOT_HISTOGRAM_SAMPLE_USE_CASE = histogramSampleUseCase() internal val SCREENSHOT_STACKED_BAR_SAMPLE_USE_CASE = stackedBarSampleUseCase() internal val SCREENSHOT_STACKED_AREA_SAMPLE_USE_CASE = stackedAreaSampleUseCase() internal val SCREENSHOT_RADAR_SAMPLE_USE_CASE = radarSampleUseCase() diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Dark_9cfa4316_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Dark_9cfa4316_0.png new file mode 100644 index 00000000..5337581b Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Dark_9cfa4316_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light Tablet Landscape_5e637cf0_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light Tablet Landscape_5e637cf0_0.png new file mode 100644 index 00000000..caebe9c5 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light Tablet Landscape_5e637cf0_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light Tablet_9301444c_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light Tablet_9301444c_0.png new file mode 100644 index 00000000..e065f77a Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light Tablet_9301444c_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light_bb9fafb0_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light_bb9fafb0_0.png new file mode 100644 index 00000000..5dda1b37 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomBarColorsPreview_Light_bb9fafb0_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Dark_9cfa4316_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Dark_9cfa4316_0.png new file mode 100644 index 00000000..8b96dbbd Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Dark_9cfa4316_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light Tablet Landscape_5e637cf0_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light Tablet Landscape_5e637cf0_0.png new file mode 100644 index 00000000..699043b6 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light Tablet Landscape_5e637cf0_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light Tablet_9301444c_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light Tablet_9301444c_0.png new file mode 100644 index 00000000..33afcc07 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light Tablet_9301444c_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light_bb9fafb0_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light_bb9fafb0_0.png new file mode 100644 index 00000000..089d5526 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartCustomPreview_Light_bb9fafb0_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Dark_9cfa4316_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Dark_9cfa4316_0.png new file mode 100644 index 00000000..ea066cf7 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Dark_9cfa4316_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light Tablet Landscape_5e637cf0_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light Tablet Landscape_5e637cf0_0.png new file mode 100644 index 00000000..2086e383 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light Tablet Landscape_5e637cf0_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light Tablet_9301444c_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light Tablet_9301444c_0.png new file mode 100644 index 00000000..bbcef8df Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light Tablet_9301444c_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light_bb9fafb0_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light_bb9fafb0_0.png new file mode 100644 index 00000000..64dd5f5a Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartDefaultPreview_Light_bb9fafb0_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Dark_9cfa4316_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Dark_9cfa4316_0.png new file mode 100644 index 00000000..87ada029 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Dark_9cfa4316_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light Tablet Landscape_5e637cf0_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light Tablet Landscape_5e637cf0_0.png new file mode 100644 index 00000000..b2194186 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light Tablet Landscape_5e637cf0_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light Tablet_9301444c_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light Tablet_9301444c_0.png new file mode 100644 index 00000000..1b3d26a2 Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light Tablet_9301444c_0.png differ diff --git a/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light_bb9fafb0_0.png b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light_bb9fafb0_0.png new file mode 100644 index 00000000..bb1d40ad Binary files /dev/null and b/androidApp/src/screenshotTestDebug/reference/io/github/dautovicharis/charts/app/screenshot/HistogramChartScreenshotTestKt/HistogramChartSelectedBarPreview_Light_bb9fafb0_0.png differ diff --git a/app/src/commonMain/composeResources/values/strings.xml b/app/src/commonMain/composeResources/values/strings.xml index 1159503d..d49cd73d 100644 --- a/app/src/commonMain/composeResources/values/strings.xml +++ b/app/src/commonMain/composeResources/values/strings.xml @@ -18,6 +18,8 @@ Select theme %1$d Data points: %1$d Value range: %1$d - %2$d + Data points: %1$d + Value range: %1$d - %2$d Data points: %1$d Value range: %1$d - %2$d Data points: %1$d @@ -50,6 +52,7 @@ Pie Line Bar + Histogram Stacked Bar Stacked Area Multi Line diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/ChartGalleryPreviews.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/ChartGalleryPreviews.kt index 523cd70d..749e8d2c 100644 --- a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/ChartGalleryPreviews.kt +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/ChartGalleryPreviews.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.github.dautovicharis.charts.BarChart +import io.github.dautovicharis.charts.HistogramChart import io.github.dautovicharis.charts.LineChart import io.github.dautovicharis.charts.PieChart import io.github.dautovicharis.charts.RadarChart @@ -27,6 +28,7 @@ import io.github.dautovicharis.charts.model.toMultiChartDataSet import io.github.dautovicharis.charts.style.BarChartDefaults import io.github.dautovicharis.charts.style.ChartViewDefaults import io.github.dautovicharis.charts.style.ChartViewStyle +import io.github.dautovicharis.charts.style.HistogramChartDefaults import io.github.dautovicharis.charts.style.LineChartDefaults import io.github.dautovicharis.charts.style.PieChartDefaults import io.github.dautovicharis.charts.style.RadarChartDefaults @@ -83,6 +85,7 @@ internal fun ChartPreview( is ChartDestination.StackedAreaChartScreen -> StackedAreaChartPreview(previews.stackedAreaSeries) is ChartDestination.BarChartScreen -> BarChartPreview(previews.barValues) + is ChartDestination.HistogramChartScreen -> HistogramChartPreview(previews.histogramValues) is ChartDestination.StackedBarChartScreen -> StackedBarChartPreview(previews.stackedSeries) is ChartDestination.RadarChartScreen -> RadarChartPreview(previews.radarSeries) } @@ -180,6 +183,33 @@ private fun BarChartPreview(values: List) { ) } +@Composable +private fun HistogramChartPreview(values: List) { + val labels = + remember(values) { + List(values.size) { index -> "B${index + 1}" } + } + val dataSet = + remember(values, labels) { + values.toChartDataSet( + title = "", + labels = labels, + ) + } + HistogramChart( + dataSet = dataSet, + style = + HistogramChartDefaults.style( + minValue = 0f, + xAxisLabelsVisible = false, + yAxisLabelsVisible = false, + chartViewStyle = previewChartViewStyle(), + ), + interactionEnabled = false, + animateOnStart = false, + ) +} + @Composable private fun StackedBarChartPreview(series: List>>) { val dataSet = diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/ChartGalleryViewModel.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/ChartGalleryViewModel.kt index 2318509f..ed3ebec7 100644 --- a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/ChartGalleryViewModel.kt +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/ChartGalleryViewModel.kt @@ -22,6 +22,7 @@ data class ChartGalleryPreviewState( val multiLineSeries: List>>, val stackedAreaSeries: List>>, val barValues: List, + val histogramValues: List, val stackedSeries: List>>, val radarSeries: List>>, ) @@ -82,6 +83,17 @@ class ChartGalleryViewModel( }, ) } + launch { + previewLoop( + baseIntervalMs = LIVE_PREVIEW_INTERVAL_MS + 225L, + jitterMs = 400L, + update = { previews -> + previews.copy( + histogramValues = previewUseCase.nextHistogramPreview(previews.histogramValues), + ) + }, + ) + } launch { previewLoop( baseIntervalMs = LIVE_PREVIEW_INTERVAL_MS + 300L, @@ -135,6 +147,7 @@ class ChartGalleryViewModel( is ChartDestination.MultiLineChartScreen -> "Compare multiple series." is ChartDestination.StackedAreaChartScreen -> "Cumulative layers by category." is ChartDestination.BarChartScreen -> "Rank values quickly." + is ChartDestination.HistogramChartScreen -> "Distribution across bins." is ChartDestination.StackedBarChartScreen -> "Segment composition by category." is ChartDestination.RadarChartScreen -> "Live radial signals." } diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/MainViewModel.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/MainViewModel.kt index 722e16f5..977d9b1f 100644 --- a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/MainViewModel.kt +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/MainViewModel.kt @@ -40,6 +40,7 @@ class MainViewModel : ViewModel() { ChartDestination.MultiLineChartScreen, ChartDestination.StackedAreaChartScreen, ChartDestination.BarChartScreen, + ChartDestination.HistogramChartScreen, ChartDestination.StackedBarChartScreen, ChartDestination.RadarChartScreen, ), diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/Navigation.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/Navigation.kt index eb9603f1..a1f4418d 100644 --- a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/Navigation.kt +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/Navigation.kt @@ -8,18 +8,21 @@ import androidx.navigation.compose.composable import chartsproject.app.generated.resources.Res import chartsproject.app.generated.resources.bar_chart import chartsproject.app.generated.resources.bar_stacked_chart +import chartsproject.app.generated.resources.histogram_chart import chartsproject.app.generated.resources.line_chart import chartsproject.app.generated.resources.multi_line_chart import chartsproject.app.generated.resources.pie_chart import chartsproject.app.generated.resources.radar_chart import chartsproject.app.generated.resources.stacked_area_chart import chartsproject.charts_demo_shared.generated.resources.ic_bar_chart +import chartsproject.charts_demo_shared.generated.resources.ic_histogram_chart import chartsproject.charts_demo_shared.generated.resources.ic_line_chart import chartsproject.charts_demo_shared.generated.resources.ic_multi_line_chart import chartsproject.charts_demo_shared.generated.resources.ic_pie_chart import chartsproject.charts_demo_shared.generated.resources.ic_radar_chart import chartsproject.charts_demo_shared.generated.resources.ic_stacked_bar_chart import io.github.dautovicharis.charts.app.demo.bar.BarChartDemo +import io.github.dautovicharis.charts.app.demo.histogram.HistogramChartDemo import io.github.dautovicharis.charts.app.demo.line.LineChartDemo import io.github.dautovicharis.charts.app.demo.multiline.MultiLineChartDemo import io.github.dautovicharis.charts.app.demo.pie.PieChartDemo @@ -82,6 +85,13 @@ sealed class ChartDestination( title = Res.string.bar_stacked_chart, ) + data object HistogramChartScreen : + ChartDestination( + route = "histogramChart", + icon = SharedRes.drawable.ic_histogram_chart, + title = Res.string.histogram_chart, + ) + data object RadarChartScreen : ChartDestination( route = "radarChart", @@ -133,6 +143,10 @@ fun Navigation( StackedBarChartDemo(onStyleItemsChanged = onStyleItemsChanged) } + composable(ChartDestination.HistogramChartScreen.route) { + HistogramChartDemo(onStyleItemsChanged = onStyleItemsChanged) + } + composable(ChartDestination.RadarChartScreen.route) { RadarChartDemo(onStyleItemsChanged = onStyleItemsChanged) } diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/data/ChartPreviewUseCase.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/data/ChartPreviewUseCase.kt index 289e10dd..16da4b26 100644 --- a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/data/ChartPreviewUseCase.kt +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/data/ChartPreviewUseCase.kt @@ -11,6 +11,8 @@ interface ChartPreviewUseCase { fun nextBarPreview(values: List): List + fun nextHistogramPreview(values: List): List + fun nextMultiLinePreview(): List>> fun nextStackedAreaPreview(): List>> diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/data/impl/DefaultChartPreviewUseCase.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/data/impl/DefaultChartPreviewUseCase.kt index c4474b42..472e982b 100644 --- a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/data/impl/DefaultChartPreviewUseCase.kt +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/data/impl/DefaultChartPreviewUseCase.kt @@ -20,6 +20,7 @@ class DefaultChartPreviewUseCase : ChartPreviewUseCase { "Premium Plan" to listOf(6f, 7f, 8f, 9f, 10f), ) private val previewBarValues = listOf(18f, 32f, 26f, 48f, 36f, 28f, 54f) + private val previewHistogramValues = listOf(4f, 7f, 12f, 16f, 14f, 9f, 5f) private val previewStackedSeries = listOf( "North America" to listOf(20f, 22f, 25f), @@ -38,6 +39,7 @@ class DefaultChartPreviewUseCase : ChartPreviewUseCase { multiLineSeries = previewMultiLineSeries, stackedAreaSeries = previewStackedAreaSeries, barValues = previewBarValues, + histogramValues = previewHistogramValues, stackedSeries = previewStackedSeries, radarSeries = previewRadarSeries, ) @@ -57,6 +59,11 @@ class DefaultChartPreviewUseCase : ChartPreviewUseCase { jitter(value, from = -8, until = 9, min = 0f, max = 100f) } + override fun nextHistogramPreview(values: List): List = + values.map { value -> + jitter(value, from = -4, until = 5, min = 0f, max = 60f) + } + override fun nextMultiLinePreview(): List>> = previewMultiLineSeries.map { (label, values) -> label to diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramChartStyleItems.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramChartStyleItems.kt new file mode 100644 index 00000000..78421264 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramChartStyleItems.kt @@ -0,0 +1,59 @@ +package io.github.dautovicharis.charts.app.demo.histogram + +import androidx.compose.runtime.Composable +import io.github.dautovicharis.charts.app.ui.composable.ChartAspectRatioPreset +import io.github.dautovicharis.charts.app.ui.composable.ChartStyleItems +import io.github.dautovicharis.charts.app.ui.composable.StyleItems +import io.github.dautovicharis.charts.app.ui.composable.toChartModifier +import io.github.dautovicharis.charts.demoshared.fixtures.ChartTestStyleFixtures +import io.github.dautovicharis.charts.style.ChartViewDefaults +import io.github.dautovicharis.charts.style.HistogramChartDefaults + +object HistogramChartStyleItems { + @Composable + fun default(aspectRatioPreset: ChartAspectRatioPreset = ChartAspectRatioPreset.Square): StyleItems = + ChartStyleItems( + currentStyle = defaultStyle(aspectRatioPreset), + defaultStyle = defaultStyle(), + ) + + @Composable + fun defaultStyle(aspectRatioPreset: ChartAspectRatioPreset = ChartAspectRatioPreset.Square) = + HistogramChartDefaults.style(chartViewStyle = chartViewStyle(aspectRatioPreset)) + + @Composable + fun customStyle( + barCount: Int, + minValue: Float, + maxValue: Float, + aspectRatioPreset: ChartAspectRatioPreset = ChartAspectRatioPreset.Square, + ) = ChartTestStyleFixtures.histogramCustomStyle( + 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, + ), + defaultStyle = defaultStyle(), + ) + + @Composable + private fun chartViewStyle(aspectRatioPreset: ChartAspectRatioPreset) = + ChartViewDefaults.style(modifierChart = aspectRatioPreset.toChartModifier()) +} diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramChartViewModel.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramChartViewModel.kt new file mode 100644 index 00000000..2faaed2e --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramChartViewModel.kt @@ -0,0 +1,158 @@ +package io.github.dautovicharis.charts.app.demo.histogram + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.dautovicharis.charts.demoshared.data.HistogramSampleUseCase +import io.github.dautovicharis.charts.model.ChartDataSet +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +data class HistogramChartControlsState( + val points: Int, + val minValue: Int, + val maxValue: Int, +) + +class HistogramChartViewModel( + private val histogramSampleUseCase: HistogramSampleUseCase, +) : ViewModel() { + companion object { + const val MIN_SUPPORTED_POINTS = 10 + const val MAX_SUPPORTED_POINTS = 500 + const val MIN_SUPPORTED_VALUE = 0 + const val MAX_SUPPORTED_VALUE = 500 + private const val LIVE_UPDATE_INTERVAL_MS = 2000L + } + + private val defaultPoints = + histogramSampleUseCase + .histogramDefaultPoints() + .coerceIn(MIN_SUPPORTED_POINTS, MAX_SUPPORTED_POINTS) + private val defaultRange = + histogramSampleUseCase + .histogramDefaultRange() + .let { range -> + val safeStart = range.first.coerceIn(MIN_SUPPORTED_VALUE, MAX_SUPPORTED_VALUE) + val safeEnd = range.last.coerceIn(safeStart, MAX_SUPPORTED_VALUE) + safeStart..safeEnd + } + + private val _dataSet = + MutableStateFlow( + histogramSampleUseCase.histogramDataSet( + points = defaultPoints, + range = defaultRange, + ), + ) + + val dataSet: StateFlow = _dataSet.asStateFlow() + private val _isPlaying = MutableStateFlow(false) + val isPlaying: StateFlow = _isPlaying.asStateFlow() + private var liveUpdatesJob: Job? = null + private val _controlsState = + MutableStateFlow( + HistogramChartControlsState( + points = defaultPoints, + minValue = defaultRange.first, + maxValue = defaultRange.last, + ), + ) + val controlsState: StateFlow = _controlsState.asStateFlow() + + fun refresh() { + regenerateDataSet() + } + + fun regenerateDataSet( + points: Int = _controlsState.value.points, + range: IntRange = _controlsState.value.minValue.._controlsState.value.maxValue, + ) { + val safePoints = + points.coerceIn( + minimumValue = MIN_SUPPORTED_POINTS, + maximumValue = MAX_SUPPORTED_POINTS, + ) + val safeRangeStart = range.first.coerceIn(MIN_SUPPORTED_VALUE, MAX_SUPPORTED_VALUE) + val safeRangeEnd = range.last.coerceIn(safeRangeStart, MAX_SUPPORTED_VALUE) + _dataSet.value = + histogramSampleUseCase.histogramDataSet( + points = safePoints, + range = safeRangeStart..safeRangeEnd, + ) + } + + fun updateDataPoints(points: Int) { + val controls = _controlsState.value + val safePoints = points.coerceIn(MIN_SUPPORTED_POINTS, MAX_SUPPORTED_POINTS) + if (safePoints == controls.points) return + + _controlsState.value = controls.copy(points = safePoints) + regenerateDataSet( + points = safePoints, + range = controls.minValue..controls.maxValue, + ) + } + + fun updateDataRange( + minValue: Int, + maxValue: Int, + ) { + val safeMin = minValue.coerceIn(MIN_SUPPORTED_VALUE, MAX_SUPPORTED_VALUE) + val safeMax = maxValue.coerceIn(safeMin, MAX_SUPPORTED_VALUE) + val controls = _controlsState.value + + if ( + controls.minValue == safeMin && + controls.maxValue == safeMax + ) { + return + } + + _controlsState.value = + controls.copy( + minValue = safeMin, + maxValue = safeMax, + ) + regenerateDataSet( + points = controls.points, + range = safeMin..safeMax, + ) + } + + fun togglePlaying() { + val shouldPlay = !_isPlaying.value + _isPlaying.value = shouldPlay + if (shouldPlay) { + startLiveUpdates() + } else { + stopLiveUpdates() + } + } + + override fun onCleared() { + stopLiveUpdates() + super.onCleared() + } + + private fun startLiveUpdates() { + liveUpdatesJob?.cancel() + liveUpdatesJob = + viewModelScope.launch { + refresh() + while (isActive) { + delay(LIVE_UPDATE_INTERVAL_MS) + refresh() + } + } + } + + private fun stopLiveUpdates() { + liveUpdatesJob?.cancel() + liveUpdatesJob = null + } +} diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramDemo.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramDemo.kt new file mode 100644 index 00000000..348547fb --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/demo/histogram/HistogramDemo.kt @@ -0,0 +1,189 @@ +package io.github.dautovicharis.charts.app.demo.histogram + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RangeSlider +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import chartsproject.app.generated.resources.Res +import chartsproject.app.generated.resources.cd_pause_live_updates +import chartsproject.app.generated.resources.cd_play_live_updates +import chartsproject.app.generated.resources.histogram_data_points +import chartsproject.app.generated.resources.histogram_data_points_range +import io.github.dautovicharis.charts.HistogramChart +import io.github.dautovicharis.charts.app.ui.composable.ChartAspectRatioPreset +import io.github.dautovicharis.charts.app.ui.composable.ChartAspectRatioToggle +import io.github.dautovicharis.charts.app.ui.composable.ChartDemo +import io.github.dautovicharis.charts.app.ui.composable.ChartPreset +import io.github.dautovicharis.charts.app.ui.composable.ChartPresetToggle +import io.github.dautovicharis.charts.app.ui.composable.StyleItems +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import kotlin.math.roundToInt + +@Composable +fun HistogramChartDemo( + viewModel: HistogramChartViewModel = koinViewModel(), + onStyleItemsChanged: (StyleItems?) -> Unit = {}, +) { + val dataSet by viewModel.dataSet.collectAsStateWithLifecycle() + val isPlaying by viewModel.isPlaying.collectAsStateWithLifecycle() + val controlsState by viewModel.controlsState.collectAsStateWithLifecycle() + var preset by remember { mutableStateOf(ChartPreset.Default) } + var aspectRatioPreset by remember { mutableStateOf(ChartAspectRatioPreset.Square) } + val refresh: () -> Unit = viewModel::refresh + + val styleItems = + when (preset) { + ChartPreset.Default -> HistogramChartStyleItems.default(aspectRatioPreset) + ChartPreset.Custom -> + HistogramChartStyleItems.custom( + barCount = dataSet.data.item.points.size, + minValue = controlsState.minValue.toFloat(), + maxValue = controlsState.maxValue.toFloat(), + aspectRatioPreset = aspectRatioPreset, + ) + } + + ChartDemo( + styleItems = styleItems, + onRefresh = refresh, + onStyleItemsChanged = onStyleItemsChanged, + presetContent = { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ChartPresetToggle( + selectedPreset = preset, + onPresetSelected = { preset = it }, + ) + ChartAspectRatioToggle( + selectedPreset = aspectRatioPreset, + onPresetSelected = { aspectRatioPreset = it }, + ) + } + }, + extraButtons = { + IconButton( + onClick = viewModel::togglePlaying, + ) { + Icon( + imageVector = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = + stringResource( + if (isPlaying) Res.string.cd_pause_live_updates else Res.string.cd_play_live_updates, + ), + ) + } + }, + controlsContent = { + HistogramDataPointsControls( + points = controlsState.points, + minValue = controlsState.minValue, + maxValue = controlsState.maxValue, + onPointsChange = viewModel::updateDataPoints, + onRangeChange = viewModel::updateDataRange, + ) + }, + ) { + key(controlsState.points, controlsState.minValue, controlsState.maxValue, preset, aspectRatioPreset) { + when (preset) { + ChartPreset.Default -> { + HistogramChart( + dataSet, + style = HistogramChartStyleItems.defaultStyle(aspectRatioPreset), + ) + } + + ChartPreset.Custom -> { + HistogramChart( + dataSet = dataSet, + style = + HistogramChartStyleItems.customStyle( + barCount = dataSet.data.item.points.size, + minValue = controlsState.minValue.toFloat(), + maxValue = controlsState.maxValue.toFloat(), + aspectRatioPreset = aspectRatioPreset, + ), + ) + } + } + } + } +} + +@Composable +private fun HistogramDataPointsControls( + points: Int, + minValue: Int, + maxValue: Int, + onPointsChange: (Int) -> Unit, + onRangeChange: (Int, Int) -> Unit, +) { + val minPointsSupported = HistogramChartViewModel.MIN_SUPPORTED_POINTS.toFloat() + val maxPointsSupported = HistogramChartViewModel.MAX_SUPPORTED_POINTS.toFloat() + val minValueSupported = HistogramChartViewModel.MIN_SUPPORTED_VALUE.toFloat() + val maxValueSupported = HistogramChartViewModel.MAX_SUPPORTED_VALUE.toFloat() + var draftPoints by remember(points) { mutableFloatStateOf(points.toFloat()) } + var draftRange by remember(minValue, maxValue) { mutableStateOf(minValue.toFloat()..maxValue.toFloat()) } + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = stringResource(Res.string.histogram_data_points, draftPoints.roundToInt()), + color = MaterialTheme.colorScheme.onSurface, + ) + Slider( + value = draftPoints, + valueRange = minPointsSupported..maxPointsSupported, + onValueChange = { draftPoints = it }, + onValueChangeFinished = { onPointsChange(draftPoints.roundToInt()) }, + ) + Text( + text = + stringResource( + Res.string.histogram_data_points_range, + draftRange.start.roundToInt(), + draftRange.endInclusive.roundToInt(), + ), + color = MaterialTheme.colorScheme.onSurface, + ) + RangeSlider( + value = draftRange, + valueRange = minValueSupported..maxValueSupported, + onValueChange = { draftRange = it }, + onValueChangeFinished = { + onRangeChange( + draftRange.start.roundToInt(), + draftRange.endInclusive.roundToInt(), + ) + }, + ) + } +} diff --git a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/di/AppModule.kt b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/di/AppModule.kt index d2a09b89..3422b9a0 100644 --- a/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/di/AppModule.kt +++ b/app/src/commonMain/kotlin/io/github/dautovicharis/charts/app/di/AppModule.kt @@ -7,6 +7,7 @@ import io.github.dautovicharis.charts.app.data.LiveLatencyTimelineUseCase import io.github.dautovicharis.charts.app.data.impl.DefaultChartPreviewUseCase import io.github.dautovicharis.charts.app.data.impl.DefaultLiveLatencyTimelineUseCase import io.github.dautovicharis.charts.app.demo.bar.BarChartViewModel +import io.github.dautovicharis.charts.app.demo.histogram.HistogramChartViewModel import io.github.dautovicharis.charts.app.demo.line.LineChartViewModel import io.github.dautovicharis.charts.app.demo.multiline.MultiLineChartViewModel import io.github.dautovicharis.charts.app.demo.pie.PieChartViewModel @@ -14,6 +15,7 @@ import io.github.dautovicharis.charts.app.demo.radar.RadarChartViewModel import io.github.dautovicharis.charts.app.demo.stackedarea.StackedAreaChartViewModel import io.github.dautovicharis.charts.app.demo.stackedbar.StackedBarChartViewModel import io.github.dautovicharis.charts.demoshared.data.BarSampleUseCase +import io.github.dautovicharis.charts.demoshared.data.HistogramSampleUseCase import io.github.dautovicharis.charts.demoshared.data.LineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.MultiLineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.PieSampleUseCase @@ -21,6 +23,7 @@ import io.github.dautovicharis.charts.demoshared.data.RadarSampleUseCase import io.github.dautovicharis.charts.demoshared.data.StackedAreaSampleUseCase import io.github.dautovicharis.charts.demoshared.data.StackedBarSampleUseCase import io.github.dautovicharis.charts.demoshared.data.barSampleUseCase +import io.github.dautovicharis.charts.demoshared.data.histogramSampleUseCase import io.github.dautovicharis.charts.demoshared.data.lineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.multiLineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.pieSampleUseCase @@ -38,6 +41,7 @@ val appModule = single { lineSampleUseCase() } single { multiLineSampleUseCase() } single { barSampleUseCase() } + single { histogramSampleUseCase() } single { stackedBarSampleUseCase() } single { stackedAreaSampleUseCase() } single { radarSampleUseCase() } @@ -47,6 +51,7 @@ val appModule = viewModel { LineChartViewModel(get()) } viewModel { MultiLineChartViewModel(get()) } viewModel { BarChartViewModel(get()) } + viewModel { HistogramChartViewModel(get()) } viewModel { StackedBarChartViewModel(get()) } viewModel { StackedAreaChartViewModel(get()) } viewModel { RadarChartViewModel(get()) } diff --git a/app/src/jsMain/kotlin/JsMainScreen.kt b/app/src/jsMain/kotlin/JsMainScreen.kt index 6c0dbd79..fd42ea6a 100644 --- a/app/src/jsMain/kotlin/JsMainScreen.kt +++ b/app/src/jsMain/kotlin/JsMainScreen.kt @@ -62,6 +62,7 @@ private fun rememberJsDemoStartupResourcesReady(): Boolean { ChartDestination.MultiLineChartScreen, ChartDestination.StackedAreaChartScreen, ChartDestination.BarChartScreen, + ChartDestination.HistogramChartScreen, ChartDestination.StackedBarChartScreen, ChartDestination.RadarChartScreen, ) diff --git a/build.gradle.kts b/build.gradle.kts index b5f31319..6a72b6d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -186,14 +186,14 @@ tasks.register("listDocsGifScenarios") { tasks.register("recordDocsGif") { group = "Charts" description = - "Records one docs GIF scenario to docs/content//wiki/assets (set -PgifScenario=, defaults to first)" + "Records one docs GIF scenario to //wiki/assets (set -PgifScenario=, defaults to first)" dependsOn(":androidApp:recordGifDebug") } tasks.register("recordDocsGifs") { group = "Charts" description = - "Records all docs GIF scenarios to docs/content//wiki/assets (default version: snapshot)" + "Records all docs GIF scenarios to //wiki/assets (default version: snapshot)" dependsOn(":androidApp:recordGifsDebug") } diff --git a/buildSrc/src/main/kotlin/ChartsModules.kt b/buildSrc/src/main/kotlin/ChartsModules.kt index bdc0164d..594fa363 100644 --- a/buildSrc/src/main/kotlin/ChartsModules.kt +++ b/buildSrc/src/main/kotlin/ChartsModules.kt @@ -7,6 +7,7 @@ object ChartsModules { ":charts-line", ":charts-pie", ":charts-bar", + ":charts-histogram", ":charts-stacked-bar", ":charts-stacked-area", ":charts-radar", diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index fd5c2aee..687560ff 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -10,6 +10,7 @@ object Config { const val ARTIFACT_LINE_ID = "charts-line" const val ARTIFACT_PIE_ID = "charts-pie" const val ARTIFACT_BAR_ID = "charts-bar" + const val ARTIFACT_HISTOGRAM_ID = "charts-histogram" const val ARTIFACT_STACKED_BAR_ID = "charts-stacked-bar" const val ARTIFACT_STACKED_AREA_ID = "charts-stacked-area" const val ARTIFACT_RADAR_ID = "charts-radar" @@ -37,6 +38,7 @@ object Config { const val CHARTS_LINE_NAMESPACE = "$GROUP_ID.charts.line" const val CHARTS_PIE_NAMESPACE = "$GROUP_ID.charts.pie" const val CHARTS_BAR_NAMESPACE = "$GROUP_ID.charts.bar" + const val CHARTS_HISTOGRAM_NAMESPACE = "$GROUP_ID.charts.histogram" const val CHARTS_STACKED_BAR_NAMESPACE = "$GROUP_ID.charts.stackedbar" const val CHARTS_STACKED_AREA_NAMESPACE = "$GROUP_ID.charts.stackedarea" const val CHARTS_RADAR_NAMESPACE = "$GROUP_ID.charts.radar" diff --git a/charts-bom/build.gradle.kts b/charts-bom/build.gradle.kts index b06c2bd2..2dad06fc 100644 --- a/charts-bom/build.gradle.kts +++ b/charts-bom/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { api("${Config.GROUP_ID}:${Config.ARTIFACT_LINE_ID}:${project.version}") api("${Config.GROUP_ID}:${Config.ARTIFACT_PIE_ID}:${project.version}") api("${Config.GROUP_ID}:${Config.ARTIFACT_BAR_ID}:${project.version}") + api("${Config.GROUP_ID}:${Config.ARTIFACT_HISTOGRAM_ID}:${project.version}") api("${Config.GROUP_ID}:${Config.ARTIFACT_STACKED_BAR_ID}:${project.version}") api("${Config.GROUP_ID}:${Config.ARTIFACT_STACKED_AREA_ID}:${project.version}") api("${Config.GROUP_ID}:${Config.ARTIFACT_RADAR_ID}:${project.version}") diff --git a/charts-core/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/Constants.kt b/charts-core/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/Constants.kt index 41898a41..6a08fdb8 100644 --- a/charts-core/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/Constants.kt +++ b/charts-core/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/Constants.kt @@ -29,6 +29,7 @@ object TestTags { const val CHART_TITLE = "ChartTitle" const val PIE_CHART = "PieChart" const val BAR_CHART = "BarChart" + const val HISTOGRAM_CHART = "HistogramChart" const val BAR_CHART_ZOOM_OUT = "BarChartZoomOut" const val BAR_CHART_ZOOM_IN = "BarChartZoomIn" const val BAR_CHART_DENSE_EXPAND = "BarChartDenseExpand" diff --git a/charts-core/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/DataValidation.kt b/charts-core/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/DataValidation.kt index 5cec5b4a..010b89ce 100644 --- a/charts-core/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/DataValidation.kt +++ b/charts-core/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/DataValidation.kt @@ -14,6 +14,8 @@ object ValidationErrors { "Data points size should be greater than or equal to %d." const val RULE_DATA_POINT_NOT_NUMBER: String = "Data point at index %d is not a valid number." + const val RULE_DATA_POINT_NEGATIVE: String = + "Data point at index %d must be non-negative." const val RULE_ITEM_POINT_NEGATIVE: String = "Item at index %d has negative value at index %d." @@ -22,6 +24,7 @@ object ValidationErrors { const val MIN_REQUIRED_STACKED_AREA: Int = 2 const val MIN_REQUIRED_STACKED_BAR: Int = 1 const val MIN_REQUIRED_BAR: Int = 2 + const val MIN_REQUIRED_HISTOGRAM: Int = 2 const val MIN_REQUIRED_RADAR: Int = 3 } diff --git a/charts-demo-shared/src/commonMain/composeResources/drawable/ic_histogram_chart.xml b/charts-demo-shared/src/commonMain/composeResources/drawable/ic_histogram_chart.xml new file mode 100644 index 00000000..eab3e398 --- /dev/null +++ b/charts-demo-shared/src/commonMain/composeResources/drawable/ic_histogram_chart.xml @@ -0,0 +1,9 @@ + + + diff --git a/charts-demo-shared/src/commonMain/composeResources/drawable/ic_stacked_area_chart.xml b/charts-demo-shared/src/commonMain/composeResources/drawable/ic_stacked_area_chart.xml new file mode 100644 index 00000000..da2433c4 --- /dev/null +++ b/charts-demo-shared/src/commonMain/composeResources/drawable/ic_stacked_area_chart.xml @@ -0,0 +1,9 @@ + + + diff --git a/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/BasicChartSampleUseCases.kt b/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/BasicChartSampleUseCases.kt index e24eb684..e1d3ec75 100644 --- a/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/BasicChartSampleUseCases.kt +++ b/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/BasicChartSampleUseCases.kt @@ -42,3 +42,16 @@ interface BarSampleUseCase { range: IntRange, ): ChartDataSet } + +interface HistogramSampleUseCase { + fun initialHistogramDataSet(): ChartDataSet + + fun histogramDefaultPoints(): Int + + fun histogramDefaultRange(): IntRange + + fun histogramDataSet( + points: Int, + range: IntRange, + ): ChartDataSet +} diff --git a/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/SampleUseCases.kt b/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/SampleUseCases.kt index 55bae9b2..eda26351 100644 --- a/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/SampleUseCases.kt +++ b/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/SampleUseCases.kt @@ -1,6 +1,7 @@ package io.github.dautovicharis.charts.demoshared.data import io.github.dautovicharis.charts.demoshared.data.impl.DefaultBarSampleUseCase +import io.github.dautovicharis.charts.demoshared.data.impl.DefaultHistogramSampleUseCase import io.github.dautovicharis.charts.demoshared.data.impl.DefaultLineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.impl.DefaultMultiLineSampleUseCase import io.github.dautovicharis.charts.demoshared.data.impl.DefaultPieSampleUseCase @@ -14,6 +15,8 @@ fun lineSampleUseCase(): LineSampleUseCase = DefaultLineSampleUseCase() fun barSampleUseCase(): BarSampleUseCase = DefaultBarSampleUseCase() +fun histogramSampleUseCase(): HistogramSampleUseCase = DefaultHistogramSampleUseCase() + fun multiLineSampleUseCase(): MultiLineSampleUseCase = DefaultMultiLineSampleUseCase() fun stackedBarSampleUseCase(): StackedBarSampleUseCase = DefaultStackedBarSampleUseCase() diff --git a/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/impl/DefaultHistogramSampleUseCase.kt b/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/impl/DefaultHistogramSampleUseCase.kt new file mode 100644 index 00000000..7b3618ce --- /dev/null +++ b/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/data/impl/DefaultHistogramSampleUseCase.kt @@ -0,0 +1,38 @@ +package io.github.dautovicharis.charts.demoshared.data.impl + +import io.github.dautovicharis.charts.demoshared.data.HistogramSampleUseCase +import io.github.dautovicharis.charts.model.ChartDataSet +import io.github.dautovicharis.charts.model.toChartDataSet + +internal class DefaultHistogramSampleUseCase : HistogramSampleUseCase { + companion object { + private const val DEFAULT_TITLE = "Request Duration Distribution" + private const val DEFAULT_POINTS = 60 + private val DEFAULT_RANGE = 0..120 + } + + override fun initialHistogramDataSet(): ChartDataSet = + listOf(3f, 6f, 11f, 16f, 14f, 9f, 5f).toChartDataSet( + title = DEFAULT_TITLE, + labels = listOf("0-50ms", "50-100ms", "100-150ms", "150-200ms", "200-250ms", "250-300ms", "300ms+"), + ) + + override fun histogramDefaultPoints(): Int = DEFAULT_POINTS + + override fun histogramDefaultRange(): IntRange = DEFAULT_RANGE + + override fun histogramDataSet( + points: Int, + range: IntRange, + ): ChartDataSet { + val safePoints = points.coerceAtLeast(2) + val safeRangeStart = range.first.coerceAtLeast(0) + val safeRangeEnd = range.last.coerceAtLeast(safeRangeStart) + val values = List(safePoints) { (safeRangeStart..safeRangeEnd).random().toFloat() } + val labels = List(safePoints) { index -> "B${index + 1}" } + return values.toChartDataSet( + title = DEFAULT_TITLE, + labels = labels, + ) + } +} diff --git a/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/fixtures/ChartTestStyleFixtures.kt b/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/fixtures/ChartTestStyleFixtures.kt index ca7b8c91..6e07ff6b 100644 --- a/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/fixtures/ChartTestStyleFixtures.kt +++ b/charts-demo-shared/src/commonMain/kotlin/io/github/dautovicharis/charts/demoshared/fixtures/ChartTestStyleFixtures.kt @@ -9,6 +9,8 @@ import io.github.dautovicharis.charts.demoshared.theme.seriesColors import io.github.dautovicharis.charts.style.BarChartDefaults import io.github.dautovicharis.charts.style.BarChartStyle import io.github.dautovicharis.charts.style.ChartViewStyle +import io.github.dautovicharis.charts.style.HistogramChartDefaults +import io.github.dautovicharis.charts.style.HistogramChartStyle import io.github.dautovicharis.charts.style.LineChartDefaults import io.github.dautovicharis.charts.style.LineChartStyle import io.github.dautovicharis.charts.style.PieChartDefaults @@ -105,6 +107,36 @@ object ChartTestStyleFixtures { ) } + @Composable + fun histogramCustomStyle( + chartViewStyle: ChartViewStyle, + barCount: Int = 1, + useBarColors: Boolean = false, + minValue: Float? = 0f, + maxValue: Float? = null, + ): HistogramChartStyle { + val chartColors = LocalChartColors.current + val barColors = + if (useBarColors) { + chartColors.seriesColors(barCount.coerceAtLeast(1)) + } else { + emptyList() + } + return HistogramChartDefaults.style( + chartViewStyle = chartViewStyle, + barColor = chartColors.seriesColor(4), + barColors = barColors, + minValue = minValue, + maxValue = maxValue, + gridColor = chartColors.gridLine, + axisColor = chartColors.axisLine, + xAxisLabelColor = chartColors.axisLabel, + selectionLineVisible = true, + selectionLineColor = chartColors.selection, + selectionLineWidth = 2f, + ) + } + @Composable fun stackedBarCustomStyle( chartViewStyle: ChartViewStyle, diff --git a/charts-histogram/build.gradle.kts b/charts-histogram/build.gradle.kts new file mode 100644 index 00000000..4347ccbe --- /dev/null +++ b/charts-histogram/build.gradle.kts @@ -0,0 +1,83 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidKotlinMultiplatformLibrary) + alias(libs.plugins.jetbrainsCompose) + `maven-publish` + signing + alias(libs.plugins.mavenPublish) + alias(libs.plugins.compose.compiler) +} + +kotlin { + jvmToolchain( + libs.versions.java + .get() + .toInt(), + ) + + android { + namespace = Config.CHARTS_HISTOGRAM_NAMESPACE + compileSdk = Config.COMPILE_SDK + minSdk = Config.MIN_SDK + compilerOptions { + jvmTarget.set( + org.jetbrains.kotlin.gradle.dsl.JvmTarget + .fromTarget(libs.versions.java.get()), + ) + } + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + js(IR) { + browser() + binaries.executable() + } + + jvm() + + sourceSets { + all { + languageSettings.optIn("io.github.dautovicharis.charts.internal.InternalChartsApi") + } + + commonMain.dependencies { + api(projects.chartsCore) + api(projects.chartsBar) + } + + commonTest.dependencies { + implementation(kotlin("test")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(libs.compose.mpp.ui.test) + } + + androidMain.dependencies { + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.ui.tooling) + } + + jvmMain.dependencies { + implementation(compose.desktop.currentOs) + } + } +} + +mavenPublishing { + coordinates( + groupId = Config.GROUP_ID, + artifactId = Config.ARTIFACT_HISTOGRAM_ID, + version = project.version.toString(), + ) + + pom { + ChartsPublishing.configurePom( + pom = this, + moduleName = "Histogram Chart", + moduleDescription = "Histogram chart module for Charts.", + ) + } +} diff --git a/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/HistogramChart.kt b/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/HistogramChart.kt new file mode 100644 index 00000000..5da62127 --- /dev/null +++ b/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/HistogramChart.kt @@ -0,0 +1,51 @@ +package io.github.dautovicharis.charts + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import io.github.dautovicharis.charts.internal.NO_SELECTION +import io.github.dautovicharis.charts.internal.TestTags +import io.github.dautovicharis.charts.internal.common.composable.ChartErrors +import io.github.dautovicharis.charts.internal.validateHistogramData +import io.github.dautovicharis.charts.model.ChartDataSet +import io.github.dautovicharis.charts.style.HistogramChartDefaults +import io.github.dautovicharis.charts.style.HistogramChartStyle +import kotlinx.collections.immutable.toImmutableList + +/** + * A composable function that displays a Histogram Chart. + * + * Expects pre-binned data where each point is a bin count. + */ +@Composable +fun HistogramChart( + dataSet: ChartDataSet, + style: HistogramChartStyle = HistogramChartDefaults.style(), + interactionEnabled: Boolean = true, + animateOnStart: Boolean = true, + selectedBarIndex: Int = NO_SELECTION, +) { + val errors = + remember(dataSet, style.barColors) { + validateHistogramData( + data = dataSet.data.item, + colorsSize = style.barColors.size, + ) + } + + if (errors.isEmpty()) { + Box(modifier = Modifier.testTag(TestTags.HISTOGRAM_CHART)) { + BarChart( + dataSet = dataSet, + style = style, + interactionEnabled = interactionEnabled, + animateOnStart = animateOnStart, + selectedBarIndex = selectedBarIndex, + ) + } + } else { + ChartErrors(style = style.chartViewStyle, errors = errors.toImmutableList()) + } +} diff --git a/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/HistogramChartPreviews.kt b/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/HistogramChartPreviews.kt new file mode 100644 index 00000000..831e6b9d --- /dev/null +++ b/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/HistogramChartPreviews.kt @@ -0,0 +1,39 @@ +package io.github.dautovicharis.charts + +import androidx.compose.runtime.Composable +import io.github.dautovicharis.charts.model.toChartDataSet +import io.github.dautovicharis.charts.style.HistogramChartDefaults + +private const val HISTOGRAM_CHART_TITLE = "Histogram" +private val HISTOGRAM_VALUES = listOf(4f, 8f, 11f, 9f, 6f, 3f) + +@Composable +private fun HistogramChartPreviewContent() { + HistogramChart( + dataSet = + HISTOGRAM_VALUES.toChartDataSet( + title = HISTOGRAM_CHART_TITLE, + labels = listOf("0-10", "10-20", "20-30", "30-40", "40-50", "50-60"), + ), + style = HistogramChartDefaults.style(), + ) +} + +@ChartsPreviewLightDark +@Composable +private fun HistogramChartPreview() { + ChartsPreviewTheme { + HistogramChartPreviewContent() + } +} + +@ChartsPreviewLightDark +@Composable +private fun HistogramChartErrorPreview() { + ChartsPreviewTheme { + HistogramChart( + dataSet = listOf(-2f, 4f).toChartDataSet(title = HISTOGRAM_CHART_TITLE), + style = HistogramChartDefaults.style(), + ) + } +} diff --git a/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/HistogramValidation.kt b/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/HistogramValidation.kt new file mode 100644 index 00000000..ce46c536 --- /dev/null +++ b/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/internal/HistogramValidation.kt @@ -0,0 +1,19 @@ +package io.github.dautovicharis.charts.internal + +import io.github.dautovicharis.charts.internal.common.model.ChartData + +@InternalChartsApi +fun validateHistogramData( + data: ChartData, + colorsSize: Int = 0, +): List { + val validationErrors = validateBarData(data = data, colorsSize = colorsSize).toMutableList() + + data.points.forEachIndexed { index, value -> + if (value < 0) { + validationErrors.add(ValidationErrors.RULE_DATA_POINT_NEGATIVE.format(index)) + } + } + + return validationErrors +} diff --git a/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/style/HistogramChartStyle.kt b/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/style/HistogramChartStyle.kt new file mode 100644 index 00000000..184981c3 --- /dev/null +++ b/charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts/style/HistogramChartStyle.kt @@ -0,0 +1,81 @@ +package io.github.dautovicharis.charts.style + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * Public style alias for histogram charts. + */ +typealias HistogramChartStyle = BarChartStyle + +/** + * Defaults for [HistogramChartStyle]. + * + * Histogram defaults enforce contiguous bins and zero baseline. + */ +object HistogramChartDefaults { + @Composable + fun style( + barColor: Color = MaterialTheme.colorScheme.primary, + barColors: List = emptyList(), + barAlpha: Float = defaultChartAlpha(), + space: Dp = 0.dp, + minValue: Float? = 0f, + maxValue: Float? = null, + minBarWidth: Dp = 10.dp, + zoomControlsVisible: Boolean = true, + gridVisible: Boolean = true, + gridSteps: Int = 4, + gridColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.15f), + gridLineWidth: Float = 1f, + axisVisible: Boolean = true, + axisColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), + axisLineWidth: Float = 1f, + xAxisLabelsVisible: Boolean = true, + xAxisLabelColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), + xAxisLabelSize: TextUnit = 11.sp, + xAxisLabelMaxCount: Int = 6, + selectionLineVisible: Boolean = true, + selectionLineColor: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), + selectionLineWidth: Float = 1f, + chartViewStyle: ChartViewStyle = ChartViewDefaults.style(), + yAxisLabelsVisible: Boolean = true, + yAxisLabelColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), + yAxisLabelSize: TextUnit = 11.sp, + yAxisLabelCount: Int = 5, + ): HistogramChartStyle = + BarChartDefaults.style( + barColor = barColor, + barColors = barColors, + barAlpha = barAlpha, + space = space, + minValue = minValue, + maxValue = maxValue, + minBarWidth = minBarWidth, + zoomControlsVisible = zoomControlsVisible, + gridVisible = gridVisible, + gridSteps = gridSteps, + gridColor = gridColor, + gridLineWidth = gridLineWidth, + axisVisible = axisVisible, + axisColor = axisColor, + axisLineWidth = axisLineWidth, + xAxisLabelsVisible = xAxisLabelsVisible, + xAxisLabelColor = xAxisLabelColor, + xAxisLabelSize = xAxisLabelSize, + xAxisLabelMaxCount = xAxisLabelMaxCount, + selectionLineVisible = selectionLineVisible, + selectionLineColor = selectionLineColor, + selectionLineWidth = selectionLineWidth, + chartViewStyle = chartViewStyle, + yAxisLabelsVisible = yAxisLabelsVisible, + yAxisLabelColor = yAxisLabelColor, + yAxisLabelSize = yAxisLabelSize, + yAxisLabelCount = yAxisLabelCount, + ) +} diff --git a/charts-histogram/src/commonTest/kotlin/io/github/dautovicharis/charts/ui/HistogramChartTest.kt b/charts-histogram/src/commonTest/kotlin/io/github/dautovicharis/charts/ui/HistogramChartTest.kt new file mode 100644 index 00000000..56a21e30 --- /dev/null +++ b/charts-histogram/src/commonTest/kotlin/io/github/dautovicharis/charts/ui/HistogramChartTest.kt @@ -0,0 +1,106 @@ +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 +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.runComposeUiTest +import io.github.dautovicharis.charts.HistogramChart +import io.github.dautovicharis.charts.internal.TestTags +import io.github.dautovicharis.charts.internal.ValidationErrors.RULE_COLORS_SIZE_MISMATCH +import io.github.dautovicharis.charts.internal.ValidationErrors.RULE_DATA_POINT_NEGATIVE +import io.github.dautovicharis.charts.internal.format +import io.github.dautovicharis.charts.model.toChartDataSet +import io.github.dautovicharis.charts.style.HistogramChartDefaults +import kotlin.test.Test + +class HistogramChartTest { + private val dataSet = + listOf(3f, 7f, 10f, 6f) + .toChartDataSet( + title = "Histogram", + labels = listOf("0-10", "10-20", "20-30", "30-40"), + ) + + @OptIn(ExperimentalTestApi::class) + @Test + fun histogramChart_withValidData_displaysChart() = + runComposeUiTest { + setContent { + HistogramChart(dataSet) + } + + onNodeWithTag(TestTags.HISTOGRAM_CHART).isDisplayed() + onNodeWithTag(TestTags.CHART_TITLE) + .assertTextEquals(dataSet.data.label) + .isDisplayed() + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun histogramChart_withNegativeData_displaysError() = + runComposeUiTest { + val expectedError = RULE_DATA_POINT_NEGATIVE.format(1) + + setContent { + HistogramChart( + dataSet = + listOf(2f, -1f, 4f).toChartDataSet( + title = "Histogram", + labels = listOf("0-10", "10-20", "20-30"), + ), + ) + } + + onNodeWithTag(TestTags.CHART_ERROR).isDisplayed() + onNodeWithText("${expectedError}\n").isDisplayed() + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun histogramChart_withSelectedBarIndex_displaysSelectedBarDetails() = + runComposeUiTest { + val selectedBarIndex = 2 + val expectedLabel = dataSet.data.item.labels[selectedBarIndex] + val expectedValue = dataSet.data.item.points[selectedBarIndex] + val expectedTitle = "$expectedLabel: $expectedValue" + + setContent { + HistogramChart( + dataSet = dataSet, + interactionEnabled = false, + animateOnStart = false, + selectedBarIndex = selectedBarIndex, + ) + } + + onNodeWithTag(TestTags.HISTOGRAM_CHART).isDisplayed() + onNodeWithTag(TestTags.CHART_TITLE) + .assertTextEquals(expectedTitle) + .isDisplayed() + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun histogramChart_withInvalidBarColors_displaysError() = + runComposeUiTest { + val expectedError = + RULE_COLORS_SIZE_MISMATCH.format( + 2, + dataSet.data.item.points.size, + ) + + setContent { + val style = HistogramChartDefaults.style(barColors = listOf(Color.Red, Color.Green)) + HistogramChart( + dataSet = dataSet, + style = style, + ) + } + + onNodeWithTag(TestTags.CHART_ERROR).isDisplayed() + onNodeWithText("${expectedError}\n").isDisplayed() + } +} diff --git a/charts/build.gradle.kts b/charts/build.gradle.kts index 982b5392..806dc53b 100644 --- a/charts/build.gradle.kts +++ b/charts/build.gradle.kts @@ -49,6 +49,7 @@ kotlin { api(projects.chartsLine) api(projects.chartsPie) api(projects.chartsBar) + api(projects.chartsHistogram) api(projects.chartsStackedBar) api(projects.chartsStackedArea) api(projects.chartsRadar) @@ -78,6 +79,7 @@ private val apiSourceRoots = project.rootDir.resolve("charts-line/src/commonMain/kotlin/io/github/dautovicharis/charts"), project.rootDir.resolve("charts-pie/src/commonMain/kotlin/io/github/dautovicharis/charts"), project.rootDir.resolve("charts-bar/src/commonMain/kotlin/io/github/dautovicharis/charts"), + project.rootDir.resolve("charts-histogram/src/commonMain/kotlin/io/github/dautovicharis/charts"), project.rootDir.resolve("charts-stacked-bar/src/commonMain/kotlin/io/github/dautovicharis/charts"), project.rootDir.resolve("charts-stacked-area/src/commonMain/kotlin/io/github/dautovicharis/charts"), project.rootDir.resolve("charts-radar/src/commonMain/kotlin/io/github/dautovicharis/charts"), diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f53b894..cbdbcc9c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,6 +45,7 @@ include(":charts") include(":charts-demo-shared") include(":charts-core") include(":charts-bar") +include(":charts-histogram") include(":charts-line") include(":charts-pie") include(":charts-radar")