├── docs ├── styles │ ├── logo-styles.css │ └── jetbrains-mono.css ├── images │ ├── logo-icon.svg │ ├── copy-icon.svg │ ├── footer-go-to-link.svg │ ├── arrow_down.svg │ ├── copy-successful-icon.svg │ ├── go-to-top-icon.svg │ ├── anchor-copy-button.svg │ └── docs_logo.svg ├── scripts │ ├── sourceset_dependencies.js │ ├── clipboard.js │ └── navigation-loader.js └── -kotlin multiplatform charts │ ├── com.netguru.multiplatform.charts.line │ ├── -symbol-shape │ │ ├── -l-i-n-e │ │ │ ├── name.html │ │ │ └── ordinal.html │ │ └── -r-e-c-t-a-n-g-l-e │ │ │ ├── name.html │ │ │ └── ordinal.html │ └── -point-f │ │ ├── x.html │ │ └── y.html │ └── com.netguru.multiplatform.charts.bubble │ └── -bubble │ └── update.html ├── assets ├── bar-chart.png ├── pie-chart.png ├── bubble-chart.png ├── dial-chart.png ├── gas-bottle.png ├── line-chart.png └── readme_netguru_logo.png ├── charts ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── netguru │ │ └── multiplatform │ │ └── charts │ │ ├── grid │ │ ├── axisscale │ │ │ ├── XAxisScale.kt │ │ │ ├── FixedTicksXAxisScale.kt │ │ │ ├── TimestampXAxisScale.kt │ │ │ └── YAxisScale.kt │ │ ├── LineParameters.kt │ │ ├── GridChartData.kt │ │ ├── ChartGrid.kt │ │ ├── GridDefaults.kt │ │ ├── ChartAxis.kt │ │ └── GridChartDrawing.kt │ │ ├── bar │ │ ├── BarChartBar.kt │ │ ├── BarChartDefaults.kt │ │ ├── BarChartEntry.kt │ │ ├── BarChartCategory.kt │ │ ├── BarChartColors.kt │ │ ├── BarChartConfig.kt │ │ ├── BarChartData.kt │ │ ├── BarChartWithLegend.kt │ │ └── BarChartDrawing.kt │ │ ├── theme │ │ ├── ChartColors.kt │ │ ├── Theme.kt │ │ └── ChartDefaults.kt │ │ ├── gasbottle │ │ └── GasBottleColors.kt │ │ ├── line │ │ ├── LineChartColors.kt │ │ ├── LineChartWithLegend.kt │ │ ├── LineChartSeries.kt │ │ └── Line.kt │ │ ├── dial │ │ ├── DialColors.kt │ │ ├── DialDefaults.kt │ │ ├── DialConfig.kt │ │ └── PercentageDial.kt │ │ ├── ChartAnimation.kt │ │ ├── bubble │ │ ├── Bubble.kt │ │ ├── Vector.kt │ │ └── BubbleDefaults.kt │ │ ├── pie │ │ ├── PieDefaults.kt │ │ ├── PieChartConfig.kt │ │ └── PieChartWithLegend.kt │ │ ├── Helpers.kt │ │ └── OverlayInformation.kt └── build.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── example-app ├── common │ ├── src │ │ ├── androidMain │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── netguru │ │ │ │ └── multiplatform │ │ │ │ └── charts │ │ │ │ └── common │ │ │ │ ├── locale │ │ │ │ └── setApplicationLocale.kt │ │ │ │ └── Resources.kt │ │ ├── commonMain │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── netguru │ │ │ │ │ └── multiplatform │ │ │ │ │ └── charts │ │ │ │ │ └── common │ │ │ │ │ ├── TimeUtil.kt │ │ │ │ │ ├── locale │ │ │ │ │ ├── setApplicationLocale.kt │ │ │ │ │ └── LocaleProvider.kt │ │ │ │ │ ├── ButtonUtil.kt │ │ │ │ │ ├── Resources.kt │ │ │ │ │ ├── WindowSize.kt │ │ │ │ │ └── Divider.kt │ │ │ └── resources │ │ │ │ ├── font │ │ │ │ ├── aeonik_bold.otf │ │ │ │ ├── aeonik_medium.otf │ │ │ │ └── aeonik_regular.otf │ │ │ │ ├── drawable │ │ │ │ ├── netguru_logo_dark.png │ │ │ │ └── netguru_logo_light.png │ │ │ │ └── values │ │ │ │ └── strings_en.xml │ │ └── desktopMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── netguru │ │ │ └── multiplatform │ │ │ └── charts │ │ │ └── common │ │ │ ├── locale │ │ │ └── setApplicationLocale.kt │ │ │ └── Resources.kt │ └── build.gradle.kts ├── application │ ├── src │ │ ├── commonMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── netguru │ │ │ │ └── multiplatform │ │ │ │ └── charts │ │ │ │ └── application │ │ │ │ ├── AppAction.kt │ │ │ │ ├── navigation │ │ │ │ ├── NavigationAction.kt │ │ │ │ ├── NavigationState.kt │ │ │ │ └── NavigationStore.kt │ │ │ │ ├── home │ │ │ │ └── HomeMapper.kt │ │ │ │ ├── ActionDispatcher.kt │ │ │ │ ├── Application.kt │ │ │ │ ├── UiWrappers.kt │ │ │ │ ├── Store.kt │ │ │ │ └── screen │ │ │ │ ├── BubbleChartScreen.kt │ │ │ │ ├── DialChartScreen.kt │ │ │ │ └── LineChartScreen.kt │ │ └── androidMain │ │ │ └── AndroidManifest.xml │ └── build.gradle.kts ├── android │ ├── screenshots │ │ └── debug │ │ │ ├── Nexus_10_API_31 │ │ │ ├── com.netguru.multiplatform.charts.pie.PieTest_threeCategories.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieTest_twoCategories.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieTest_twoCategories_withZeroValue.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_0_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_0_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_100_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_150_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_50_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_fiveBars_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_negativeValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_100_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_150_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_50_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_no_labels.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_no_labels.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories_customUI.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_threeCategories_oneBar_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_negativeValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_minus100_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories_withZeroValue.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_minus50_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues_customUI.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues_customUI.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_fiveBars_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveAndNegativeValues.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_custom_colors_and_labels.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_custom_colors_and_labels.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues_customUI.png │ │ │ └── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveAndNegativeValues_customUI.png │ │ │ └── Pixel_4a_API_31 │ │ │ ├── com.netguru.multiplatform.charts.pie.PieTest_threeCategories.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieTest_twoCategories.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieTest_twoCategories_withZeroValue.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_0_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_0_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_100_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_150_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_50_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_fiveBars_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_negativeValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_100_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_150_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_50_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_no_labels.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_no_labels.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories_customUI.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_threeCategories_oneBar_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_negativeValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_minus100_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories_withZeroValue.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_minus50_UI_default.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues_customUI.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues_customUI.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_fiveBars_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveAndNegativeValues.png │ │ │ ├── com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_custom_colors_and_labels.png │ │ │ ├── com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_custom_colors_and_labels.png │ │ │ ├── com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues_customUI.png │ │ │ └── com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveAndNegativeValues_customUI.png │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── netguru │ │ │ │ └── multiplatform │ │ │ │ └── charts │ │ │ │ └── android │ │ │ │ ├── previews │ │ │ │ ├── HomePreviews.kt │ │ │ │ ├── LineChartPreview.kt │ │ │ │ └── BarChartPreview.kt │ │ │ │ └── MainActivity.kt │ │ └── androidTest │ │ │ └── kotlin │ │ │ └── com │ │ │ └── netguru │ │ │ └── multiplatform │ │ │ └── charts │ │ │ ├── Util.kt │ │ │ ├── line │ │ │ ├── Data.kt │ │ │ ├── LineWithLegendTest.kt │ │ │ └── LineTest.kt │ │ │ ├── bar │ │ │ ├── Data.kt │ │ │ └── BarWithLegendTest.kt │ │ │ ├── pie │ │ │ ├── PieTest.kt │ │ │ └── PieWithLegendTest.kt │ │ │ ├── gasbottle │ │ │ └── GasBottleTest.kt │ │ │ └── dial │ │ │ ├── PercentageDialTest.kt │ │ │ └── DialTest.kt │ └── build.gradle.kts └── desktop │ ├── src │ └── jvmMain │ │ └── kotlin │ │ └── Main.kt │ └── build.gradle.kts ├── SECURITY.md ├── .gitignore ├── gradle.properties ├── pull_request_template.md ├── settings.gradle.kts ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE.md ├── gradlew.bat └── CONTRIBUTING.md /docs/styles/logo-styles.css: -------------------------------------------------------------------------------- 1 | #logo { 2 | background-image: url(../images/docs_logo.svg); 3 | } -------------------------------------------------------------------------------- /assets/bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/assets/bar-chart.png -------------------------------------------------------------------------------- /assets/pie-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/assets/pie-chart.png -------------------------------------------------------------------------------- /assets/bubble-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/assets/bubble-chart.png -------------------------------------------------------------------------------- /assets/dial-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/assets/dial-chart.png -------------------------------------------------------------------------------- /assets/gas-bottle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/assets/gas-bottle.png -------------------------------------------------------------------------------- /assets/line-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/assets/line-chart.png -------------------------------------------------------------------------------- /charts/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/readme_netguru_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/assets/readme_netguru_logo.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example-app/common/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/kotlin/com/netguru/multiplatform/charts/common/TimeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common 2 | 3 | const val HOUR_IN_MS = 3600000 4 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/AppAction.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application 2 | 3 | interface AppAction 4 | -------------------------------------------------------------------------------- /example-app/application/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/resources/font/aeonik_bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/common/src/commonMain/resources/font/aeonik_bold.otf -------------------------------------------------------------------------------- /docs/images/logo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/resources/font/aeonik_medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/common/src/commonMain/resources/font/aeonik_medium.otf -------------------------------------------------------------------------------- /example-app/common/src/commonMain/resources/font/aeonik_regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/common/src/commonMain/resources/font/aeonik_regular.otf -------------------------------------------------------------------------------- /example-app/common/src/commonMain/resources/drawable/netguru_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/common/src/commonMain/resources/drawable/netguru_logo_dark.png -------------------------------------------------------------------------------- /example-app/common/src/commonMain/resources/drawable/netguru_logo_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/common/src/commonMain/resources/drawable/netguru_logo_light.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Vulnerabilities 2 | 3 | If you find a security issue with the application, please reach out 4 | to [compose.multiplatform.charts@netguru.com](mailto:compose.multiplatform.charts@netguru.com) 5 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/kotlin/com/netguru/multiplatform/charts/common/locale/setApplicationLocale.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common.locale 2 | 3 | internal expect fun setApplicationLocale(localeTag: String) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | charts/build/ 3 | example-app/android/build/ 4 | example-app/application/build/ 5 | example-app/common/build/ 6 | example-app/desktop/build/ 7 | modules.xml 8 | local.properties 9 | .gradle 10 | .idea/ 11 | .DS_Store -------------------------------------------------------------------------------- /docs/images/copy-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/images/footer-go-to-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/axisscale/XAxisScale.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid.axisscale 2 | 3 | interface XAxisScale { 4 | val tick: Long 5 | val min: Long 6 | val max: Long 7 | val start: Long 8 | } 9 | -------------------------------------------------------------------------------- /docs/images/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/images/copy-successful-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartBar.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | data class BarChartBar( 4 | val width: ClosedFloatingPointRange, 5 | val height: ClosedFloatingPointRange, 6 | val data: BarChartEntry 7 | ) 8 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/LineParameters.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class LineParameters( 7 | val position: Float, 8 | val value: Number, 9 | ) 10 | -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieTest_threeCategories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieTest_threeCategories.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieTest_twoCategories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieTest_twoCategories.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieTest_threeCategories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieTest_threeCategories.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieTest_twoCategories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieTest_twoCategories.png -------------------------------------------------------------------------------- /docs/images/go-to-top-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories.png -------------------------------------------------------------------------------- /example-app/common/src/desktopMain/kotlin/com/netguru/multiplatform/charts/common/locale/setApplicationLocale.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common.locale 2 | 3 | import java.util.Locale 4 | 5 | internal actual fun setApplicationLocale(localeTag: String) { 6 | Locale.setDefault(Locale(localeTag)) 7 | } 8 | -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieTest_twoCategories_withZeroValue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieTest_twoCategories_withZeroValue.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieTest_twoCategories_withZeroValue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieTest_twoCategories_withZeroValue.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_0_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_0_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_0_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_0_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_0_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_0_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_100_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_100_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_150_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_150_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_50_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_50_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_0_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_0_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_100_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_100_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_150_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_150_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_50_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_50_UI_default.png -------------------------------------------------------------------------------- /example-app/common/src/commonMain/resources/values/strings_en.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | KMP Charts examples 4 | Navigation panel logo 5 | Navigation menu 6 | 7 | -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_fiveBars_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_fiveBars_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_negativeValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_negativeValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_100_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_100_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_150_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_150_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_50_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_50_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_no_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_no_labels.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_no_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_no_labels.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories_customUI.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_fiveBars_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_fiveBars_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_negativeValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_negativeValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_100_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_100_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_150_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_150_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_50_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_50_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_no_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_no_labels.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_no_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_no_labels.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_threeCategories_customUI.png -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | internal object BarChartDefaults { 6 | val BAR_THICKNESS = 14.dp 7 | val BAR_CORNER_RADIUS = 7.dp 8 | val BAR_HORIZONTAL_SPACING = 4.dp 9 | } 10 | -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_threeCategories_oneBar_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_threeCategories_oneBar_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_negativeValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_negativeValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_minus100_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_minus100_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories_withZeroValue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories_withZeroValue.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_threeCategories_oneBar_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_threeCategories_oneBar_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_negativeValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_negativeValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_minus100_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_minus100_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories_withZeroValue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.pie.PieWithLegendTest_twoCategories_withZeroValue.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_minus50_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_minus50_UI_default.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_minus50_UI_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_minus50_UI_default.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx3072m -XX:+UseParallelGC 4 | org.gradle.caching=true 5 | org.gradle.configureondemand=true 6 | org.gradle.parallel=true 7 | android.defaults.buildfeatures.buildconfig=true 8 | android.nonTransitiveRClass=false 9 | android.nonFinalResIds=false 10 | -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues_customUI.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues_customUI.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_negativeValues_customUI.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_oneBar_positiveValues_customUI.png -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartEntry.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | @Immutable 7 | data class BarChartEntry( 8 | val x: String, 9 | val y: Float, 10 | val color: Color 11 | ) 12 | -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_fiveBars_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_fiveBars_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_fiveBars_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_fiveBars_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveAndNegativeValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveAndNegativeValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveAndNegativeValues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_twoCategories_twoBars_positiveAndNegativeValues.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_custom_colors_and_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_custom_colors_and_labels.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_custom_colors_and_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_custom_colors_and_labels.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_custom_colors_and_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.DialTest_range_0_100_value_69_UI_custom_colors_and_labels.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_custom_colors_and_labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.dial.PercentageDialTest_value_69_UI_custom_colors_and_labels.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues_customUI.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarWithLegendTest_oneCategory_oneBar_positiveValues_customUI.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveAndNegativeValues_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Nexus_10_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveAndNegativeValues_customUI.png -------------------------------------------------------------------------------- /example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveAndNegativeValues_customUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/compose-multiplatform-charts/HEAD/example-app/android/screenshots/debug/Pixel_4a_API_31/com.netguru.multiplatform.charts.bar.BarTest_oneCategory_twoBars_positiveAndNegativeValues_customUI.png -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/GridChartData.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid 2 | 3 | import com.netguru.multiplatform.charts.line.LegendItemData 4 | 5 | interface GridChartData { 6 | val minX: Long 7 | val maxX: Long 8 | val minY: Float 9 | val maxY: Float 10 | val legendData: List 11 | } 12 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Task 2 | 3 | - [JIRA](https://netguru.atlassian.net/browse/BN-) 4 | 5 | ### Description 6 | 7 | 8 | ### Additional Notes (optional) 9 | 10 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/axisscale/FixedTicksXAxisScale.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid.axisscale 2 | 3 | class FixedTicksXAxisScale( 4 | override val min: Long, 5 | override val max: Long, 6 | tickCount: Int 7 | ) : XAxisScale { 8 | override val tick: Long = (max - min) / tickCount 9 | override val start: Long = min 10 | } 11 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartCategory.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class BarChartCategory( 7 | val name: String, 8 | val entries: List 9 | ) { 10 | val minY: Float 11 | get() = entries.minOf { it.y } 12 | val maxY: Float 13 | get() = entries.maxOf { it.y } 14 | } 15 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/navigation/NavigationAction.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application.navigation 2 | 3 | import com.netguru.multiplatform.charts.application.AppAction 4 | 5 | sealed class NavigationAction : AppAction { 6 | 7 | object ToggleDrawer : NavigationAction() 8 | 9 | data class OpenTab(val tab: NavigationState.Tab) : NavigationAction() 10 | } 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | } 8 | } 9 | rootProject.name = "compose-multiplatform-charts" 10 | 11 | include(":charts") 12 | include(":example-app:application") 13 | include(":example-app:common") 14 | include(":example-app:android") 15 | include(":example-app:desktop") 16 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/navigation/NavigationState.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application.navigation 2 | 3 | data class NavigationState( 4 | val isDrawerOpened: Boolean = false, 5 | val currentTab: Tab = Tab.BAR 6 | ) { 7 | enum class Tab { 8 | BAR, 9 | BUBBLE, 10 | DIAL, 11 | GAS_BOTTLE, 12 | LINE, 13 | PIE, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/theme/ChartColors.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.theme 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | @Immutable 7 | data class ChartColors constructor( 8 | val primary: Color, 9 | val surface: Color, 10 | val grid: Color, 11 | val emptyGasBottle: Color, 12 | val fullGasBottle: Color, 13 | val overlayLine: Color, 14 | ) 15 | -------------------------------------------------------------------------------- /docs/scripts/sourceset_dependencies.js: -------------------------------------------------------------------------------- 1 | sourceset_dependencies='{":charts:dokkaHtml/androidAndroidTestRelease":[],":charts:dokkaHtml/androidDebug":[],":charts:dokkaHtml/androidMain":[":charts:dokkaHtml/commonMain"],":charts:dokkaHtml/androidRelease":[],":charts:dokkaHtml/androidTestFixtures":[],":charts:dokkaHtml/androidTestFixturesDebug":[],":charts:dokkaHtml/androidTestFixturesRelease":[],":charts:dokkaHtml/commonMain":[],":charts:dokkaHtml/desktopMain":[":charts:dokkaHtml/commonMain"]}' 2 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/ChartGrid.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlin.math.abs 5 | 6 | @Immutable 7 | data class ChartGrid( 8 | val verticalLines: List, 9 | val horizontalLines: List, 10 | val zeroPosition: LineParameters, 11 | ) { 12 | val distanceBetweenVerticalLines: Float 13 | get() = abs(verticalLines[1].position - verticalLines[0].position) 14 | } 15 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartColors.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | import com.netguru.multiplatform.charts.theme.ChartColors 6 | 7 | @Immutable 8 | data class BarChartColors( 9 | val grid: Color, 10 | val surface: Color, 11 | ) 12 | 13 | val ChartColors.barChartColors 14 | get() = BarChartColors( 15 | grid = grid, 16 | surface = surface, 17 | ) 18 | -------------------------------------------------------------------------------- /example-app/common/src/androidMain/kotlin/com/netguru/multiplatform/charts/common/locale/setApplicationLocale.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common.locale 2 | 3 | import android.content.res.Resources 4 | import java.util.Locale 5 | 6 | internal actual fun setApplicationLocale(localeTag: String) { 7 | val resources = Resources.getSystem() 8 | val configuration = resources.configuration 9 | 10 | val locale = Locale(localeTag) 11 | configuration.setLocale(locale) 12 | resources.updateConfiguration(configuration, resources.displayMetrics) 13 | } 14 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/kotlin/com/netguru/multiplatform/charts/common/ButtonUtil.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common 2 | 3 | import androidx.compose.material.ButtonDefaults 4 | import androidx.compose.material.ButtonElevation 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.unit.dp 7 | 8 | @Composable 9 | fun ButtonDefaults.noElevation(): ButtonElevation = elevation( 10 | defaultElevation = 0.dp, 11 | pressedElevation = 0.dp, 12 | disabledElevation = 0.dp, 13 | hoveredElevation = 0.dp, 14 | focusedElevation = 0.dp 15 | ) 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? What motivated You to create this request?** 11 | A clear and concise description of what the problem is, e.g., I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like to see** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/gasbottle/GasBottleColors.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.gasbottle 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | import com.netguru.multiplatform.charts.theme.ChartColors 6 | 7 | @Immutable 8 | data class GasBottleColors( 9 | val fullGasBottle: Color, 10 | val emptyGasBottle: Color, 11 | ) 12 | 13 | val ChartColors.gasBottleColors 14 | get() = GasBottleColors( 15 | fullGasBottle = fullGasBottle, 16 | emptyGasBottle = emptyGasBottle, 17 | ) 18 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/kotlin/com/netguru/multiplatform/charts/common/Resources.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.painter.Painter 5 | import androidx.compose.ui.text.font.Font 6 | import androidx.compose.ui.text.font.FontStyle 7 | import androidx.compose.ui.text.font.FontWeight 8 | 9 | @Composable 10 | expect fun imageResources(image: String): Painter 11 | 12 | @Composable 13 | expect fun fontResources( 14 | font: String, 15 | weight: FontWeight, 16 | style: FontStyle 17 | ): Font 18 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/line/LineChartColors.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.line 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | import com.netguru.multiplatform.charts.theme.ChartColors 6 | 7 | @Immutable 8 | data class LineChartColors( 9 | val grid: Color, 10 | val surface: Color, 11 | val overlayLine: Color, 12 | ) 13 | 14 | val ChartColors.lineChartColors 15 | get() = LineChartColors( 16 | grid = grid, 17 | surface = surface, 18 | overlayLine = overlayLine, 19 | ) 20 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/kotlin/com/netguru/multiplatform/charts/common/WindowSize.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import androidx.compose.ui.unit.dp 5 | 6 | enum class WindowSize { 7 | COMPACT, 8 | MEDIUM, 9 | EXPANDED; 10 | 11 | companion object { 12 | fun basedOnWidth(windowWidth: Dp): WindowSize { 13 | return when { 14 | windowWidth < 600.dp -> COMPACT 15 | windowWidth < 1000.dp -> MEDIUM 16 | else -> EXPANDED 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/dial/DialColors.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.dial 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | import com.netguru.multiplatform.charts.theme.ChartColors 6 | 7 | @Immutable 8 | data class DialColors( 9 | val progressBarColor: Color, 10 | val progressBarBackgroundColor: Color, 11 | val gridScaleColor: Color, 12 | ) 13 | 14 | val ChartColors.dialColors 15 | get() = DialColors( 16 | progressBarColor = primary, 17 | progressBarBackgroundColor = grid, 18 | gridScaleColor = grid 19 | ) 20 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/home/HomeMapper.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.netguru.multiplatform.charts.application.navigation.NavigationState 5 | 6 | @Composable 7 | fun NavigationState.Tab.toLabel(): String = when (this) { 8 | NavigationState.Tab.BAR -> "Bar" 9 | NavigationState.Tab.BUBBLE -> "Bubble" 10 | NavigationState.Tab.DIAL -> "Dial" 11 | NavigationState.Tab.GAS_BOTTLE -> "Gas bottle" 12 | NavigationState.Tab.LINE -> "Line" 13 | NavigationState.Tab.PIE -> "Pie" 14 | } 15 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/kotlin/com/netguru/multiplatform/charts/common/Divider.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.material.Divider 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | 9 | @Composable 10 | fun HorizontalDivider(modifier: Modifier = Modifier) { 11 | Divider( 12 | modifier = modifier 13 | .height(AppTheme.dimens.borders_thickness) 14 | .fillMaxWidth(), 15 | color = AppTheme.colors.borders 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/ActionDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application 2 | 3 | import com.netguru.multiplatform.charts.application.navigation.NavigationAction 4 | import com.netguru.multiplatform.charts.application.navigation.NavigationStore 5 | 6 | class ActionDispatcher { 7 | 8 | private val navigationStore = NavigationStore() 9 | 10 | val navigationState by lazy { navigationStore.state } 11 | 12 | fun dispatch(action: AppAction) { 13 | when (action) { 14 | is NavigationAction -> navigationStore.dispatch(action) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example-app/common/src/commonMain/kotlin/com/netguru/multiplatform/charts/common/locale/LocaleProvider.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common.locale 2 | 3 | import androidx.compose.ui.text.intl.Locale 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import kotlinx.coroutines.flow.StateFlow 6 | import kotlinx.coroutines.flow.update 7 | 8 | object LocaleProvider { 9 | 10 | val locale: StateFlow 11 | get() = _locale 12 | 13 | private val _locale = MutableStateFlow(Locale.current.language) 14 | 15 | fun setLocale(localeTag: String) { 16 | _locale.update { localeTag } 17 | setApplicationLocale(localeTag) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/Application.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.staticCompositionLocalOf 5 | import com.netguru.multiplatform.charts.application.home.HomeScreen 6 | import com.netguru.multiplatform.charts.common.AppTheme 7 | import com.netguru.multiplatform.charts.common.WindowSize 8 | 9 | val LocalActionDispatcher = staticCompositionLocalOf { ActionDispatcher() } 10 | 11 | @Composable 12 | fun Application(windowSize: WindowSize) { 13 | AppTheme(windowSize) { 14 | HomeScreen() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 'Report a bug ' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 19 | **Actual behaviour** 20 | A clear and concise description of what happened. 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Environment:** 26 | Provide environment on which the issue exists, e.g., GooglePixel 5, Android 13 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /example-app/common/src/desktopMain/kotlin/com/netguru/multiplatform/charts/common/Resources.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.painter.Painter 5 | import androidx.compose.ui.res.painterResource 6 | import androidx.compose.ui.text.font.Font 7 | import androidx.compose.ui.text.font.FontStyle 8 | import androidx.compose.ui.text.font.FontWeight 9 | 10 | @Composable 11 | actual fun imageResources(image: String): Painter = painterResource("drawable/$image") 12 | 13 | actual fun fontResources( 14 | font: String, 15 | weight: FontWeight, 16 | style: FontStyle 17 | ): Font = androidx.compose.ui.text.platform.Font("font/$font", weight, style) 18 | -------------------------------------------------------------------------------- /docs/images/anchor-copy-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.staticCompositionLocalOf 5 | import androidx.compose.ui.graphics.Color 6 | 7 | val LocalChartColors = staticCompositionLocalOf { 8 | ChartColors( 9 | primary = Color.Unspecified, 10 | surface = Color.Unspecified, 11 | grid = Color.Unspecified, 12 | emptyGasBottle = Color.Unspecified, 13 | fullGasBottle = Color.Unspecified, 14 | overlayLine = Color.Unspecified, 15 | ) 16 | } 17 | 18 | internal object ChartTheme { 19 | val colors: ChartColors 20 | @Composable 21 | get() = LocalChartColors.current 22 | } 23 | -------------------------------------------------------------------------------- /example-app/desktop/src/jvmMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.unit.DpSize 2 | import androidx.compose.ui.unit.dp 3 | import androidx.compose.ui.window.Window 4 | import androidx.compose.ui.window.application 5 | import androidx.compose.ui.window.rememberWindowState 6 | import com.netguru.multiplatform.charts.application.Application 7 | import com.netguru.multiplatform.charts.common.AppTheme.strings 8 | import com.netguru.multiplatform.charts.common.WindowSize 9 | 10 | fun main() = application { 11 | val windowState = rememberWindowState(size = DpSize(1050.dp, 950.dp)) 12 | Window( 13 | onCloseRequest = ::exitApplication, 14 | state = windowState, 15 | title = strings.app_name 16 | ) { 17 | Application(WindowSize.basedOnWidth(windowState.size.width)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example-app/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/ChartAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts 2 | 3 | import androidx.compose.animation.core.AnimationSpec 4 | import androidx.compose.animation.core.tween 5 | 6 | sealed class ChartAnimation { 7 | object Disabled : ChartAnimation() 8 | 9 | class Simple( 10 | val animationSpec: () -> AnimationSpec = { 11 | tween(DEFAULT_DURATION, DEFAULT_DELAY) 12 | }, 13 | ) : ChartAnimation() 14 | 15 | class Sequenced( 16 | val animationSpec: (dataSeriesIndex: Int) -> AnimationSpec = { index -> 17 | tween(DEFAULT_DURATION, index * DEFAULT_DELAY) 18 | }, 19 | ) : ChartAnimation() 20 | 21 | private companion object { 22 | const val DEFAULT_DURATION = 300 23 | const val DEFAULT_DELAY = 100 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/Util.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.ui.test.junit4.ComposeContentTestRule 6 | import com.karumi.shot.ScreenshotTest 7 | import com.netguru.multiplatform.charts.theme.ChartDefaults 8 | import com.netguru.multiplatform.charts.theme.LocalChartColors 9 | 10 | object Util { 11 | fun ScreenshotTest.checkComposable(composeRule: ComposeContentTestRule, contentToCheck: @Composable () -> Unit) { 12 | composeRule.setContent { 13 | CompositionLocalProvider(LocalChartColors provides ChartDefaults.chartColors()) { 14 | contentToCheck() 15 | } 16 | } 17 | 18 | compareScreenshot(composeRule) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/axisscale/TimestampXAxisScale.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid.axisscale 2 | 3 | class TimestampXAxisScale( 4 | override val min: Long, 5 | override val max: Long, 6 | val maxTicksCount: Int = 10 7 | ) : XAxisScale { 8 | // find first round hour, greater than min timestamp 9 | override val start: Long = min - min % HOUR_MS + HOUR_MS 10 | 11 | // find period of vertical lines based on maxTicksCount, period can be in round hours ie. 1h, 2h, 3h 12 | private val period: Long = HOUR_MS * ((max - min) / HOUR_MS / maxTicksCount) 13 | 14 | // if period is 0 or less set period to round hour 15 | // avoid division by 0 when app starts and min and max are 0 16 | override val tick: Long = if (period > 0) period else HOUR_MS 17 | 18 | companion object { 19 | const val HOUR_MS = 3600000L 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bubble/Bubble.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bubble 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.vector.ImageVector 5 | import kotlin.random.Random 6 | 7 | data class Bubble( 8 | val name: String, 9 | val value: Float, 10 | val icon: ImageVector, 11 | val color: Color, 12 | var radius: Float = value 13 | ) { 14 | var position: Vector = Vector(0f, 0f) 15 | var velocity: Vector = Vector( 16 | Random.nextFloat() - 0.5f, 17 | Random.nextFloat() - 0.5f 18 | ).normalize() 19 | private var acceleration: Vector = Vector(0f, 0f) 20 | 21 | fun applyForce(force: Vector) { 22 | acceleration.add(force) 23 | } 24 | 25 | fun update() { 26 | velocity.add(acceleration) 27 | position.add(velocity) 28 | acceleration.mult(0f) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/navigation/NavigationStore.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application.navigation 2 | 3 | import com.netguru.multiplatform.charts.application.Store 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.Dispatchers 6 | 7 | internal class NavigationStore( 8 | dispatcher: CoroutineDispatcher = Dispatchers.Default 9 | ) : Store(NavigationState(), dispatcher) { 10 | 11 | init { 12 | onAction { toggleDrawer() } 13 | onAction { openTab(it.tab) } 14 | } 15 | 16 | private fun toggleDrawer() { 17 | updateState { copy(isDrawerOpened = isDrawerOpened.not()) } 18 | } 19 | 20 | private fun openTab(tab: NavigationState.Tab) { 21 | updateState { copy(currentTab = tab, isDrawerOpened = false) } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example-app/application/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.netguru.multiplatform.charts.extensions.androidMain 2 | import com.netguru.multiplatform.charts.extensions.baseAndroidSetup 3 | import com.netguru.multiplatform.charts.extensions.baseTestSetup 4 | import com.netguru.multiplatform.charts.extensions.commonMain 5 | 6 | baseAndroidSetup() 7 | baseTestSetup() 8 | 9 | plugins { 10 | kotlin("multiplatform") 11 | id("com.android.library") 12 | @Suppress("DSL_SCOPE_VIOLATION") 13 | alias(libs.plugins.kotlin.compose) 14 | } 15 | 16 | kotlin { 17 | androidTarget() 18 | jvm("desktop") 19 | 20 | sourceSets { 21 | commonMain { 22 | dependencies { 23 | api(project(":example-app:common")) 24 | } 25 | } 26 | androidMain { 27 | dependencies { 28 | api(libs.compose.uiTooling) 29 | } 30 | } 31 | } 32 | } 33 | android { 34 | namespace = "com.netguru.multiplatform.charts.application" 35 | } 36 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/theme/ChartDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.theme 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | 7 | /** 8 | * Contains the default values used by all charts 9 | */ 10 | object ChartDefaults { 11 | @Composable 12 | fun chartColors( 13 | primary: Color = MaterialTheme.colors.primary, 14 | surface: Color = Color.Unspecified, 15 | grid: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f), 16 | emptyGasBottle: Color = MaterialTheme.colors.error, 17 | fullGasBottle: Color = MaterialTheme.colors.primary, 18 | overlayLine: Color = MaterialTheme.colors.error, 19 | ) = ChartColors( 20 | primary = primary, 21 | surface = surface, 22 | grid = grid, 23 | emptyGasBottle = emptyGasBottle, 24 | fullGasBottle = fullGasBottle, 25 | overlayLine = overlayLine, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /docs/styles/jetbrains-mono.css: -------------------------------------------------------------------------------- 1 | @font-face{ 2 | font-family: 'JetBrains Mono'; 3 | src: url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/web/JetBrainsMono-Regular.eot') format('embedded-opentype'), 4 | url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/webfonts/JetBrainsMono-Regular.woff2') format('woff2'), 5 | url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/ttf/JetBrainsMono-Regular.ttf') format('truetype'); 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | @font-face{ 11 | font-family: 'JetBrains Mono'; 12 | src: url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/web/JetBrainsMono-Bold.eot') format('embedded-opentype'), 13 | url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/webfonts/JetBrainsMono-Bold.woff2') format('woff2'), 14 | url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/ttf/JetBrainsMono-Bold.ttf') format('truetype'); 15 | font-weight: normal; 16 | font-style: normal; 17 | } -------------------------------------------------------------------------------- /example-app/desktop/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.netguru.multiplatform.charts.extensions.jvmMain 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | @Suppress("DSL_SCOPE_VIOLATION") 7 | alias(libs.plugins.kotlin.compose) 8 | alias(libs.plugins.compose.desktop) 9 | } 10 | 11 | group = libs.versions.project.group.get() 12 | version = libs.versions.project.version.get() 13 | 14 | kotlin { 15 | jvm() 16 | sourceSets { 17 | jvmMain { 18 | dependencies { 19 | implementation(compose.desktop.currentOs) 20 | implementation(project(":example-app:application")) 21 | } 22 | } 23 | } 24 | } 25 | 26 | compose.desktop { 27 | application { 28 | mainClass = "MainKt" 29 | nativeDistributions { 30 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 31 | packageName = libs.versions.desktop.packageName.get() 32 | packageVersion = libs.versions.desktop.packageVersion.get() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartConfig.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.unit.Dp 5 | import com.netguru.multiplatform.charts.grid.GridDefaults 6 | 7 | /** 8 | * The customization parameters for [BarChart] 9 | * 10 | * @param thickness Width of a single bar 11 | * @param cornerRadius 0 for square bars, thickness/2 for fully rounded corners 12 | * @param barsSpacing The space between bars in a cluster 13 | * @param maxHorizontalLinesCount Max number of lines that are allowed to draw for marking y-axis 14 | * @param roundMinMaxClosestTo Number to which min and max range will be rounded to 15 | */ 16 | @Immutable 17 | data class BarChartConfig( 18 | val thickness: Dp = BarChartDefaults.BAR_THICKNESS, 19 | val cornerRadius: Dp = BarChartDefaults.BAR_CORNER_RADIUS, 20 | val barsSpacing: Dp = BarChartDefaults.BAR_HORIZONTAL_SPACING, 21 | val maxHorizontalLinesCount: Int = GridDefaults.NUMBER_OF_GRID_LINES, 22 | val roundMinMaxClosestTo: Int = GridDefaults.ROUND_MIN_MAX_CLOSEST_TO, 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Netguru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/dial/DialDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.dial 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import androidx.compose.ui.unit.sp 10 | 11 | internal object DialDefaults { 12 | 13 | val THICKNESS = 15.dp 14 | val SCALE_PADDING = 24.dp 15 | val SCALE_STROKE_WIDTH = 2.dp 16 | val SCALE_STROKE_LENGTH = 16.dp 17 | const val START_ANGLE = -180f 18 | val JOIN_STYLE = DialJoinStyle.WithDegreeGap(2f) 19 | 20 | val MainLabel: @Composable (value: Any) -> Unit = { 21 | Text(text = it.toString(), fontSize = 24.sp) 22 | } 23 | 24 | val MinAndMaxValueLabel: @Composable (value: Any) -> Unit = { 25 | Text( 26 | text = it.toString(), 27 | style = MaterialTheme.typography.body2, 28 | modifier = Modifier 29 | .padding(top = 16.dp) 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example-app/common/src/androidMain/kotlin/com/netguru/multiplatform/charts/common/Resources.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.painter.Painter 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.compose.ui.res.painterResource 7 | import androidx.compose.ui.text.font.Font 8 | import androidx.compose.ui.text.font.FontStyle 9 | import androidx.compose.ui.text.font.FontWeight 10 | 11 | @Composable 12 | actual fun imageResources(image: String): Painter { 13 | val context = LocalContext.current 14 | val name = image.substringBefore(".") 15 | val drawable = context.resources.getIdentifier(name, "drawable", context.packageName) 16 | return painterResource(drawable) 17 | } 18 | 19 | @Composable 20 | actual fun fontResources( 21 | font: String, 22 | weight: FontWeight, 23 | style: FontStyle 24 | ): Font { 25 | val context = LocalContext.current 26 | val name = font.substringBefore(".") 27 | val fontRes = context.resources.getIdentifier(name, "font", context.packageName) 28 | return Font(fontRes, weight, style) 29 | } 30 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/dial/DialConfig.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.dial 2 | 3 | import androidx.compose.ui.unit.Dp 4 | 5 | /** 6 | * The customization parameters for [Dial] 7 | * 8 | * @param thickness The width of arc 9 | * @param scalePadding Size of space between arc and scale 10 | * @param scaleLineWidth Thickness of scale lines 11 | * @param scaleLineLength The length of majors scale lines. 12 | * The maximum value is 20% width of the chart 13 | * @param joinStyle How the lib should draw space between major arc and minor arc [DialJoinStyle] 14 | */ 15 | data class DialConfig( 16 | val thickness: Dp = DialDefaults.THICKNESS, 17 | val scalePadding: Dp = DialDefaults.SCALE_PADDING, 18 | val scaleLineWidth: Dp = DialDefaults.SCALE_STROKE_WIDTH, 19 | val scaleLineLength: Dp = DialDefaults.SCALE_STROKE_LENGTH, 20 | val joinStyle: DialJoinStyle = DialDefaults.JOIN_STYLE, 21 | val displayScale: Boolean = true, 22 | val roundCorners: Boolean = false, 23 | ) 24 | 25 | sealed class DialJoinStyle { 26 | object Joined : DialJoinStyle() 27 | object Overlapped : DialJoinStyle() 28 | data class WithDegreeGap(val degrees: Float) : DialJoinStyle() 29 | } 30 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/dial/PercentageDial.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.dial 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.netguru.multiplatform.charts.ChartAnimation 6 | import com.netguru.multiplatform.charts.theme.ChartTheme 7 | 8 | /** 9 | * Variant of [Dial] with min value set to 0 and max value set to 100. 10 | * 11 | * @param percentage Value to show 12 | * 13 | * @see Dial 14 | */ 15 | @Composable 16 | fun PercentageDial( 17 | percentage: Int, 18 | modifier: Modifier = Modifier, 19 | animation: ChartAnimation = ChartAnimation.Simple(), 20 | colors: DialColors = ChartTheme.colors.dialColors, 21 | config: DialConfig = DialConfig(), 22 | minAndMaxValueLabel: @Composable (value: Int) -> Unit = DialDefaults.MinAndMaxValueLabel, 23 | mainLabel: @Composable (value: Int) -> Unit = DialDefaults.MainLabel, 24 | ) { 25 | Dial( 26 | value = percentage, 27 | minValue = 0, 28 | maxValue = 100, 29 | modifier = modifier, 30 | animation = animation, 31 | colors = colors, 32 | config = config, 33 | minAndMaxValueLabel = minAndMaxValueLabel, 34 | mainLabel = mainLabel, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /docs/images/docs_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/pie/PieDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.pie 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.Dp 12 | import androidx.compose.ui.unit.dp 13 | import com.netguru.multiplatform.charts.round 14 | 15 | internal object PieDefaults { 16 | const val FULL_CIRCLE_DEGREES = 360f 17 | const val START_ANGLE = 270.0 18 | const val NUMBER_OF_COLS_IN_LEGEND = 4 19 | val THICKNESS = Dp.Infinity 20 | val GAP_SIZE = 0.dp 21 | val LEGEND_PADDING = 16.dp 22 | val LEGEND_ICON_SIZE = 12.dp 23 | 24 | val LegendItemLabel: @Composable (PieChartData) -> Unit = { 25 | 26 | Column( 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | ) { 29 | Spacer(modifier = Modifier.height(8.dp)) 30 | 31 | Text( 32 | text = it.value.round(1) 33 | ) 34 | 35 | Text( 36 | text = it.name, 37 | style = MaterialTheme.typography.body2, 38 | ) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example-app/android/src/main/java/com/netguru/multiplatform/charts/android/previews/HomePreviews.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.android.previews 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Favorite 5 | import androidx.compose.material.icons.filled.Person 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 8 | import androidx.compose.ui.tooling.preview.Preview 9 | import com.netguru.multiplatform.charts.application.home.HomeDrawerButton 10 | import com.netguru.multiplatform.charts.common.AppTheme 11 | import com.netguru.multiplatform.charts.common.WindowSize 12 | 13 | @Composable 14 | @Preview(widthDp = 200) 15 | fun CurrentHomeDrawerButtonPreview() { 16 | AppTheme(WindowSize.EXPANDED) { 17 | HomeDrawerButton( 18 | isCurrent = true, 19 | onClick = {}, 20 | iconPainter = rememberVectorPainter(Icons.Filled.Person), 21 | label = "User" 22 | ) 23 | } 24 | } 25 | 26 | @Composable 27 | @Preview(widthDp = 200, showBackground = true) 28 | fun IdleHomeDrawerButtonPreview() { 29 | AppTheme(WindowSize.EXPANDED) { 30 | HomeDrawerButton( 31 | isCurrent = false, 32 | onClick = {}, 33 | iconPainter = rememberVectorPainter(Icons.Filled.Favorite), 34 | label = "Favorite" 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example-app/android/src/main/java/com/netguru/multiplatform/charts/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.android 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import androidx.activity.compose.setContent 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.graphics.toComposeRect 10 | import androidx.compose.ui.platform.LocalConfiguration 11 | import androidx.compose.ui.platform.LocalDensity 12 | import androidx.window.layout.WindowMetricsCalculator 13 | import com.netguru.multiplatform.charts.application.Application 14 | import com.netguru.multiplatform.charts.common.WindowSize 15 | 16 | class MainActivity : AppCompatActivity() { 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContent { 21 | Application(rememberWindowSize()) 22 | } 23 | } 24 | } 25 | 26 | @Composable 27 | private fun Activity.rememberWindowSize(): WindowSize { 28 | val configuration = LocalConfiguration.current 29 | val windowMetrics = remember(configuration) { 30 | WindowMetricsCalculator.getOrCreate() 31 | .computeCurrentWindowMetrics(this) 32 | } 33 | val windowDpSize = with(LocalDensity.current) { 34 | windowMetrics.bounds.toComposeRect().size.toDpSize() 35 | } 36 | return WindowSize.basedOnWidth(windowDpSize.width) 37 | } 38 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/line/Data.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.line 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.soywiz.klock.DateTime 5 | import com.soywiz.klock.TimeSpan 6 | 7 | internal object Data { 8 | 9 | private val colors = listOf( 10 | Color.Blue, 11 | Color.Yellow, 12 | Color.Cyan, 13 | Color.Black, 14 | Color.DarkGray, 15 | Color.Green, 16 | Color.LightGray, 17 | Color.Magenta, 18 | Color.Red, 19 | ) 20 | 21 | fun generateLineData(nOfLines: Int) : LineChartData { 22 | return LineChartData( 23 | series = (1..nOfLines).map { 24 | LineChartSeries( 25 | dataName = "data $it", 26 | lineColor = colors[it % colors.size], 27 | listOfPoints = (1..5).map { point -> 28 | val sign = if((point+it) % 2 == 0) -1 else 1 29 | val value = (it + point) * sign 30 | LineChartPoint( 31 | x = DateTime.fromString("2021-12-31") 32 | .minus(TimeSpan(point * 24 * 60 * 60 * 1000.0)) 33 | .utc 34 | .unixMillisLong, 35 | y = value.toFloat(), 36 | ) 37 | } 38 | ) 39 | }, 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example-app/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.netguru.multiplatform.charts.extensions.baseAndroidSetup 2 | import kotlin.collections.listOf 3 | 4 | baseAndroidSetup() 5 | plugins { 6 | kotlin("android") 7 | @Suppress("DSL_SCOPE_VIOLATION") 8 | alias(libs.plugins.kotlin.compose) 9 | id("com.android.application") 10 | id("shot") 11 | } 12 | 13 | group = libs.versions.project.group.get() 14 | version = libs.versions.project.version.get() 15 | 16 | dependencies { 17 | implementation(libs.androidx.compose) 18 | debugImplementation(libs.compose.uiTooling) 19 | implementation(libs.compose.preview) 20 | implementation(libs.androidx.window) 21 | implementation(project(":example-app:application")) 22 | androidTestImplementation(libs.test.junit) 23 | androidTestImplementation(libs.test.ui.junit) 24 | debugImplementation(libs.test.ui.manifest) 25 | } 26 | 27 | android { 28 | buildTypes { 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | } 32 | } 33 | defaultConfig { 34 | applicationId = libs.versions.applicationId.get() 35 | testInstrumentationRunner = "com.karumi.shot.ShotTestRunner" 36 | } 37 | 38 | packagingOptions { 39 | resources.excludes += listOf( 40 | "META-INF/AL2.0", 41 | "META-INF/LGPL2.1" 42 | ) 43 | } 44 | namespace = "com.netguru.multiplatform.charts.android" 45 | } 46 | 47 | shot { 48 | tolerance = 2.0 // 2% tolerance, needed for testing on different devices 49 | } 50 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/line/LineWithLegendTest.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.line 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material.Text 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.test.junit4.createComposeRule 7 | import com.karumi.shot.ScreenshotTest 8 | import com.netguru.multiplatform.charts.Util.checkComposable 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | class LineWithLegendTest : ScreenshotTest { 13 | 14 | @get:Rule 15 | val composeRule = createComposeRule() 16 | 17 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 18 | // fun mixedValues_defaultUI() { 19 | // checkComposable(composeRule) { 20 | // LineChartWithLegend( 21 | // lineChartData = Data.generateLineData(3), 22 | // ) 23 | // } 24 | // } 25 | // 26 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 27 | // fun mixedValues_customUI() { 28 | // checkComposable(composeRule) { 29 | // LineChartWithLegend( 30 | // lineChartData = Data.generateLineData(3), 31 | // legendItemLabel = { 32 | // Column( 33 | // horizontalAlignment = Alignment.CenterHorizontally 34 | // ) { 35 | // Text(text = it) 36 | // Text(text = "line") 37 | // } 38 | // } 39 | // ) 40 | // } 41 | // } 42 | } 43 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartData.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.netguru.multiplatform.charts.grid.GridChartData 5 | import com.netguru.multiplatform.charts.line.LegendItemData 6 | import com.netguru.multiplatform.charts.line.SymbolShape 7 | 8 | @Immutable 9 | data class BarChartData( 10 | val categories: List, 11 | ) : GridChartData { 12 | // TODO hide those values from the user 13 | override val minX: Long = 0 14 | override val maxX: Long 15 | get() = if (categories.isEmpty()) { 16 | 1 17 | } else { 18 | categories.size * 2.toLong() 19 | } 20 | 21 | override val minY: Float 22 | get() = if (categories.isEmpty()) { 23 | 0f 24 | } else { 25 | categories.minOf { it.minY }.coerceAtMost(0f) 26 | } 27 | 28 | override val maxY: Float 29 | get() = if (categories.isEmpty()) { 30 | 1f 31 | } else { 32 | categories.maxOf { it.maxY }.coerceAtLeast(0f) 33 | } 34 | 35 | override val legendData: List 36 | get() { 37 | return categories 38 | .flatMap { it.entries } 39 | .distinctBy { it.x } 40 | .map { 41 | LegendItemData( 42 | name = it.x, 43 | symbolShape = SymbolShape.RECTANGLE, 44 | color = it.color, 45 | dashed = false, 46 | ) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/pie/PieChartConfig.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.pie 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.unit.Dp 5 | 6 | /** 7 | * The customization parameters for [PieChart] 8 | * 9 | * @param thickness Width of the arc 10 | * @param gap The margin between segments 11 | * @param legendPadding The space between chart and legend in both orientations 12 | * @param legendIcon The type of icon in legend [LegendIcon] 13 | * @param legendIconSize Size of the icon in legend 14 | * @param legendOrientation This parameters determinate the place where the legend will be drawn. 15 | * [LegendOrientation.HORIZONTAL] will draw all legend items under the chart in grid with number 16 | * of columns provided by [numberOfColsInLegend]. 17 | * [LegendOrientation.VERTICAL] will draw all legend items on the right side in column. 18 | * @param numberOfColsInLegend Determinate number of legend items per row in 19 | * [LegendOrientation.HORIZONTAL] orientation. 20 | */ 21 | @Immutable 22 | data class PieChartConfig( 23 | val thickness: Dp = PieDefaults.THICKNESS, 24 | val gap: Dp = PieDefaults.GAP_SIZE, 25 | val legendPadding: Dp = PieDefaults.LEGEND_PADDING, 26 | val legendIcon: LegendIcon = LegendIcon.CAKE, 27 | val legendIconSize: Dp = PieDefaults.LEGEND_ICON_SIZE, 28 | val legendOrientation: LegendOrientation = LegendOrientation.HORIZONTAL, 29 | val numberOfColsInLegend: Int = PieDefaults.NUMBER_OF_COLS_IN_LEGEND, 30 | ) 31 | 32 | enum class LegendIcon { 33 | SQUARE, 34 | CIRCLE, 35 | ROUND, 36 | CAKE, 37 | } 38 | 39 | enum class LegendOrientation { 40 | HORIZONTAL, 41 | VERTICAL, 42 | } 43 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/UiWrappers.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.rememberScrollState 9 | import androidx.compose.foundation.verticalScroll 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.sp 15 | import com.netguru.multiplatform.charts.common.AppTheme 16 | 17 | @Composable 18 | fun TitleText( 19 | text: String, 20 | modifier: Modifier = Modifier, 21 | ) { 22 | Text( 23 | text = text, 24 | color = AppTheme.colors.primaryText, 25 | fontSize = 40.sp, 26 | fontWeight = FontWeight.SemiBold, 27 | modifier = modifier, 28 | ) 29 | } 30 | 31 | @Composable 32 | fun SpacedColumn( 33 | content: @Composable ColumnScope.() -> Unit, 34 | ) { 35 | Column( 36 | verticalArrangement = Arrangement.spacedBy(AppTheme.dimens.grid_4), 37 | content = content, 38 | ) 39 | } 40 | 41 | @Composable 42 | fun ScrollableScreen( 43 | content: @Composable ColumnScope.() -> Unit, 44 | ) { 45 | Column( 46 | modifier = Modifier 47 | .fillMaxWidth() 48 | .verticalScroll(rememberScrollState()) 49 | .padding(AppTheme.dimens.grid_4), 50 | content = content, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/Store.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.SupervisorJob 7 | import kotlinx.coroutines.cancel 8 | import kotlinx.coroutines.flow.MutableSharedFlow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.flow.filterIsInstance 12 | import kotlinx.coroutines.flow.launchIn 13 | import kotlinx.coroutines.flow.mapLatest 14 | import kotlinx.coroutines.flow.update 15 | import kotlinx.coroutines.launch 16 | 17 | @OptIn(ExperimentalCoroutinesApi::class) 18 | internal abstract class Store( 19 | initialState: State, 20 | dispatcher: CoroutineDispatcher, 21 | ) { 22 | 23 | val state: StateFlow 24 | get() = _state 25 | 26 | private val _state = MutableStateFlow(initialState) 27 | private val actions = MutableSharedFlow() 28 | private val storeScope = CoroutineScope(dispatcher + SupervisorJob()) 29 | 30 | fun dispatch(action: Action) { 31 | storeScope.launch { actions.emit(action) } 32 | } 33 | 34 | open fun onCleared() { 35 | storeScope.cancel() 36 | } 37 | 38 | protected fun updateState(function: State.() -> State) = _state.update(function) 39 | 40 | protected inline fun onAction( 41 | crossinline function: suspend (SpecificAction) -> Unit, 42 | ) { 43 | actions 44 | .filterIsInstance() 45 | .mapLatest { function(it) } 46 | .launchIn(storeScope) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /charts/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.netguru.multiplatform.charts.extensions.baseAndroidSetup 2 | import com.netguru.multiplatform.charts.extensions.commonMain 3 | import com.netguru.multiplatform.charts.extensions.commonTest 4 | import com.netguru.multiplatform.charts.extensions.kotlin 5 | import com.netguru.multiplatform.charts.extensions.sourceSets 6 | import java.net.URL 7 | 8 | baseAndroidSetup() 9 | 10 | plugins { 11 | alias(libs.plugins.kotlin.compose) 12 | kotlin("multiplatform") 13 | id("com.android.library") 14 | alias(libs.plugins.dokka) 15 | } 16 | 17 | kotlin { 18 | androidTarget() 19 | jvm("desktop") 20 | 21 | sourceSets { 22 | commonMain { 23 | dependencies { 24 | api(libs.compose.runtime) 25 | api(libs.compose.ui) 26 | api(libs.compose.foundation) 27 | api(libs.compose.material) 28 | api(libs.compose.materialIconsExtended) 29 | } 30 | } 31 | commonTest { 32 | dependencies { 33 | implementation(libs.kotlin.test) 34 | } 35 | } 36 | } 37 | } 38 | android { 39 | namespace = "com.netguru.multiplatform.charts" 40 | } 41 | 42 | tasks.withType().configureEach { 43 | dokkaSourceSets { 44 | named("commonMain") { 45 | moduleName.set("Kotlin multiplatform charts") 46 | sourceLink { 47 | val dir = "src/commonMain/kotlin" 48 | localDirectory.set(file(dir)) 49 | remoteUrl.set( 50 | URL( 51 | "https://github.com/netguru/compose-multiplatform-charts/tree/main/charts/$dir" 52 | ) 53 | ) 54 | remoteLineSuffix.set("#L") 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartWithLegend.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import com.netguru.multiplatform.charts.ChartAnimation 7 | import com.netguru.multiplatform.charts.grid.GridDefaults 8 | import com.netguru.multiplatform.charts.line.ChartLegend 9 | import com.netguru.multiplatform.charts.theme.ChartTheme 10 | 11 | /** 12 | * This bar chart shows data organised in categories together with a legend. 13 | * 14 | * @param legendItemLabel Composable to show for each item in the legend. Square representing the 15 | * color of the item, drawn to the left of it, is not customizable. 16 | * 17 | * @see BarChart 18 | */ 19 | @Composable 20 | fun BarChartWithLegend( 21 | data: BarChartData, 22 | modifier: Modifier = Modifier, 23 | animation: ChartAnimation = ChartAnimation.Simple(), 24 | colors: BarChartColors = ChartTheme.colors.barChartColors, 25 | config: BarChartConfig = BarChartConfig(), 26 | xAxisLabel: @Composable (value: Any) -> Unit = GridDefaults.XAxisLabel, 27 | yAxisLabel: @Composable (value: Any) -> Unit = GridDefaults.YAxisLabel, 28 | legendItemLabel: @Composable (String) -> Unit = GridDefaults.LegendItemLabel, 29 | ) { 30 | Column(modifier) { 31 | BarChart( 32 | modifier = Modifier.weight(1f), 33 | data = data, 34 | animation = animation, 35 | colors = colors, 36 | config = config, 37 | xAxisLabel = xAxisLabel, 38 | yAxisLabel = yAxisLabel, 39 | ) 40 | ChartLegend( 41 | legendData = data.legendData, 42 | animation = animation, 43 | config = config, 44 | legendItemLabel = legendItemLabel, 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/GridDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.text.style.TextAlign 9 | import androidx.compose.ui.unit.dp 10 | import androidx.compose.ui.unit.sp 11 | 12 | internal object GridDefaults { 13 | 14 | val HORIZONTAL_LINES_OFFSET = 10.dp 15 | const val NUMBER_OF_GRID_LINES = 5 16 | const val ROUND_MIN_MAX_CLOSEST_TO = 10 17 | 18 | val YAxisLabel: @Composable (value: Any) -> Unit = { value -> 19 | Text( 20 | modifier = Modifier 21 | .fillMaxWidth(), 22 | fontSize = 12.sp, 23 | text = value.toString(), 24 | textAlign = TextAlign.End, 25 | maxLines = 1 26 | ) 27 | } 28 | 29 | val OverlayHeaderLabel: @Composable (value: Any) -> Unit = { value -> 30 | Text( 31 | text = value.toString(), 32 | modifier = Modifier.fillMaxWidth(), 33 | textAlign = TextAlign.Center, 34 | style = MaterialTheme.typography.overline 35 | ) 36 | } 37 | 38 | val OverlayDataEntryLabel: @Composable (dataName: String, value: Any) -> Unit = { dataName, value -> 39 | Text( 40 | text = "$dataName: $value" 41 | ) 42 | } 43 | 44 | val XAxisLabel: @Composable (value: Any) -> Unit = { value -> 45 | Text( 46 | fontSize = 12.sp, 47 | text = value.toString(), 48 | textAlign = TextAlign.Center 49 | ) 50 | } 51 | 52 | val LegendItemLabel: @Composable (String) -> Unit = { 53 | Text( 54 | text = it, 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/scripts/clipboard.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', () => { 2 | document.querySelectorAll('span.copy-icon').forEach(element => { 3 | element.addEventListener('click', (el) => copyElementsContentToClipboard(element)); 4 | }) 5 | 6 | document.querySelectorAll('span.anchor-icon').forEach(element => { 7 | element.addEventListener('click', (el) => { 8 | if(element.hasAttribute('pointing-to')){ 9 | const location = hrefWithoutCurrentlyUsedAnchor() + '#' + element.getAttribute('pointing-to') 10 | copyTextToClipboard(element, location) 11 | } 12 | }); 13 | }) 14 | }) 15 | 16 | const copyElementsContentToClipboard = (element) => { 17 | const selection = window.getSelection(); 18 | const range = document.createRange(); 19 | range.selectNodeContents(element.parentNode.parentNode); 20 | selection.removeAllRanges(); 21 | selection.addRange(range); 22 | 23 | copyAndShowPopup(element, () => selection.removeAllRanges()) 24 | } 25 | 26 | const copyTextToClipboard = (element, text) => { 27 | var textarea = document.createElement("textarea"); 28 | textarea.textContent = text; 29 | textarea.style.position = "fixed"; 30 | document.body.appendChild(textarea); 31 | textarea.select(); 32 | 33 | copyAndShowPopup(element, () => document.body.removeChild(textarea)) 34 | } 35 | 36 | const copyAndShowPopup = (element, after) => { 37 | try { 38 | document.execCommand('copy'); 39 | element.nextElementSibling.classList.add('active-popup'); 40 | setTimeout(() => { 41 | element.nextElementSibling.classList.remove('active-popup'); 42 | }, 1200); 43 | } catch (e) { 44 | console.error('Failed to write to clipboard:', e) 45 | } 46 | finally { 47 | if(after) after() 48 | } 49 | } 50 | 51 | const hrefWithoutCurrentlyUsedAnchor = () => window.location.href.split('#')[0] 52 | 53 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/bar/Data.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import kotlin.math.roundToInt 5 | 6 | internal object Data { 7 | internal enum class ValueTypes { 8 | POSITIVE, 9 | NEGATIVE, 10 | POSITIVE_AND_NEGATIVE, 11 | ZERO, 12 | } 13 | 14 | private val colors = listOf( 15 | Color.Blue, 16 | Color.Cyan, 17 | Color.Black, 18 | Color.DarkGray, 19 | Color.Green, 20 | Color.LightGray, 21 | Color.Magenta, 22 | Color.Red, 23 | Color.Yellow, 24 | ) 25 | 26 | private fun generateEntries(n: Int, valueTypes: ValueTypes): List { 27 | val nonZeroValues = when (valueTypes) { 28 | ValueTypes.POSITIVE -> 1..n 29 | ValueTypes.NEGATIVE -> -n..-1 30 | ValueTypes.POSITIVE_AND_NEGATIVE -> ((-n / 2)..-1) + (1..(n/2.0).roundToInt()) 31 | ValueTypes.ZERO -> List(n) { 0 } 32 | } 33 | return nonZeroValues.map { 34 | BarChartEntry( 35 | x = "entry_$it", 36 | y = it * 10f, 37 | color = colors.elementAt(it.mod(colors.size)) 38 | ) 39 | } 40 | } 41 | 42 | private fun generateCategories( 43 | nOfCategories: Int, 44 | nOfEntries: Int, 45 | valueTypes: ValueTypes 46 | ): List { 47 | return (1..nOfCategories).map { 48 | BarChartCategory( 49 | name = "category_$it", 50 | entries = generateEntries(nOfEntries, valueTypes), 51 | ) 52 | } 53 | } 54 | 55 | fun generateData(nOfCategories: Int, nOfEntries: Int, valueTypes: ValueTypes): BarChartData { 56 | return BarChartData( 57 | categories = generateCategories(nOfCategories, nOfEntries, valueTypes) 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/pie/PieTest.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.pie 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.compose.ui.unit.dp 6 | import com.karumi.shot.ScreenshotTest 7 | import com.netguru.multiplatform.charts.Util.checkComposable 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class PieTest : ScreenshotTest { 12 | 13 | @get:Rule 14 | val composeRule = createComposeRule() 15 | 16 | @Test 17 | fun twoCategories() { 18 | checkComposable(composeRule) { 19 | PieChart( 20 | data = listOf( 21 | PieChartData("first", 22.0, Color.Blue), 22 | PieChartData("second", 22.0, Color.Yellow) 23 | ), 24 | config = PieChartConfig( 25 | thickness = 80.dp 26 | ) 27 | ) 28 | } 29 | } 30 | 31 | @Test 32 | fun threeCategories() { 33 | checkComposable(composeRule) { 34 | PieChart( 35 | data = listOf( 36 | PieChartData("first", 22.0, Color.Blue), 37 | PieChartData("second", 22.0, Color.Yellow), 38 | PieChartData("third", 22.0, Color.Green) 39 | ), 40 | config = PieChartConfig( 41 | thickness = 80.dp 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | @Test 48 | fun twoCategories_withZeroValue() { 49 | checkComposable(composeRule) { 50 | PieChart( 51 | data = listOf( 52 | PieChartData("first", 22.0, Color.Blue), 53 | PieChartData("second", 0.0, Color.Yellow) 54 | ), 55 | config = PieChartConfig( 56 | thickness = 80.dp 57 | ) 58 | ) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/screen/BubbleChartScreen.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application.screen 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Album 6 | import androidx.compose.material.icons.filled.Bed 7 | import androidx.compose.material.icons.filled.House 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import com.netguru.multiplatform.charts.ChartAnimation 12 | import com.netguru.multiplatform.charts.application.ScrollableScreen 13 | import com.netguru.multiplatform.charts.application.SpacedColumn 14 | import com.netguru.multiplatform.charts.application.TitleText 15 | import com.netguru.multiplatform.charts.bubble.Bubble 16 | import com.netguru.multiplatform.charts.bubble.BubbleChart 17 | import com.netguru.multiplatform.charts.common.AppTheme 18 | 19 | @Composable 20 | fun BubbleChartScreen() { 21 | val bubbles = listOf( 22 | Bubble( 23 | name = "first", 24 | value = 1.2f, 25 | icon = Icons.Default.Album, 26 | color = AppTheme.colors.yellow 27 | ), 28 | Bubble( 29 | name = "second", 30 | value = 4.6f, 31 | icon = Icons.Default.House, 32 | color = AppTheme.colors.green 33 | ), 34 | Bubble( 35 | name = "third", 36 | value = 6.9f, 37 | icon = Icons.Default.Bed, 38 | color = AppTheme.colors.blue 39 | ), 40 | ) 41 | 42 | ScrollableScreen { 43 | SpacedColumn { 44 | TitleText(text = "Bubble chart") 45 | BubbleChart( 46 | bubbles = bubbles, 47 | modifier = Modifier 48 | .size(300.dp), 49 | animation = ChartAnimation.Sequenced(), 50 | ) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/Helpers.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | import kotlin.math.pow 10 | import kotlin.math.roundToInt 11 | 12 | internal fun Double.mapValueToDifferentRange( 13 | inMin: Double, 14 | inMax: Double, 15 | outMin: Double, 16 | outMax: Double, 17 | ) = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin 18 | 19 | fun Float.mapValueToDifferentRange( 20 | inMin: Float, 21 | inMax: Float, 22 | outMin: Float, 23 | outMax: Float, 24 | ) = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin 25 | 26 | fun Long.mapValueToDifferentRange( 27 | inMin: Long, 28 | inMax: Long, 29 | outMin: Long, 30 | outMax: Long, 31 | ) = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin 32 | 33 | fun Long.mapValueToDifferentRange( 34 | inMin: Long, 35 | inMax: Long, 36 | outMin: Float, 37 | outMax: Float, 38 | ) = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin 39 | 40 | fun Number.round(decimals: Int = 2): String { 41 | return when (this) { 42 | is Double, 43 | is Float, 44 | -> try { 45 | ((this.toDouble() * 10.0.pow(decimals)).roundToInt() / 10.0.pow(decimals)).toString() 46 | } catch (e: IllegalArgumentException) { 47 | "-" 48 | } 49 | else -> { 50 | this.toString() 51 | } 52 | } 53 | } 54 | 55 | @Composable 56 | internal fun StartAnimation(animation: ChartAnimation, data: Any): Boolean { 57 | var animationPlayed by remember(data) { 58 | mutableStateOf(animation is ChartAnimation.Disabled) 59 | } 60 | LaunchedEffect(Unit) { 61 | animationPlayed = true 62 | } 63 | 64 | return animationPlayed 65 | } 66 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/ChartAxis.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.IntrinsicSize 5 | import androidx.compose.foundation.layout.fillMaxHeight 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.layout.layout 11 | import androidx.compose.ui.unit.dp 12 | import kotlin.math.roundToInt 13 | 14 | @Composable 15 | internal fun YAxisLabels( 16 | horizontalGridLines: List, 17 | yAxisMarkerLayout: @Composable (value: Number) -> Unit, 18 | ) { 19 | Box( 20 | modifier = Modifier 21 | .width(IntrinsicSize.Max) 22 | .fillMaxHeight() 23 | .padding(end = 8.dp) 24 | ) { 25 | horizontalGridLines.forEach { horizontalLine -> 26 | Box( 27 | modifier = Modifier 28 | .alignCenterToOffsetVertical(horizontalLine.position) 29 | ) { 30 | yAxisMarkerLayout(horizontalLine.value) 31 | } 32 | } 33 | } 34 | } 35 | 36 | private fun Modifier.alignCenterToOffsetVertical( 37 | offsetToAlignWith: Float, 38 | ) = layout { measurable, constraints -> 39 | val placeable = measurable.measure(constraints) 40 | val placeableY = (offsetToAlignWith - (placeable.height / 2f)).coerceAtLeast(0f) 41 | 42 | layout(placeable.width, placeable.height) { 43 | placeable.placeRelative(0, placeableY.roundToInt()) 44 | } 45 | } 46 | 47 | internal fun Modifier.alignCenterToOffsetHorizontal( 48 | offsetToAlignWith: Float, 49 | ) = layout { measurable, constraints -> 50 | val placeable = measurable.measure(constraints) 51 | val placeableX = offsetToAlignWith - (placeable.width / 2f) 52 | 53 | layout(placeable.width, placeable.height) { 54 | placeable.placeRelative(placeableX.roundToInt(), 0) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/OverlayInformation.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.BoxWithConstraints 5 | import androidx.compose.foundation.layout.offset 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.geometry.Size 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.platform.LocalDensity 16 | import androidx.compose.ui.unit.Dp 17 | import androidx.compose.ui.unit.dp 18 | 19 | private val TOUCH_OFFSET = 20.dp 20 | private val OVERLAY_WIDTH = 200.dp 21 | 22 | @Composable 23 | internal fun OverlayInformation( 24 | positionX: Float, 25 | containerSize: Size, 26 | surfaceColor: Color, 27 | touchOffset: Dp = TOUCH_OFFSET, 28 | overlayWidth: Dp = OVERLAY_WIDTH, 29 | content: @Composable () -> Unit, 30 | ) { 31 | if (positionX < 0) return 32 | BoxWithConstraints( 33 | modifier = Modifier 34 | .offset( 35 | x = with(LocalDensity.current) { 36 | positionX.toDp() + 37 | // change offset based on cursor position to avoid out of screen drawing on the right 38 | if (positionX.toDp() > (containerSize.width / 2).toDp()) { 39 | -OVERLAY_WIDTH - TOUCH_OFFSET 40 | } else { 41 | TOUCH_OFFSET 42 | } 43 | }, 44 | y = touchOffset 45 | ) 46 | .width(OVERLAY_WIDTH) 47 | .alpha(0.9f) 48 | .clip(RoundedCornerShape(10.dp)) 49 | .background(surfaceColor) 50 | .padding(8.dp) 51 | ) { 52 | content() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/axisscale/YAxisScale.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid.axisscale 2 | 3 | import kotlin.math.floor 4 | import kotlin.math.log10 5 | import kotlin.math.pow 6 | 7 | class YAxisScale( 8 | min: Float, 9 | max: Float, 10 | maxTickCount: Int, 11 | roundClosestTo: Int, 12 | ) { 13 | val tick: Float 14 | val min: Float 15 | val max: Float 16 | 17 | init { 18 | this.min = if (!min.isNaN()) { 19 | min.getClosest(roundClosestTo) 20 | } else { 21 | 0f 22 | } 23 | this.max = if (!max.isNaN()) { 24 | max.getClosest(roundClosestTo) 25 | } else { 26 | 0f 27 | } 28 | 29 | val range = niceNum(this.max - this.min, false) 30 | this.tick = niceNum(range / (maxTickCount), true) 31 | } 32 | 33 | private fun Float.getClosest(n: Int) = when { 34 | this > 0f -> (((this.toInt() + n - 1) / n) * n).toFloat() 35 | this < 0f -> (((this.toInt() - n + 1) / n) * n).toFloat() 36 | else -> 0f 37 | } 38 | 39 | /** 40 | * Returns a "nice" number approximately equal to range. 41 | * Rounds the number if round = true Takes the ceiling if round = false. 42 | * 43 | * @param range the data range 44 | * @param round whether to round the result 45 | * @return a "nice" number to be used for the data range 46 | */ 47 | private fun niceNum(range: Float, round: Boolean): Float { 48 | /** nice, rounded fraction */ 49 | val exponent: Float = floor(log10(range)) 50 | /** exponent of range */ 51 | val fraction = range / 10.0f.pow(exponent) 52 | /** fractional part of range */ 53 | val niceFraction: Float = if (round) { 54 | if (fraction < 1.5) 1.0f else if (fraction < 3) 2.0f else if (fraction < 7) 5.0f else 10.0f 55 | } else { 56 | if (fraction <= 1) 1.0f else if (fraction <= 2) 2.0f else if (fraction <= 5) 5.0f else 10.0f 57 | } 58 | return niceFraction * 10.0f.pow(exponent) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bubble/Vector.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bubble 2 | 3 | import kotlin.math.sqrt 4 | 5 | data class Vector(var x: Float, var y: Float) { 6 | fun mag(): Float { 7 | return sqrt((magSq().toDouble()).toFloat()) 8 | } 9 | 10 | private fun magSq(): Float { 11 | return x * x + y * y 12 | } 13 | 14 | fun add(v: Vector): Vector { 15 | x += v.x 16 | y += v.y 17 | return this 18 | } 19 | 20 | fun sub(v: Vector): Vector { 21 | x -= v.x 22 | y -= v.y 23 | return this 24 | } 25 | 26 | operator fun set(x: Float, y: Float): Vector { 27 | this.x = x 28 | this.y = y 29 | return this 30 | } 31 | 32 | fun mult(n: Float): Vector { 33 | x *= n 34 | y *= n 35 | return this 36 | } 37 | 38 | operator fun div(n: Float): Vector { 39 | x /= n 40 | y /= n 41 | return this 42 | } 43 | 44 | fun normalize(): Vector { 45 | val m = mag() 46 | if (m != 0f && m != 1f) { 47 | div(m) 48 | } 49 | return this 50 | } 51 | 52 | fun limit(max: Float): Vector { 53 | if (magSq() > max * max) { 54 | normalize() 55 | mult(max) 56 | } 57 | return this 58 | } 59 | 60 | fun setMag(len: Float): Vector { 61 | normalize() 62 | mult(len) 63 | return this 64 | } 65 | 66 | override fun equals(other: Any?): Boolean { 67 | if (other !is Vector) { 68 | return false 69 | } 70 | return x == other.x && y == other.y 71 | } 72 | 73 | override fun hashCode(): Int { 74 | var result = x.hashCode() 75 | result = 31 * result + y.hashCode() 76 | return result 77 | } 78 | 79 | companion object { 80 | fun sub(v1: Vector, v2: Vector): Vector = Vector(v1.x - v2.x, v1.y - v2.y) 81 | 82 | fun dist(v1: Vector, v2: Vector): Float { 83 | val dx = v1.x - v2.x 84 | val dy = v1.y - v2.y 85 | return sqrt((dx * dx + dy * dy).toDouble()).toFloat() 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/gasbottle/GasBottleTest.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.gasbottle 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import com.karumi.shot.ScreenshotTest 6 | import com.netguru.multiplatform.charts.Util.checkComposable 7 | import com.netguru.multiplatform.charts.theme.ChartDefaults 8 | import org.junit.Rule 9 | import org.junit.Test 10 | 11 | class GasBottleTest : ScreenshotTest { 12 | 13 | @get:Rule 14 | val composeRule = createComposeRule() 15 | 16 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 17 | // fun value0_defaultUI() { 18 | // checkComposable(composeRule) { 19 | // GasBottle(percentage = 0f) 20 | // } 21 | // } 22 | // 23 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 24 | // fun value50_defaultUI() { 25 | // checkComposable(composeRule) { 26 | // GasBottle(percentage = 50f) 27 | // } 28 | // } 29 | // 30 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 31 | // fun value100_defaultUI() { 32 | // checkComposable(composeRule) { 33 | // GasBottle(percentage = 100f) 34 | // } 35 | // } 36 | // 37 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 38 | // fun value25_customUI() { 39 | // checkComposable(composeRule) { 40 | // GasBottle( 41 | // percentage = 25f, 42 | // chartColors = ChartDefaults.chartColors( 43 | // emptyGasBottle = Color.Yellow, 44 | // fullGasBottle = Color.Blue, 45 | // ) 46 | // ) 47 | // } 48 | // } 49 | // 50 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 51 | // fun value100_customUI() { 52 | // checkComposable(composeRule) { 53 | // GasBottle( 54 | // percentage = 100f, 55 | // chartColors = ChartDefaults.chartColors( 56 | // emptyGasBottle = Color.Yellow, 57 | // fullGasBottle = Color.Blue, 58 | // ) 59 | // ) 60 | // } 61 | // } 62 | } 63 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/line/LineChartWithLegend.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.line 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import com.netguru.multiplatform.charts.ChartAnimation 7 | import com.netguru.multiplatform.charts.grid.GridDefaults 8 | import com.netguru.multiplatform.charts.theme.ChartTheme 9 | 10 | /** 11 | * Classic line chart with legend below the chart. 12 | * 13 | * For more information, check [LineChart]. 14 | * 15 | * @param legendItemLabel Composable to use to represent the value in the legend. Only text value 16 | * is customizable. 17 | * 18 | * @see LineChart 19 | */ 20 | @Composable 21 | fun LineChartWithLegend( 22 | lineChartData: LineChartData, 23 | modifier: Modifier = Modifier, 24 | maxVerticalLines: Int = GridDefaults.NUMBER_OF_GRID_LINES, 25 | maxHorizontalLines: Int = GridDefaults.NUMBER_OF_GRID_LINES, 26 | animation: ChartAnimation = ChartAnimation.Simple(), 27 | colors: LineChartColors = ChartTheme.colors.lineChartColors, 28 | xAxisLabel: @Composable (value: Any) -> Unit = GridDefaults.XAxisLabel, 29 | yAxisLabel: @Composable (value: Any) -> Unit = GridDefaults.YAxisLabel, 30 | overlayHeaderLabel: @Composable (value: Any) -> Unit = GridDefaults.OverlayHeaderLabel, 31 | overlayDataEntryLabel: @Composable (dataName: String, value: Any) -> Unit = GridDefaults.OverlayDataEntryLabel, 32 | legendItemLabel: @Composable (String) -> Unit = GridDefaults.LegendItemLabel, 33 | ) { 34 | Column(modifier = modifier) { 35 | LineChart( 36 | lineChartData = lineChartData, 37 | modifier = Modifier.weight(1f), 38 | maxVerticalLines = maxVerticalLines, 39 | maxHorizontalLines = maxHorizontalLines, 40 | animation = animation, 41 | colors = colors, 42 | xAxisLabel = xAxisLabel, 43 | yAxisLabel = yAxisLabel, 44 | overlayHeaderLabel = overlayHeaderLabel, 45 | overlayDataEntryLabel = overlayDataEntryLabel, 46 | ) 47 | ChartLegend( 48 | legendData = lineChartData.legendData, 49 | animation = animation, 50 | legendItemLabel = legendItemLabel, 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bubble/BubbleDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bubble 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.material.Icon 10 | import androidx.compose.material.MaterialTheme 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.text.style.TextOverflow 19 | import androidx.compose.ui.unit.dp 20 | import com.netguru.multiplatform.charts.round 21 | 22 | internal object BubbleDefaults { 23 | 24 | const val MINIMUM_BUBBLE_RADIUS = 40f 25 | 26 | val BubbleLabel: @Composable (Bubble) -> Unit = { bubble -> 27 | Column( 28 | horizontalAlignment = Alignment.CenterHorizontally, 29 | modifier = Modifier.width(bubble.radius.dp * 1.6f) 30 | ) { 31 | Icon( 32 | painter = rememberVectorPainter(bubble.icon), 33 | contentDescription = null, 34 | tint = Color.White, 35 | modifier = Modifier.size(20.dp), 36 | ) 37 | 38 | Spacer(Modifier.height(4.dp)) 39 | 40 | Text( 41 | modifier = Modifier.fillMaxWidth(), 42 | text = bubble.value.round(1), 43 | textAlign = TextAlign.Center, 44 | color = Color.White, 45 | style = MaterialTheme.typography.h5 46 | ) 47 | Text( 48 | modifier = Modifier.fillMaxWidth(), 49 | text = bubble.name, 50 | color = Color.White.copy(alpha = 0.6f), 51 | maxLines = 1, 52 | overflow = TextOverflow.Ellipsis, 53 | textAlign = TextAlign.Center, 54 | style = MaterialTheme.typography.body2 55 | ) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example-app/common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.netguru.multiplatform.charts.extensions.androidMain 2 | import com.netguru.multiplatform.charts.extensions.baseAndroidSetup 3 | import com.netguru.multiplatform.charts.extensions.baseTestSetup 4 | import com.netguru.multiplatform.charts.extensions.sourceSets 5 | import org.gradle.kotlin.dsl.kotlin 6 | 7 | baseAndroidSetup() 8 | baseTestSetup() 9 | 10 | plugins { 11 | kotlin("multiplatform") 12 | @Suppress("DSL_SCOPE_VIOLATION") 13 | alias(libs.plugins.kotlin.compose) 14 | id("com.android.library") 15 | } 16 | 17 | kotlin { 18 | androidTarget() 19 | jvm("desktop") 20 | 21 | sourceSets { 22 | commonMain { 23 | kotlin.srcDir("${buildDir.absolutePath}/generated/resources") 24 | dependencies { 25 | api(libs.compose.runtime) 26 | api(libs.compose.ui) 27 | api(libs.compose.foundation) 28 | api(libs.compose.material) 29 | api(libs.compose.materialIconsExtended) 30 | api(libs.time.klock) 31 | api(project(":charts")) 32 | } 33 | } 34 | androidMain { 35 | dependencies { 36 | api(libs.androidx.appcompat) 37 | api(libs.androidx.coreKtx) 38 | } 39 | } 40 | } 41 | } 42 | 43 | android { 44 | sourceSets["main"].apply { 45 | res.srcDirs("src/androidMain/res", "src/commonMain/resources") 46 | } 47 | namespace = "com.netguru.multiplatform.charts.common" 48 | } 49 | 50 | project.rootProject.tasks.apply { 51 | register("resourceGeneratorTask", com.netguru.multiplatform.charts.resources.ResourceGeneratorTask::class) { 52 | group = "resource Generator" 53 | description = "Task for generating resources" 54 | } 55 | matching { 56 | it.name.contains("prepareKotlinBuildScriptModel") 57 | }.configureEach { 58 | dependsOn("resourceGeneratorTask") 59 | } 60 | } 61 | 62 | project.tasks.apply { 63 | register("resourceGeneratorTask", com.netguru.multiplatform.charts.resources.ResourceGeneratorTask::class) { 64 | group = "resource Generator" 65 | description = "Task for generating resources" 66 | } 67 | withType { 68 | dependsOn("resourceGeneratorTask") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /docs/scripts/navigation-loader.js: -------------------------------------------------------------------------------- 1 | navigationPageText = fetch(pathToRoot + "navigation.html").then(response => response.text()) 2 | 3 | displayNavigationFromPage = () => { 4 | navigationPageText.then(data => { 5 | document.getElementById("sideMenu").innerHTML = data; 6 | }).then(() => { 7 | document.querySelectorAll(".overview > a").forEach(link => { 8 | link.setAttribute("href", pathToRoot + link.getAttribute("href")); 9 | }) 10 | }).then(() => { 11 | document.querySelectorAll(".sideMenuPart").forEach(nav => { 12 | if (!nav.classList.contains("hidden")) 13 | nav.classList.add("hidden") 14 | }) 15 | }).then(() => { 16 | revealNavigationForCurrentPage() 17 | }) 18 | document.querySelectorAll('.footer a[href^="#"]').forEach(anchor => { 19 | anchor.addEventListener('click', function (e) { 20 | e.preventDefault(); 21 | document.querySelector(this.getAttribute('href')).scrollIntoView({ 22 | behavior: 'smooth' 23 | }); 24 | }); 25 | }); 26 | } 27 | 28 | revealNavigationForCurrentPage = () => { 29 | let pageId = document.getElementById("content").attributes["pageIds"].value.toString(); 30 | let parts = document.querySelectorAll(".sideMenuPart"); 31 | let found = 0; 32 | do { 33 | parts.forEach(part => { 34 | if (part.attributes['pageId'].value.indexOf(pageId) !== -1 && found === 0) { 35 | found = 1; 36 | if (part.classList.contains("hidden")) { 37 | part.classList.remove("hidden"); 38 | part.setAttribute('data-active', ""); 39 | } 40 | revealParents(part) 41 | } 42 | }); 43 | pageId = pageId.substring(0, pageId.lastIndexOf("/")) 44 | } while (pageId.indexOf("/") !== -1 && found === 0) 45 | }; 46 | revealParents = (part) => { 47 | if (part.classList.contains("sideMenuPart")) { 48 | if (part.classList.contains("hidden")) 49 | part.classList.remove("hidden"); 50 | revealParents(part.parentNode) 51 | } 52 | }; 53 | 54 | /* 55 | This is a work-around for safari being IE of our times. 56 | It doesn't fire a DOMContentLoaded, presumabely because eventListener is added after it wants to do it 57 | */ 58 | if (document.readyState == 'loading') { 59 | window.addEventListener('DOMContentLoaded', () => { 60 | displayNavigationFromPage() 61 | }) 62 | } else { 63 | displayNavigationFromPage() 64 | } -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/pie/PieWithLegendTest.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.pie 2 | 3 | import androidx.compose.material.Text 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.unit.dp 7 | import com.karumi.shot.ScreenshotTest 8 | import com.netguru.multiplatform.charts.Util.checkComposable 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | class PieWithLegendTest : ScreenshotTest { 13 | 14 | @get:Rule 15 | val composeRule = createComposeRule() 16 | 17 | @Test 18 | fun twoCategories() { 19 | checkComposable(composeRule) { 20 | PieChartWithLegend( 21 | config = PieChartConfig(thickness = 80.dp, legendIcon = LegendIcon.ROUND), 22 | pieChartData = listOf( 23 | PieChartData("first", 22.0, Color.Blue), 24 | PieChartData("second", 22.0, Color.Yellow) 25 | ) 26 | ) 27 | } 28 | } 29 | 30 | @Test 31 | fun threeCategories() { 32 | checkComposable(composeRule) { 33 | PieChartWithLegend( 34 | config = PieChartConfig(thickness = 80.dp, legendIcon = LegendIcon.ROUND), 35 | pieChartData = listOf( 36 | PieChartData("first", 22.0, Color.Blue), 37 | PieChartData("second", 22.0, Color.Yellow), 38 | PieChartData("third", 22.0, Color.Green), 39 | ) 40 | ) 41 | } 42 | } 43 | 44 | @Test 45 | fun twoCategories_withZeroValue() { 46 | checkComposable(composeRule) { 47 | PieChartWithLegend( 48 | config = PieChartConfig(thickness = 80.dp, legendIcon = LegendIcon.ROUND), 49 | pieChartData = listOf( 50 | PieChartData("first", 22.0, Color.Blue), 51 | PieChartData("second", 0.0, Color.Yellow), 52 | ) 53 | ) 54 | } 55 | } 56 | 57 | @Test 58 | fun threeCategories_customUI() { 59 | checkComposable(composeRule) { 60 | PieChartWithLegend( 61 | pieChartData = listOf( 62 | PieChartData("first", 22.0, Color.Blue), 63 | PieChartData("second", 22.0, Color.Yellow), 64 | PieChartData("third", 22.0, Color.Green), 65 | ), 66 | config = PieChartConfig( 67 | thickness = 80.dp, 68 | numberOfColsInLegend = 2, 69 | legendIcon = LegendIcon.ROUND 70 | ), 71 | legendItemLabel = { 72 | Text(text = "${it.name} (${it.value})") 73 | } 74 | ) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/pie/PieChartWithLegend.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.pie 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxHeight 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.wrapContentWidth 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import com.netguru.multiplatform.charts.ChartAnimation 13 | 14 | /** 15 | * Version of [PieChart] with legend. 16 | * 17 | * @param legendItemLabel Composable to use to represent the item in the legend 18 | * 19 | * @see PieChart 20 | */ 21 | @Composable 22 | fun PieChartWithLegend( 23 | pieChartData: List, 24 | modifier: Modifier = Modifier, 25 | animation: ChartAnimation = ChartAnimation.Simple(), 26 | config: PieChartConfig = PieChartConfig(), 27 | legendItemLabel: @Composable (PieChartData) -> Unit = PieDefaults.LegendItemLabel, 28 | ) { 29 | when (config.legendOrientation) { 30 | LegendOrientation.HORIZONTAL -> 31 | Column( 32 | horizontalAlignment = Alignment.CenterHorizontally, 33 | verticalArrangement = Arrangement.spacedBy(config.legendPadding), 34 | modifier = modifier, 35 | ) { 36 | PieChart( 37 | data = pieChartData, 38 | modifier = Modifier.weight(1f), 39 | animation = animation, 40 | config = config, 41 | ) 42 | PieChartLegend( 43 | data = pieChartData, 44 | animation = animation, 45 | legendItemLabel = legendItemLabel, 46 | config = config, 47 | modifier = Modifier.fillMaxWidth(), 48 | ) 49 | } 50 | LegendOrientation.VERTICAL -> 51 | Row( 52 | verticalAlignment = Alignment.CenterVertically, 53 | horizontalArrangement = Arrangement.spacedBy(config.legendPadding), 54 | modifier = modifier, 55 | ) { 56 | PieChart( 57 | data = pieChartData, 58 | modifier = Modifier.fillMaxHeight(), 59 | animation = animation, 60 | config = config, 61 | ) 62 | PieChartLegend( 63 | data = pieChartData, 64 | animation = animation, 65 | legendItemLabel = legendItemLabel, 66 | config = config, 67 | modifier = Modifier.wrapContentWidth(), 68 | ) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/line/LineChartSeries.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.line 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.unit.Dp 6 | import androidx.compose.ui.unit.dp 7 | import com.netguru.multiplatform.charts.grid.GridChartData 8 | 9 | @Immutable 10 | data class LineChartPoint( 11 | val x: Long, 12 | val y: Float, 13 | ) 14 | 15 | @Immutable 16 | data class LineChartSeries( 17 | val dataName: String, 18 | val lineWidth: Dp = 3.dp, 19 | val lineColor: Color, 20 | val fillColor: Color = lineColor, 21 | val dashedLine: Boolean = false, 22 | val listOfPoints: List = emptyList(), 23 | ) { 24 | val minValue: Float 25 | val maxValue: Float 26 | val minTimestamp: Long 27 | val maxTimeStamp: Long 28 | 29 | init { 30 | // find max and min in series 31 | if (listOfPoints.isNotEmpty()) { 32 | val minMaxValue = getMinMaxValue() 33 | minValue = minMaxValue.first 34 | maxValue = minMaxValue.second 35 | 36 | val minMaxTimestamp = getMinMaxTimestamp() 37 | minTimestamp = minMaxTimestamp.first 38 | maxTimeStamp = minMaxTimestamp.second 39 | } else { 40 | minValue = 0f 41 | maxValue = 0f 42 | minTimestamp = 0L 43 | maxTimeStamp = 0L 44 | } 45 | } 46 | 47 | private fun getMinMaxTimestamp(): Pair { 48 | val sortedTimestamp = listOfPoints.sortedBy { it.x } 49 | return Pair(sortedTimestamp.first().x, sortedTimestamp.last().x) 50 | } 51 | 52 | private fun getMinMaxValue(): Pair { 53 | val sortedValue = listOfPoints.sortedBy { it.y } 54 | return Pair(sortedValue.first().y, sortedValue.last().y) 55 | } 56 | } 57 | 58 | @Immutable 59 | data class LineChartData( 60 | val series: List, 61 | ) : GridChartData { 62 | override val legendData: List 63 | get() = series.map { 64 | LegendItemData( 65 | name = it.dataName, 66 | symbolShape = SymbolShape.LINE, 67 | color = it.lineColor, 68 | dashed = it.dashedLine 69 | ) 70 | } 71 | 72 | override val minX: Long 73 | override val maxX: Long 74 | override val minY: Float 75 | override val maxY: Float 76 | 77 | init { 78 | // find max and min in all data 79 | val timeStamps = mutableListOf() 80 | val values = mutableListOf() 81 | series.forEach { 82 | timeStamps.add(it.minTimestamp) 83 | timeStamps.add(it.maxTimeStamp) 84 | values.add(it.minValue) 85 | values.add(it.maxValue) 86 | } 87 | minX = timeStamps.minOrNull() ?: 0 88 | maxX = timeStamps.maxOrNull() ?: 0 89 | minY = values.minOrNull() ?: 0f 90 | maxY = values.maxOrNull() ?: 0f 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/dial/PercentageDialTest.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.dial 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.Text 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.test.junit4.createComposeRule 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.unit.sp 12 | import com.karumi.shot.ScreenshotTest 13 | import com.netguru.multiplatform.charts.theme.ChartDefaults 14 | import com.netguru.multiplatform.charts.Util.checkComposable 15 | import org.junit.Rule 16 | import org.junit.Test 17 | 18 | class PercentageDialTest : ScreenshotTest { 19 | 20 | @get:Rule 21 | val composeRule = createComposeRule() 22 | 23 | @Test 24 | fun value_50_UI_default() { 25 | checkComposable(composeRule) { PercentageDial(percentage = 50) } 26 | } 27 | 28 | @Test 29 | fun value_0_UI_default() { 30 | checkComposable(composeRule) { PercentageDial(percentage = 0) } 31 | } 32 | 33 | @Test 34 | fun value_100_UI_default() { 35 | checkComposable(composeRule) { PercentageDial(percentage = 100) } 36 | } 37 | 38 | @Test 39 | fun value_minus100_UI_default() { 40 | checkComposable(composeRule) { PercentageDial(percentage = -100) } 41 | } 42 | 43 | @Test 44 | fun value_150_UI_default() { 45 | checkComposable(composeRule) { PercentageDial(percentage = 150) } 46 | } 47 | 48 | @Test 49 | fun value_69_UI_custom_colors_and_labels() { 50 | checkComposable(composeRule) { 51 | PercentageDial( 52 | percentage = 69, 53 | colors = ChartDefaults.chartColors( 54 | primary = Color.Blue, 55 | grid = Color.Magenta, 56 | ).dialColors, 57 | minAndMaxValueLabel = { 58 | Text( 59 | text = it.toString(), 60 | color = Color.Blue, 61 | fontSize = 32.sp, 62 | modifier = Modifier.padding(top = 15.dp) 63 | ) 64 | }, 65 | mainLabel = { 66 | Column( 67 | horizontalAlignment = Alignment.CenterHorizontally 68 | ) { 69 | Text(text = it.toString(), color = Color.Blue, fontSize = 40.sp) 70 | Text(text = "tests", color = Color.Magenta, fontSize = 24.sp) 71 | } 72 | } 73 | ) 74 | } 75 | } 76 | 77 | @Test 78 | fun value_69_UI_no_labels() { 79 | checkComposable(composeRule) { 80 | PercentageDial( 81 | percentage = 69, 82 | minAndMaxValueLabel = { }, 83 | mainLabel = { } 84 | ) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/bar/BarChartDrawing.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.ui.geometry.CornerRadius 4 | import androidx.compose.ui.geometry.Offset 5 | import androidx.compose.ui.geometry.Size 6 | import androidx.compose.ui.graphics.drawscope.DrawScope 7 | import com.netguru.multiplatform.charts.mapValueToDifferentRange 8 | 9 | internal fun DrawScope.drawBarChart( 10 | data: BarChartData, 11 | config: BarChartConfig, 12 | yAxisUpperValue: Float, 13 | yAxisLowerValue: Float, 14 | valueScale: List, 15 | yAxisZeroPosition: Float 16 | ): List { 17 | val xAxisTickWidth = size.width / (data.categories.size * 2) 18 | val maxBarsCountInACluster = 19 | data.categories.maxOfOrNull { it.entries.size } ?: return emptyList() 20 | val maximumThickness = config.thickness.toPx().coerceAtLeast(1f) 21 | val barWidth = (xAxisTickWidth / maxBarsCountInACluster).coerceIn(1f, maximumThickness) 22 | val gapsCount = maxBarsCountInACluster - 1 23 | val freeSpaceInACluster = (xAxisTickWidth - (barWidth * maxBarsCountInACluster)) 24 | val maxHorizontalSpacing = if (gapsCount > 0) freeSpaceInACluster / gapsCount else 0f 25 | val barsHorizontalSpacing = config.barsSpacing.toPx().coerceIn(0f, maxHorizontalSpacing) 26 | return data.categories.flatMapIndexed { categoryIndex, category -> 27 | val clusterWidth = category.entries.size * barWidth + 28 | (category.entries.size - 1) * barsHorizontalSpacing 29 | val clusterXOffset = (categoryIndex * 2 + 1) * xAxisTickWidth - clusterWidth / 2 30 | 31 | category.entries.mapIndexed { entryIndex, entry -> 32 | val x = clusterXOffset + entryIndex * (barWidth + barsHorizontalSpacing) 33 | val y = entry.y * valueScale[entryIndex] 34 | val currentPosition = y.mapValueToDifferentRange( 35 | yAxisLowerValue, 36 | yAxisUpperValue, 37 | size.height, 38 | 0f 39 | ) 40 | val barHeight = if (currentPosition < yAxisZeroPosition) { 41 | yAxisZeroPosition - currentPosition 42 | } else { 43 | currentPosition - yAxisZeroPosition 44 | } 45 | val newY = if (entry.y > 0) { 46 | currentPosition 47 | } else { 48 | yAxisZeroPosition 49 | } 50 | drawRoundRect( 51 | color = entry.color, 52 | topLeft = Offset( 53 | x = x, 54 | y = newY 55 | ), 56 | size = Size( 57 | width = barWidth, 58 | height = barHeight 59 | ), 60 | cornerRadius = CornerRadius(config.cornerRadius.toPx(), config.cornerRadius.toPx()) 61 | ) 62 | BarChartBar( 63 | width = x..(x + barWidth), 64 | height = newY..(newY + barHeight), 65 | data = entry 66 | ) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Creating new Pull Request 4 | * Fork repository and work on your fork. 5 | * Create a new PR with a request to merge to the **main** branch 6 | * If PR is based on issue, then include its number in PR title and add link to it in description 7 | * One pull request should always address one issue or feature 8 | * When contributing a new feature, provide motivation and use-cases describing value it may brings to the project 9 | * Adding video or screenshot is very beneficial, but it's not mandatory 10 | * Make sure any code contributed is covered by test and documented 11 | 12 | ## Code Style 13 | * Make sure your code is meeting the default [Ktlint standards](https://ktlint.github.io/#rules) 14 | 15 | ## Documentation 16 | Dokka is added to the project, so to create (or, better say, update) the docs, you need to run 17 | ``` 18 | ./gradlew charts:dokkaHtml 19 | ``` 20 | on the root dir of the project. 21 | 22 | After the task is done, copy the generated docs into `./docs`, which can also be done by running 23 | ``` 24 | rm -rf ./docs ; cp -r ./charts/build/dokka/html ./docs 25 | ``` 26 | 27 | ## Testing 28 | Testing is implemented using [Shot](https://github.com/pedrovgs/Shot) library. In order to run 29 | it against prerecorded results, create at least one of the following emulators: 30 | - tablet: Nexus 10, running API 31 31 | - phone: Pixel 4a, running API 31 32 | 33 | For tests to work, make sure you create emulators with default values, as **screen size and 34 | density must be exactly the same**. 35 | 36 | Details as to how to run the tests are in the link to the library itself, but to run it against 37 | prerecorded screenshots, run the following command: 38 | ``` 39 | ./gradlew executeScreenshotTests -PdirectorySuffix=$deviceName 40 | ``` 41 | where `$deviceName` is one of: 42 | - Nexus_10_API_31 43 | - Pixel_4a_API_31 44 | 45 | ### Flakiness 46 | Due to differences in graphics between M1 and x86 chips, images appear different to the script 47 | and so the tests fail (difference between images can be well above 10%). To human eye they do 48 | seem the same, though. This is explained and shown in [issue 265](https://github.com/pedrovgs/Shot/issues/265). Until this is fixed, there is a way to test stuff at least locally: 49 | 50 | 1. uncomment tests that are commented out because of this issue 51 | 2. record all the tests 52 | 3. make the desired changes in the code 53 | 4. check the new UI state against the pre-recorded screenshots (I suggest removing tolerance if 54 | the tests were recorded on your machine!) 55 | 5. comment the flaky tests out again 56 | 6. revert the screenshots 57 | 58 | ### Other issues 59 | 60 | - Library [does not work with Java 17](https://github.com/pedrovgs/Shot/pull/292). The error 61 | itself is not as obvious as one might expect, though. To fix the issue, use Java 11. 62 | - Java heap space sometimes [needs to be enlarged](https://github.com/pedrovgs/Shot/issues/304) 63 | - You might encounter [INSTALL_FAILED_SHARED_USER_INCOMPATIBLE](https://stackoverflow.com/questions/15205159/install-failed-shared-user-incompatible-while-using-shared-user-id) 64 | issue. [Here](https://stackoverflow.com/a/21809883/6835732) is a shortcut to the answer. 65 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/line/LineTest.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.line 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material.Text 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.test.junit4.createComposeRule 8 | import androidx.compose.ui.text.style.TextAlign 9 | import androidx.compose.ui.unit.dp 10 | import androidx.compose.ui.unit.sp 11 | import com.karumi.shot.ScreenshotTest 12 | import com.netguru.multiplatform.charts.Util.checkComposable 13 | import com.netguru.multiplatform.charts.theme.ChartDefaults 14 | import com.soywiz.klock.DateTime 15 | import org.junit.Rule 16 | import org.junit.Test 17 | 18 | class LineTest : ScreenshotTest { 19 | 20 | @get:Rule 21 | val composeRule = createComposeRule() 22 | 23 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 24 | // fun mixedValues_defaultUI() { 25 | // checkComposable(composeRule) { 26 | // LineChart( 27 | // lineChartData = Data.generateLineData(3) 28 | // ) 29 | // } 30 | // } 31 | // 32 | // @Test // removed due to: https://github.com/pedrovgs/Shot/issues/265 33 | // fun mixedValues_customUI() { 34 | // val data = Data.generateLineData(3) 35 | // checkComposable(composeRule) { 36 | // LineChart( 37 | // lineChartData = data 38 | // .copy( 39 | // series = data.series.map { 40 | // it.copy( 41 | // lineWidth = 12.dp, 42 | // fillColor = Color.Yellow, 43 | // lineColor = Color.Blue, 44 | // dashedLine = true, 45 | // ) 46 | // } 47 | // ), 48 | // chartColors = ChartDefaults.chartColors( 49 | // grid = Color.Magenta, 50 | // surface = Color.Cyan, 51 | // overlayLine = Color.Gray, 52 | // ), 53 | // xAxisLabel = { 54 | // Column( 55 | // horizontalAlignment = Alignment.CenterHorizontally, 56 | // ) { 57 | // Text( 58 | // fontSize = 12.sp, 59 | // text = DateTime.fromUnix(it as Long).format("yyyy-MM-dd"), 60 | // textAlign = TextAlign.Center 61 | // ) 62 | // Text( 63 | // text = "midday" 64 | // ) 65 | // } 66 | // }, 67 | // yAxisLabel = { 68 | // Column( 69 | // horizontalAlignment = Alignment.End 70 | // ) { 71 | // Text(text = it.toString()) 72 | // Text(text = "units") 73 | // } 74 | // }, 75 | // maxVerticalLines = 2, 76 | // maxHorizontalLines = 2, 77 | // ) 78 | // } 79 | // } 80 | } 81 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/dial/DialTest.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.dial 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.Text 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.test.junit4.createComposeRule 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.unit.sp 12 | import com.karumi.shot.ScreenshotTest 13 | import com.netguru.multiplatform.charts.theme.ChartDefaults 14 | import com.netguru.multiplatform.charts.Util.checkComposable 15 | import org.junit.Rule 16 | import org.junit.Test 17 | 18 | class DialTest : ScreenshotTest { 19 | 20 | @get:Rule 21 | val composeRule = createComposeRule() 22 | 23 | @Test 24 | fun range_0_100_value_50_UI_default() { 25 | checkComposable(composeRule) { Dial(value = 50, minValue = 0, maxValue = 100) } 26 | } 27 | 28 | @Test 29 | fun range_0_100_value_0_UI_default() { 30 | checkComposable(composeRule) { Dial(value = 0, minValue = 0, maxValue = 100) } 31 | } 32 | 33 | @Test 34 | fun range_0_100_value_100_UI_default() { 35 | checkComposable(composeRule) { Dial(value = 100, minValue = 0, maxValue = 100) } 36 | } 37 | 38 | @Test 39 | fun range_0_100_value_minus50_UI_default() { 40 | checkComposable(composeRule) { Dial(value = -50, minValue = 0, maxValue = 100) } 41 | } 42 | 43 | @Test 44 | fun range_0_100_value_150_UI_default() { 45 | checkComposable(composeRule) { Dial(value = 150, minValue = 0, maxValue = 100) } 46 | } 47 | 48 | @Test 49 | fun range_0_100_value_69_UI_custom_colors_and_labels() { 50 | checkComposable(composeRule) { 51 | Dial( 52 | value = 69, 53 | minValue = 0, 54 | maxValue = 100, 55 | colors = ChartDefaults.chartColors( 56 | primary = Color.Blue, 57 | grid = Color.Magenta, 58 | ).dialColors, 59 | minAndMaxValueLabel = { 60 | Text( 61 | text = it.toString(), 62 | color = Color.Blue, 63 | fontSize = 32.sp, 64 | modifier = Modifier.padding(top = 15.dp) 65 | ) 66 | }, 67 | mainLabel = { 68 | Column( 69 | horizontalAlignment = Alignment.CenterHorizontally 70 | ) { 71 | Text(text = it.toString(), color = Color.Blue, fontSize = 40.sp) 72 | Text(text = "tests", color = Color.Magenta, fontSize = 24.sp) 73 | } 74 | } 75 | ) 76 | } 77 | } 78 | 79 | @Test 80 | fun range_0_100_value_69_UI_no_labels() { 81 | checkComposable(composeRule) { 82 | Dial( 83 | value = 69, 84 | minValue = 0, 85 | maxValue = 100, 86 | minAndMaxValueLabel = { }, 87 | mainLabel = { } 88 | ) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /example-app/android/src/main/java/com/netguru/multiplatform/charts/android/previews/LineChartPreview.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.android.previews 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import androidx.compose.ui.unit.dp 9 | import com.netguru.multiplatform.charts.ChartAnimation 10 | import com.netguru.multiplatform.charts.common.AppTheme 11 | import com.netguru.multiplatform.charts.common.HOUR_IN_MS 12 | import com.netguru.multiplatform.charts.common.WindowSize 13 | import com.netguru.multiplatform.charts.line.LineChartData 14 | import com.netguru.multiplatform.charts.line.LineChartPoint 15 | import com.netguru.multiplatform.charts.line.LineChartSeries 16 | import com.netguru.multiplatform.charts.line.LineChartWithLegend 17 | import com.soywiz.klock.DateTime 18 | 19 | @Preview(showBackground = true, widthDp = 600) 20 | @Composable 21 | fun LineChartPreview() { 22 | AppTheme(windowSize = WindowSize.EXPANDED) { 23 | LineChartWithLegend( 24 | modifier = Modifier 25 | .height(300.dp) 26 | .fillMaxWidth(), 27 | lineChartData = getLineChartSampleData(), 28 | maxVerticalLines = 10, 29 | animation = ChartAnimation.Disabled, 30 | ) 31 | } 32 | } 33 | 34 | @Composable 35 | private fun getLineChartSampleData(): LineChartData { 36 | val startTime = DateTime.now().milliseconds 37 | val list = mutableListOf() 38 | 39 | list.add( 40 | LineChartSeries( 41 | "Solar", 42 | lineColor = AppTheme.colors.yellow, 43 | dashedLine = false, 44 | listOfPoints = listOf( 45 | LineChartPoint(0L * HOUR_IN_MS + startTime, 0f), 46 | LineChartPoint(1L * HOUR_IN_MS + startTime, 1f), 47 | LineChartPoint(2L * HOUR_IN_MS + startTime, 2f), 48 | LineChartPoint(3L * HOUR_IN_MS + startTime, 3f), 49 | LineChartPoint(5L * HOUR_IN_MS + startTime, 4f), 50 | ) 51 | ) 52 | ) 53 | 54 | list.add( 55 | LineChartSeries( 56 | "Grid", 57 | lineColor = AppTheme.colors.green, 58 | dashedLine = false, 59 | listOfPoints = listOf( 60 | LineChartPoint(0L * HOUR_IN_MS + startTime, 3f), 61 | LineChartPoint(1L * HOUR_IN_MS + startTime, 2f), 62 | LineChartPoint(2L * HOUR_IN_MS + startTime, 1f), 63 | LineChartPoint(5L * HOUR_IN_MS + startTime, 3f), 64 | ) 65 | ) 66 | ) 67 | 68 | list.add( 69 | LineChartSeries( 70 | "Fossil", 71 | lineColor = AppTheme.colors.blue, 72 | dashedLine = false, 73 | listOfPoints = listOf( 74 | LineChartPoint(0L * HOUR_IN_MS + startTime, 1f), 75 | LineChartPoint(1L * HOUR_IN_MS + startTime, 3f), 76 | LineChartPoint(2L * HOUR_IN_MS + startTime, 2f), 77 | LineChartPoint(3L * HOUR_IN_MS + startTime, 0f), 78 | LineChartPoint(5L * HOUR_IN_MS + startTime, 1f), 79 | ) 80 | ) 81 | ) 82 | 83 | return LineChartData( 84 | series = list, 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /docs/-kotlin multiplatform charts/com.netguru.multiplatform.charts.line/-symbol-shape/-l-i-n-e/name.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | name 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 23 |
24 |

name

25 |
26 |
27 | 28 |
29 |
val name: String
30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/-kotlin multiplatform charts/com.netguru.multiplatform.charts.line/-symbol-shape/-l-i-n-e/ordinal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ordinal 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 23 |
24 |

ordinal

25 |
26 |
27 | 28 |
29 |
val ordinal: Int
30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/-kotlin multiplatform charts/com.netguru.multiplatform.charts.line/-symbol-shape/-r-e-c-t-a-n-g-l-e/name.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | name 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 23 |
24 |

name

25 |
26 |
27 | 28 |
29 |
val name: String
30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/-kotlin multiplatform charts/com.netguru.multiplatform.charts.line/-symbol-shape/-r-e-c-t-a-n-g-l-e/ordinal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ordinal 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 23 |
24 |

ordinal

25 |
26 |
27 | 28 |
29 |
val ordinal: Int
30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example-app/android/src/main/java/com/netguru/multiplatform/charts/android/previews/BarChartPreview.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.android.previews 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.tooling.preview.Preview 9 | import androidx.compose.ui.unit.dp 10 | import com.netguru.multiplatform.charts.bar.BarChartCategory 11 | import com.netguru.multiplatform.charts.bar.BarChartData 12 | import com.netguru.multiplatform.charts.bar.BarChartEntry 13 | import com.netguru.multiplatform.charts.bar.BarChartWithLegend 14 | import com.netguru.multiplatform.charts.common.AppTheme 15 | import com.netguru.multiplatform.charts.common.WindowSize 16 | 17 | @Preview(widthDp = 800, showBackground = true) 18 | @Composable 19 | fun BarChartPreview() { 20 | AppTheme(windowSize = WindowSize.EXPANDED) { 21 | BarChartWithLegend( 22 | data = barChartSampleData(), 23 | modifier = Modifier 24 | .height(300.dp) 25 | .fillMaxWidth() 26 | .padding(20.dp), 27 | ) 28 | } 29 | } 30 | 31 | @Composable 32 | fun barChartSampleData(): BarChartData { 33 | return BarChartData( 34 | categories = listOf( 35 | BarChartCategory( 36 | name = "Mon", 37 | entries = listOf( 38 | BarChartEntry("Actual usage", 10f, AppTheme.colors.yellow), 39 | BarChartEntry("Forecasted usage", 20f, AppTheme.colors.green) 40 | ) 41 | ), 42 | BarChartCategory( 43 | name = "Tue", 44 | entries = listOf( 45 | BarChartEntry("Actual usage", 15f, AppTheme.colors.yellow), 46 | BarChartEntry("Forecasted usage", 10f, AppTheme.colors.green) 47 | ) 48 | ), 49 | BarChartCategory( 50 | name = "Wed", 51 | entries = listOf( 52 | BarChartEntry("Actual usage", 5f, AppTheme.colors.yellow), 53 | BarChartEntry("Forecasted usage", 10f, AppTheme.colors.green) 54 | ) 55 | ), 56 | BarChartCategory( 57 | name = "Thu", 58 | entries = listOf( 59 | BarChartEntry("Actual usage", 10f, AppTheme.colors.yellow), 60 | BarChartEntry("Forecasted usage", 10f, AppTheme.colors.green), 61 | ) 62 | ), 63 | BarChartCategory( 64 | name = "Fri", 65 | entries = listOf( 66 | BarChartEntry("Actual usage", 3f, AppTheme.colors.yellow), 67 | BarChartEntry("Forecasted usage", 2f, AppTheme.colors.green) 68 | ) 69 | ), 70 | BarChartCategory( 71 | name = "Sat", 72 | entries = listOf( 73 | BarChartEntry("Actual usage", 0f, AppTheme.colors.yellow), 74 | BarChartEntry("Forecasted usage", 2f, AppTheme.colors.green) 75 | ) 76 | ), 77 | BarChartCategory( 78 | name = "Sun", 79 | entries = listOf( 80 | BarChartEntry("Actual usage", 12f, AppTheme.colors.yellow), 81 | BarChartEntry("Forecasted usage", 12f, AppTheme.colors.green) 82 | ) 83 | ), 84 | ), 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/screen/DialChartScreen.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application.screen 2 | 3 | import androidx.compose.animation.core.Spring 4 | import androidx.compose.animation.core.spring 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import com.netguru.multiplatform.charts.ChartAnimation 15 | import com.netguru.multiplatform.charts.application.ScrollableScreen 16 | import com.netguru.multiplatform.charts.application.SpacedColumn 17 | import com.netguru.multiplatform.charts.application.TitleText 18 | import com.netguru.multiplatform.charts.common.AppTheme 19 | import com.netguru.multiplatform.charts.common.HorizontalDivider 20 | import com.netguru.multiplatform.charts.dial.Dial 21 | import com.netguru.multiplatform.charts.dial.DialConfig 22 | import com.netguru.multiplatform.charts.dial.PercentageDial 23 | 24 | @Composable 25 | fun DialChartScreen() { 26 | ScrollableScreen { 27 | SpacedColumn { 28 | TitleText(text = "Percentage dial") 29 | 30 | PercentageDial( 31 | percentage = 69, 32 | modifier = Modifier 33 | .fillMaxWidth(), 34 | animation = ChartAnimation.Simple { 35 | spring( 36 | dampingRatio = Spring.DampingRatioMediumBouncy, 37 | stiffness = Spring.StiffnessLow 38 | ) 39 | }, 40 | config = DialConfig( 41 | thickness = 20.dp, 42 | roundCorners = true, 43 | ), 44 | mainLabel = { 45 | Column( 46 | horizontalAlignment = Alignment.CenterHorizontally 47 | ) { 48 | Text( 49 | text = "$it%", 50 | style = MaterialTheme.typography.h4, 51 | color = AppTheme.colors.yellow 52 | ) 53 | Text( 54 | text = "of people like numbers", 55 | style = MaterialTheme.typography.body2, 56 | modifier = Modifier.padding(vertical = AppTheme.dimens.grid_2) 57 | ) 58 | } 59 | } 60 | ) 61 | 62 | HorizontalDivider() 63 | 64 | TitleText(text = "Custom ranged dial") 65 | 66 | Dial( 67 | modifier = Modifier 68 | .fillMaxWidth(), 69 | value = 17, 70 | minValue = -20, 71 | maxValue = 50, 72 | animation = ChartAnimation.Simple { 73 | spring( 74 | dampingRatio = Spring.DampingRatioMediumBouncy, 75 | stiffness = Spring.StiffnessLow 76 | ) 77 | }, 78 | config = DialConfig( 79 | thickness = 30.dp, 80 | ), 81 | mainLabel = { 82 | Text( 83 | text = "$it°C", 84 | style = MaterialTheme.typography.h4, 85 | color = AppTheme.colors.yellow 86 | ) 87 | } 88 | ) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /example-app/android/src/androidTest/kotlin/com/netguru/multiplatform/charts/bar/BarWithLegendTest.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.bar 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material.Text 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.test.junit4.createComposeRule 8 | import androidx.compose.ui.unit.dp 9 | import com.karumi.shot.ScreenshotTest 10 | import com.netguru.multiplatform.charts.Util.checkComposable 11 | import com.netguru.multiplatform.charts.theme.ChartDefaults 12 | import org.junit.Rule 13 | import org.junit.Test 14 | 15 | class BarWithLegendTest : ScreenshotTest { 16 | 17 | @get:Rule 18 | val composeRule = createComposeRule() 19 | 20 | @Suppress("SameParameterValue") 21 | private fun testBasicChartWithLegend( 22 | nOfCategories: Int, 23 | nOfEntries: Int, 24 | valueTypes: Data.ValueTypes 25 | ) { 26 | val data = Data.generateData(nOfCategories, nOfEntries, valueTypes) 27 | checkComposable(composeRule) { 28 | BarChartWithLegend( 29 | data = data, 30 | config = BarChartConfig( 31 | thickness = 8.dp, 32 | cornerRadius = 0.dp, 33 | barsSpacing = 1.dp, 34 | maxHorizontalLinesCount = 2, 35 | ) 36 | ) 37 | } 38 | } 39 | 40 | @Suppress("SameParameterValue") 41 | private fun testCustomChartWithLegend( 42 | nOfCategories: Int, 43 | nOfEntries: Int, 44 | valueTypes: Data.ValueTypes 45 | ) { 46 | val data = Data.generateData(nOfCategories, nOfEntries, valueTypes) 47 | checkComposable(composeRule) { 48 | BarChartWithLegend( 49 | data = data, 50 | colors = ChartDefaults.chartColors(grid = Color.Red).barChartColors, 51 | xAxisLabel = { 52 | Column( 53 | horizontalAlignment = Alignment.CenterHorizontally, 54 | ) { 55 | Text(text = it.toString()) 56 | Text(text = "testing") 57 | } 58 | }, 59 | yAxisLabel = { 60 | Column( 61 | horizontalAlignment = Alignment.CenterHorizontally, 62 | ) { 63 | Text(text = it.toString()) 64 | Text(text = "units") 65 | } 66 | }, 67 | legendItemLabel = { 68 | Text(text = "Custom label for: $it") 69 | } 70 | ) 71 | } 72 | } 73 | 74 | /******************************** 75 | * zero values 76 | ********************************/ 77 | 78 | // @Test 79 | // fun oneCategory_oneBar_zeroValues() { TODO BN-3899 80 | // val data = Data.generateData(1, 1, Data.ValueTypes.ZERO) 81 | // checkComposable(composeRule) { 82 | // BarChartWithLegend(data = data) 83 | // } 84 | // } 85 | 86 | /******************************** 87 | * positive values 88 | ********************************/ 89 | 90 | @Test 91 | fun oneCategory_oneBar_positiveValues() { 92 | testBasicChartWithLegend(1, 1, Data.ValueTypes.POSITIVE) 93 | } 94 | 95 | @Test 96 | fun oneCategory_fiveBars_positiveValues() { 97 | testBasicChartWithLegend(1, 5, Data.ValueTypes.POSITIVE) 98 | } 99 | 100 | @Test 101 | fun oneCategory_oneBar_positiveValues_customUI() { 102 | testCustomChartWithLegend(1, 1, Data.ValueTypes.POSITIVE) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | ## SDK Versions 3 | compileSdk = "35" 4 | minSdk = "24" 5 | targetSdk = "31" 6 | versionCode = "1" 7 | versionName = "0.1" 8 | applicationId = "com.netguru.multiplatform.charts" 9 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 10 | optInFlags = "-opt-in=kotlin.RequiresOptIn" 11 | 12 | #kotlin 13 | jvmTarget = "11" 14 | 15 | #desktop 16 | desktop-packageVersion = "1.0.0" 17 | desktop-packageName = "jvm" 18 | 19 | # Dependencies 20 | kotlin-gradle-plugin = "2.1.20" 21 | android-gradle-plugin = "8.9.1" 22 | jetbrains-compose = "1.7.3" 23 | activity-compose = "1.10.1" 24 | compose-test = "1.7.8" 25 | coroutines = "1.10.2" 26 | appcompat = "1.7.0" 27 | core-ktx = "1.16.0" 28 | junit = "4.13.2" 29 | window = "1.3.0" 30 | turbine = "0.7.0" 31 | klock = "2.4.13" 32 | kotlin-poet = "1.14.2" 33 | mockk = "1.12.3" 34 | shot = "5.14.1" 35 | ktlint = "10.2.1" 36 | dokka = "2.0.0" 37 | 38 | # Project 39 | project-group = "com.netguru" 40 | project-version = "0.1" 41 | project-name = "Compose Multiplatform Charts" 42 | 43 | 44 | [libraries] 45 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-gradle-plugin" } 46 | plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle-plugin" } 47 | plugin-android = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" } 48 | plugin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlin-poet"} 49 | plugin-shot = { module = "com.karumi:shot", version.ref = "shot" } 50 | compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "jetbrains-compose" } 51 | compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "jetbrains-compose" } 52 | compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "jetbrains-compose" } 53 | compose-material = { module = "org.jetbrains.compose.material:material", version.ref = "jetbrains-compose" } 54 | compose-materialIconsExtended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "jetbrains-compose" } 55 | compose-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "jetbrains-compose" } 56 | compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "jetbrains-compose" } 57 | kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines"} 58 | androidx-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } 59 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } 60 | androidx-coreKtx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } 61 | androidx-window = { module = "androidx.window:window", version.ref = "window" } 62 | test-junit = { module = "junit:junit", version.ref = "junit" } 63 | test-ui-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-test" } 64 | test-ui-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose-test" } 65 | test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } 66 | test-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } 67 | time-klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" } 68 | mockk = { module = "io.mockk:mockk", version.ref = "mockk" } 69 | mockk-agentJvm = { module = "io.mockk:mockk-agent-jvm", version.ref = "mockk" } 70 | mockk-common = { module = "io.mockk:mockk-common", version.ref = "mockk" } 71 | 72 | [plugins] 73 | compose-desktop = { id = "org.jetbrains.compose", version.ref = "jetbrains-compose" } 74 | ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } 75 | kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-gradle-plugin"} 76 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 77 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-gradle-plugin" } 78 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-gradle-plugin" } 79 | -------------------------------------------------------------------------------- /example-app/application/src/commonMain/kotlin/com/netguru/multiplatform/charts/application/screen/LineChartScreen.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.application.screen 2 | 3 | import androidx.compose.foundation.layout.height 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.text.style.TextAlign 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.unit.sp 13 | import com.netguru.multiplatform.charts.ChartAnimation 14 | import com.netguru.multiplatform.charts.application.ScrollableScreen 15 | import com.netguru.multiplatform.charts.application.SpacedColumn 16 | import com.netguru.multiplatform.charts.application.TitleText 17 | import com.netguru.multiplatform.charts.common.HorizontalDivider 18 | import com.netguru.multiplatform.charts.line.LineChart 19 | import com.netguru.multiplatform.charts.line.LineChartData 20 | import com.netguru.multiplatform.charts.line.LineChartPoint 21 | import com.netguru.multiplatform.charts.line.LineChartSeries 22 | import com.netguru.multiplatform.charts.line.LineChartWithLegend 23 | import com.soywiz.klock.DateTime 24 | import com.soywiz.klock.TimeSpan 25 | 26 | @Composable 27 | fun LineChartScreen() { 28 | 29 | val lineData = remember { 30 | LineChartData( 31 | series = (1..3).map { 32 | LineChartSeries( 33 | dataName = "data $it", 34 | lineColor = listOf( 35 | Color(0xFFFFCC00), 36 | Color(0xFF00D563), 37 | Color(0xFF32ADE6), 38 | )[it - 1], 39 | listOfPoints = (1..10).map { point -> 40 | LineChartPoint( 41 | x = DateTime.now().minus(TimeSpan(point * 24 * 60 * 60 * 1000.0)).unixMillisLong, 42 | y = (1..15).random().toFloat(), 43 | ) 44 | } 45 | ) 46 | }, 47 | ) 48 | } 49 | 50 | ScrollableScreen { 51 | SpacedColumn { 52 | 53 | TitleText(text = "Line chart") 54 | LineChart( 55 | lineChartData = lineData, 56 | modifier = Modifier 57 | .height(300.dp), 58 | xAxisLabel = { 59 | Text( 60 | fontSize = 12.sp, 61 | text = DateTime.fromUnix(it as Long).format("yyyy-MM-dd"), 62 | textAlign = TextAlign.Center 63 | ) 64 | }, 65 | overlayHeaderLabel = { 66 | Text( 67 | text = DateTime.fromUnix(it as Long).format("yyyy-MM-dd"), 68 | style = MaterialTheme.typography.overline 69 | ) 70 | }, 71 | animation = ChartAnimation.Sequenced() 72 | ) 73 | 74 | HorizontalDivider() 75 | 76 | TitleText(text = "Line chart with legend") 77 | LineChartWithLegend( 78 | modifier = Modifier 79 | .height(300.dp), 80 | lineChartData = lineData, 81 | maxVerticalLines = 5, 82 | xAxisLabel = { 83 | Text( 84 | fontSize = 12.sp, 85 | text = DateTime.fromUnix(it as Long).format("yyyy-MM-dd"), 86 | textAlign = TextAlign.Center 87 | ) 88 | }, 89 | overlayHeaderLabel = { 90 | Text( 91 | text = DateTime.fromUnix(it as Long).format("yyyy-MM-dd"), 92 | style = MaterialTheme.typography.overline 93 | ) 94 | }, 95 | animation = ChartAnimation.Sequenced() 96 | ) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/line/Line.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.line 2 | 3 | import androidx.compose.ui.geometry.Size 4 | import androidx.compose.ui.graphics.Brush 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.Path 7 | import androidx.compose.ui.graphics.drawscope.DrawScope 8 | import androidx.compose.ui.graphics.drawscope.Fill 9 | import androidx.compose.ui.graphics.drawscope.Stroke 10 | import androidx.compose.ui.unit.Dp 11 | import com.netguru.multiplatform.charts.mapValueToDifferentRange 12 | 13 | internal fun DrawScope.drawLineChart( 14 | lineChartData: LineChartData, 15 | graphTopPadding: Dp, 16 | graphBottomPadding: Dp, 17 | alpha: List, 18 | ) { 19 | // calculate path 20 | val path = Path() 21 | lineChartData.series.forEachIndexed { seriesIndex, data -> 22 | 23 | val mappedPoints = 24 | mapDataToPixels( 25 | lineChartData, 26 | data, 27 | size, 28 | graphTopPadding.toPx(), 29 | graphBottomPadding.toPx() 30 | ) 31 | val connectionPoints = calculateConnectionPointsForBezierCurve(mappedPoints) 32 | 33 | path.reset() // reuse path 34 | mappedPoints.forEachIndexed { index, value -> 35 | if (index == 0) { 36 | path.moveTo(value.x, value.y) 37 | } else { 38 | path.cubicTo( 39 | connectionPoints[index - 1].first.x, 40 | connectionPoints[index - 1].first.y, 41 | connectionPoints[index - 1].second.x, 42 | connectionPoints[index - 1].second.y, 43 | value.x, 44 | value.y 45 | ) 46 | } 47 | } 48 | 49 | // draw line 50 | drawPath( 51 | path = path, 52 | color = data.lineColor.copy(alpha[seriesIndex]), 53 | style = Stroke( 54 | width = data.lineWidth.toPx(), 55 | pathEffect = if (data.dashedLine) dashedPathEffect else null 56 | ) 57 | ) 58 | 59 | // close shape and fill 60 | path.lineTo(mappedPoints.last().x, size.height) 61 | path.lineTo(mappedPoints.first().x, size.height) 62 | drawPath( 63 | path = path, 64 | Brush.verticalGradient( 65 | listOf( 66 | Color.Transparent, 67 | data.fillColor.copy(alpha[seriesIndex] / 12), 68 | data.fillColor.copy(alpha[seriesIndex] / 6) 69 | ), 70 | startY = path.getBounds().bottom, 71 | endY = path.getBounds().top, 72 | ), 73 | style = Fill 74 | ) 75 | } 76 | } 77 | 78 | private fun mapDataToPixels( 79 | lineChartData: LineChartData, 80 | currentSeries: LineChartSeries, 81 | canvasSize: Size, 82 | graphTopPadding: Float = 0f, 83 | graphBottomPadding: Float, 84 | ): List { 85 | val mappedPoints = currentSeries.listOfPoints.map { 86 | val x = it.x.mapValueToDifferentRange( 87 | lineChartData.minX, 88 | lineChartData.maxX, 89 | 0L, 90 | canvasSize.width.toLong() 91 | ).toFloat() 92 | val y = it.y.mapValueToDifferentRange( 93 | lineChartData.minY, 94 | lineChartData.maxY, 95 | canvasSize.height - graphBottomPadding, 96 | graphTopPadding 97 | ) 98 | PointF(x, y) 99 | } 100 | 101 | return mappedPoints 102 | } 103 | 104 | private fun calculateConnectionPointsForBezierCurve(points: List): MutableList> { 105 | val conPoint = mutableListOf>() 106 | for (i in 1 until points.size) { 107 | conPoint.add( 108 | Pair( 109 | PointF((points[i].x + points[i - 1].x) / 2f, points[i - 1].y), 110 | PointF((points[i].x + points[i - 1].x) / 2f, points[i].y) 111 | ) 112 | ) 113 | } 114 | return conPoint 115 | } 116 | 117 | data class PointF(val x: Float, val y: Float) 118 | -------------------------------------------------------------------------------- /charts/src/commonMain/kotlin/com/netguru/multiplatform/charts/grid/GridChartDrawing.kt: -------------------------------------------------------------------------------- 1 | package com.netguru.multiplatform.charts.grid 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.drawscope.DrawScope 6 | import androidx.compose.ui.unit.Dp 7 | import com.netguru.multiplatform.charts.grid.axisscale.XAxisScale 8 | import com.netguru.multiplatform.charts.grid.axisscale.YAxisScale 9 | import com.netguru.multiplatform.charts.mapValueToDifferentRange 10 | 11 | fun DrawScope.drawChartGrid(grid: ChartGrid, color: Color) { 12 | grid.horizontalLines.forEach { 13 | drawLine( 14 | color = color, 15 | start = Offset(0f, it.position), 16 | end = Offset(size.width, it.position), 17 | strokeWidth = 1f 18 | ) 19 | } 20 | drawLine( 21 | color = color, 22 | start = Offset(0f, grid.zeroPosition.position), 23 | end = Offset(size.width, grid.zeroPosition.position), 24 | strokeWidth = 1f 25 | ) 26 | grid.verticalLines.forEach { 27 | drawLine( 28 | color = color, 29 | start = Offset(it.position, 0f), 30 | end = Offset(it.position, size.height), 31 | strokeWidth = 1f 32 | ) 33 | } 34 | } 35 | 36 | fun DrawScope.measureChartGrid( 37 | xAxisScale: XAxisScale, 38 | yAxisScale: YAxisScale, 39 | horizontalLinesOffset: Dp 40 | ): ChartGrid { 41 | 42 | val horizontalLines = measureHorizontalLines( 43 | axisScale = yAxisScale, 44 | startPosition = size.height, 45 | endPosition = 0f 46 | ) 47 | 48 | val verticalLines = measureVerticalLines( 49 | axisScale = xAxisScale, 50 | startPosition = 0f, 51 | endPosition = size.width 52 | ) 53 | 54 | val zero = when { 55 | yAxisScale.min > 0 -> yAxisScale.min 56 | yAxisScale.max < 0 -> yAxisScale.max 57 | else -> 0f 58 | } 59 | return ChartGrid( 60 | verticalLines = verticalLines, 61 | horizontalLines = horizontalLines, 62 | zeroPosition = LineParameters( 63 | zero.mapValueToDifferentRange( 64 | yAxisScale.min, 65 | yAxisScale.max, 66 | size.height, 67 | 0f 68 | ), 69 | zero 70 | ) 71 | ) 72 | } 73 | 74 | private fun measureHorizontalLines( 75 | axisScale: YAxisScale, 76 | startPosition: Float, 77 | endPosition: Float 78 | ): List { 79 | val horizontalLines = mutableListOf() 80 | 81 | if (axisScale.max == axisScale.min || axisScale.tick == 0f) 82 | return listOf( 83 | LineParameters( 84 | position = startPosition / 2f, 85 | value = 0 86 | ) 87 | ) 88 | 89 | val valueStep = axisScale.tick 90 | var currentValue = axisScale.min 91 | 92 | while (currentValue in axisScale.min..axisScale.max) { 93 | val currentPosition = currentValue.mapValueToDifferentRange( 94 | axisScale.min, 95 | axisScale.max, 96 | startPosition, 97 | endPosition 98 | ) 99 | horizontalLines.add( 100 | LineParameters( 101 | position = currentPosition, 102 | value = currentValue 103 | ) 104 | ) 105 | currentValue += valueStep 106 | } 107 | return horizontalLines 108 | } 109 | 110 | private fun measureVerticalLines( 111 | axisScale: XAxisScale, 112 | startPosition: Float, 113 | endPosition: Float 114 | ): List { 115 | val verticalLines = mutableListOf() 116 | val valueStep = axisScale.tick 117 | var currentValue = axisScale.start 118 | 119 | while (currentValue in axisScale.min..axisScale.max) { 120 | val currentPosition = currentValue.mapValueToDifferentRange( 121 | axisScale.min, 122 | axisScale.max, 123 | startPosition, 124 | endPosition 125 | ) 126 | verticalLines.add( 127 | LineParameters( 128 | position = currentPosition, 129 | value = currentValue 130 | ) 131 | ) 132 | currentValue += valueStep 133 | } 134 | return verticalLines 135 | } 136 | -------------------------------------------------------------------------------- /docs/-kotlin multiplatform charts/com.netguru.multiplatform.charts.bubble/-bubble/update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | update 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 23 |
24 |

update

25 |
26 |
27 | 28 |
29 |
fun update()
30 |
31 |
32 |

Sources

33 |
34 |
35 |
36 |
common source 37 |
Link copied to clipboard
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/-kotlin multiplatform charts/com.netguru.multiplatform.charts.line/-point-f/x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | x 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 23 |
24 |

x

25 |
26 |
27 | 28 |
29 |
val x: Float
30 |
31 |
32 |

Sources

33 |
34 |
35 |
36 |
common source 37 |
Link copied to clipboard
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/-kotlin multiplatform charts/com.netguru.multiplatform.charts.line/-point-f/y.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | y 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 23 |
24 |

y

25 |
26 |
27 | 28 |
29 |
val y: Float
30 |
31 |
32 |

Sources

33 |
34 |
35 |
36 |
common source 37 |
Link copied to clipboard
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 |
47 | 48 | 49 | 50 | --------------------------------------------------------------------------------