Repository: bytebeats/compose-charts Branch: master Commit: 06a4fc707b1c Files: 83 Total size: 151.8 KB Directory structure: gitextract_1pinupfr/ ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── me/ │ │ └── bytebeats/ │ │ └── views/ │ │ └── charts/ │ │ └── app/ │ │ ├── MainActivity.kt │ │ └── ui/ │ │ ├── ComposeCharts.kt │ │ ├── Screen.kt │ │ ├── ScreenRouter.kt │ │ ├── screen/ │ │ │ ├── HomeScreen.kt │ │ │ ├── bar/ │ │ │ │ ├── BarChartDataModel.kt │ │ │ │ └── BarChartScreen.kt │ │ │ ├── line/ │ │ │ │ ├── LineChartDataModel.kt │ │ │ │ ├── LineChartScreen.kt │ │ │ │ └── PointDrawerType.kt │ │ │ └── pie/ │ │ │ ├── PieChartDataModel.kt │ │ │ └── PieChartScreen.kt │ │ └── theme/ │ │ ├── Color.kt │ │ ├── Margin.kt │ │ ├── Shape.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res/ │ ├── drawable/ │ │ └── ic_launcher_background.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── values/ │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle.kts ├── charts/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── me/ │ └── bytebeats/ │ └── views/ │ └── charts/ │ ├── Animations.kt │ ├── AxisLine.kt │ ├── Colors.kt │ ├── TypeAlias.kt │ ├── bar/ │ │ ├── BarChart.kt │ │ ├── BarChartData.kt │ │ ├── BarCharts.kt │ │ └── render/ │ │ ├── bar/ │ │ │ ├── IBarDrawer.kt │ │ │ └── SimpleBarDrawer.kt │ │ ├── label/ │ │ │ ├── ILabelDrawer.kt │ │ │ └── SimpleLabelDrawer.kt │ │ ├── xaxis/ │ │ │ ├── IXAxisDrawer.kt │ │ │ └── SimpleXAxisDrawer.kt │ │ └── yaxis/ │ │ ├── IYAxisDrawer.kt │ │ └── SimpleYAxisDrawer.kt │ ├── line/ │ │ ├── LineChart.kt │ │ ├── LineChartData.kt │ │ ├── LineCharts.kt │ │ └── render/ │ │ ├── line/ │ │ │ ├── EmptyLineShader.kt │ │ │ ├── GradientLineShader.kt │ │ │ ├── ILineDrawer.kt │ │ │ ├── ILineShader.kt │ │ │ ├── SolidLineDrawer.kt │ │ │ └── SolidLineShader.kt │ │ ├── point/ │ │ │ ├── EmptyPointDrawer.kt │ │ │ ├── FilledCircularPointDrawer.kt │ │ │ ├── HollowCircularPointDrawer.kt │ │ │ └── IPointDrawer.kt │ │ ├── xaxis/ │ │ │ ├── IXAxisDrawer.kt │ │ │ └── SimpleXAxisDrawer.kt │ │ └── yaxis/ │ │ ├── IYAxisDrawer.kt │ │ └── SimpleYAxisDrawer.kt │ ├── pie/ │ │ ├── PieChart.kt │ │ ├── PieChartData.kt │ │ ├── PieCharts.kt │ │ └── render/ │ │ ├── ISliceDrawer.kt │ │ └── SimpleSliceDrawer.kt │ └── util/ │ └── Floats.kt ├── config/ │ └── detekt/ │ └── detekt.yml ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /.idea/ out/ /captures .externalNativeBuild .cxx local.properties ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Chen Pan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # compose-charts [![GitHub latest commit](https://badgen.net/github/last-commit/bytebeats/compose-charts)](https://github.com/bytebeats/compose-charts/commit/) [![GitHub contributors](https://img.shields.io/github/contributors/bytebeats/compose-charts.svg)](https://github.com/bytebeats/compose-charts/graphs/contributors/) [![GitHub issues](https://img.shields.io/github/issues/bytebeats/compose-charts.svg)](https://github.com/bytebeats/compose-charts/issues/) [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/bytebeats/compose-charts/) [![GitHub forks](https://img.shields.io/github/forks/bytebeats/compose-charts.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/bytebeats/compose-charts/network/) [![GitHub stars](https://img.shields.io/github/stars/bytebeats/compose-charts.svg?style=social&label=Star&maxAge=2592000)](https://github.com/bytebeats/compose-charts/stargazers/) [![GitHub watchers](https://img.shields.io/github/watchers/bytebeats/compose-charts.svg?style=social&label=Watch&maxAge=2592000)](https://github.com/bytebeats/compose-charts/watchers/) Simple Jetpack Compose Charts for multi-platform. Including Android, Web, Desktop. Compose Multiplatform for Desktop: [compose-charts-desktop](https://github.com/bytebeats/compose-charts-desktop). **LATEST_VERSION**: 0.2.1 **COMPOSE_VERSION**: 1.6.7 **KOTLIN_VERSION**: 1.9.24 ## Graph Effects ## How to use? 1. add maven and dependency: 1.1. add specific maven url in your root `build.gradle.kts` ``` repositories { ... maven { url = uri("https://repo1.maven.org/maven2/") } } ``` 1.2. add dependency in your module `build.gradle.kts` ``` dependencies { implementation("androidx.compose.ui:ui:$compose_version") // implementation(project(":charts")) implementation("io.github.bytebeats:compose-charts:${LATEST_VERSION}") } ``` 2. show Pie Chart in Jetpack Compose: ``` @Composable fun PieChartView() { PieChart( pieChartData = PieChartData( slices = listOf( PieChartData.Slice( randomLength(), randomColor() ), PieChartData.Slice(randomLength(), randomColor()), PieChartData.Slice(randomLength(), randomColor()) ) ), // Optional properties. modifier = Modifier.fillMaxSize(), animation = simpleChartAnimation(), sliceDrawer = SimpleSliceDrawer() ) } ``` 3. show Line Chart in Jetpack Compose: ``` @Composable fun LineChartView() { LineChart( lineChartData = LineChartData( points = listOf( Point(randomYValue(), "Line 1"), Point(randomYValue(), "Line 2"), Point(randomYValue(), "Line 3"), Point(randomYValue(), "Line 4"), Point(randomYValue(), "Line 5"), Point(randomYValue(), "Line 6"), Point(randomYValue(), "Line 7") ) ), // Optional properties. modifier = Modifier.fillMaxSize(), animation = simpleChartAnimation(), pointDrawer = FilledCircularPointDrawer(), lineDrawer = SolidLineDrawer(), xAxisDrawer = SimpleXAxisDrawer(), yAxisDrawer = SimpleYAxisDrawer(), horizontalOffset = 5f ) } ``` 4. show Bar Chart in Jetpack Compose: ``` @Composable fun BarChartView() { BarChart( barChartData = BarChartData( bars = listOf( BarChartData.Bar( label = "Bar 1", value = randomValue(), color = randomColor() ), BarChartData.Bar( label = "Bar 2", value = randomValue(), color = randomColor() ), BarChartData.Bar( label = "Bar 3", value = randomValue(), color = randomColor() ), BarChartData.Bar( label = "Bar 4", value = randomValue(), color = randomColor() ), ) ), // Optional properties. modifier = Modifier.fillMaxSize(), animation = simpleChartAnimation(), barDrawer = SimpleBarDrawer(), xAxisDrawer = SimpleXAxisDrawer(), yAxisDrawer = SimpleYAxisDrawer(), labelDrawer = SimpleLabelDrawer() ) } ``` ## Stargazers over time [![Stargazers over time](https://starchart.cc/bytebeats/compose-charts.svg)](https://starchart.cc/bytebeats/compose-charts) ## Github Stars Sparklines [![Sparkline](https://stars.medv.io/bytebeats/compose-charts.svg)](https://stars.medv.io/bytebeats/compose-charts) ## Contributors [![Contributors over time](https://contributor-graph-api.apiseven.com/contributors-svg?chart=contributorOverTime&repo=bytebeats/compose-charts)](https://www.apiseven.com/en/contributor-graph?chart=contributorOverTime&repo=bytebeats/compose-charts) ## MIT License Copyright (c) 2021 Chen Pan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: app/.gitignore ================================================ /build .idea/ out/ ================================================ FILE: app/build.gradle.kts ================================================ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.detekt.gradle.plugin) } android { namespace = "me.bytebeats.views.charts.app" compileSdk = 34 defaultConfig { applicationId = "me.bytebeats.views.charts.app" minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = libs.versions.ktCompilerExt.get() } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) implementation(libs.androidx.ui) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) // implementation(project(":charts")) implementation(libs.compose.charts) } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/MainActivity.kt ================================================ package me.bytebeats.views.charts.app import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import me.bytebeats.views.charts.app.ui.ComposeCharts /** * Main activity */ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeCharts() } } } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/ComposeCharts.kt ================================================ package me.bytebeats.views.charts.app.ui import androidx.compose.animation.Crossfade import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import me.bytebeats.views.charts.app.ui.screen.HomeScreen import me.bytebeats.views.charts.app.ui.screen.bar.BarChartScreen import me.bytebeats.views.charts.app.ui.screen.line.LineChartScreen import me.bytebeats.views.charts.app.ui.screen.pie.PieChartScreen import me.bytebeats.views.charts.app.ui.theme.ComposeChartsTheme /** * Created by bytebeats on 2021/9/30 : 16:24 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ @Composable fun ComposeCharts() { ComposeChartsTheme { ComposeChartsContent() } } @Composable private fun ComposeChartsContent() { Crossfade( targetState = ScreenRouter.currentScreen, label = "Compose Charts" ) { screen -> Surface( color = MaterialTheme.colorScheme.background ) { when (screen) { Screen.Pie -> PieChartScreen() Screen.Line -> LineChartScreen() Screen.Bar -> BarChartScreen() else -> HomeScreen() } } } } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/Screen.kt ================================================ package me.bytebeats.views.charts.app.ui /** * Created by bytebeats on 2021/9/30 : 11:34 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ enum class Screen { Home, Pie, Bar, Line; } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/ScreenRouter.kt ================================================ package me.bytebeats.views.charts.app.ui import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue /** * Created by bytebeats on 2021/9/30 : 11:39 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ object ScreenRouter { var currentScreen by mutableStateOf(Screen.Home) /** * Navigate * * @param screen */ fun navigate(screen: Screen) { currentScreen = screen } /** * Navigate home */ fun navigateHome() { currentScreen = Screen.Home } } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/screen/HomeScreen.kt ================================================ package me.bytebeats.views.charts.app.ui.screen import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import me.bytebeats.views.charts.app.ui.Screen import me.bytebeats.views.charts.app.ui.ScreenRouter import me.bytebeats.views.charts.app.ui.theme.Margin /** * Created by bytebeats on 2021/9/30 : 11:43 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen() { Scaffold( topBar = { TopAppBar( title = { Text(text = "Compose Charts") } ) } ) { paddingValues -> HomeScreenContent(Modifier.padding(paddingValues)) } } @Composable private fun HomeScreenContent(modifier: Modifier) { Column( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { ChartScreenSelector( text = "Pie Chart", nextScreen = Screen.Pie ) ChartScreenSelector( text = "Line Chart", nextScreen = Screen.Line ) ChartScreenSelector( text = "Bar Chart", nextScreen = Screen.Bar ) } } @Composable private fun ChartScreenSelector( text: String, nextScreen: Screen ) { Row( modifier = Modifier.padding( horizontal = Margin.horizontal, vertical = Margin.vertical ) ) { TextButton( onClick = { ScreenRouter.navigate(nextScreen) } ) { Text(text = text) } } } @Preview @Composable private fun HomeScreenPreview() = HomeScreen() ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/screen/bar/BarChartDataModel.kt ================================================ package me.bytebeats.views.charts.app.ui.screen.bar import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import me.bytebeats.views.charts.bar.BarChartData import me.bytebeats.views.charts.bar.render.label.SimpleLabelDrawer import kotlin.random.Random /** * Created by bytebeats on 2021/9/30 : 19:39 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ class BarChartDataModel { private var colors = mutableListOf( Color(0XFFF44336), Color(0XFFE91E63), Color(0XFF9C27B0), Color(0XFF673AB7), Color(0XFF3F51B5), Color(0XFF03A9F4), Color(0XFF009688), Color(0XFFCDDC39), Color(0XFFFFC107), Color(0XFFFF5722), Color(0XFF795548), Color(0XFF9E9E9E), Color(0XFF607D8B) ) var labelDrawer by mutableStateOf(SimpleLabelDrawer(drawLocation = SimpleLabelDrawer.DrawLocation.Inside)) private set var barChartData by mutableStateOf( BarChartData( bars = listOf( BarChartData.Bar( label = "Bar 1", value = randomValue(), color = randomColor() ), BarChartData.Bar( label = "Bar 2", value = randomValue(), color = randomColor() ), BarChartData.Bar( label = "Bar 3", value = randomValue(), color = randomColor() ), BarChartData.Bar( label = "Bar 4", value = randomValue(), color = randomColor() ), ) ) ) val bars: List get() = barChartData.bars var labelLocation: SimpleLabelDrawer.DrawLocation = SimpleLabelDrawer.DrawLocation.Inside set(value) { val color = when (value) { SimpleLabelDrawer.DrawLocation.Inside -> Color.White SimpleLabelDrawer.DrawLocation.Outside, SimpleLabelDrawer.DrawLocation.XAxis -> Color.Black } labelDrawer = SimpleLabelDrawer(drawLocation = value, labelTextColor = color) field = value } internal fun addBar() { barChartData = barChartData.copy(bars = bars.toMutableList().apply { add( BarChartData.Bar( label = "Bar ${bars.size + 1}", value = randomValue(), color = randomColor() ) ) }.toList()) } internal fun removeBar() { barChartData = barChartData.copy(bars = bars.toMutableList().apply { val lastBar = bars.last() colors.add(lastBar.color) remove(lastBar) }) } private fun randomValue(): Float = Random.Default.nextInt(25, 125).toFloat() private fun randomColor(): Color { val idx = Random.Default.nextInt(colors.size) return colors.removeAt(idx) } } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/screen/bar/BarChartScreen.kt ================================================ package me.bytebeats.views.charts.app.ui.screen.bar import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.bytebeats.views.charts.app.ui.ScreenRouter import me.bytebeats.views.charts.app.ui.theme.Margin import me.bytebeats.views.charts.bar.BarChart import me.bytebeats.views.charts.bar.render.label.SimpleLabelDrawer.DrawLocation import me.bytebeats.views.charts.bar.render.yaxis.SimpleYAxisDrawer /** * Created by bytebeats on 2021/9/30 : 19:53 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun BarChartScreen() { Scaffold( topBar = { TopAppBar( navigationIcon = { IconButton( onClick = { ScreenRouter.navigateHome() } ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Go back home" ) } }, title = { Text(text = "Bar Chart") }) }) { paddingValues -> BarChartContent(Modifier.padding(paddingValues)) } } @Composable private fun BarChartContent(modifier: Modifier = Modifier) { val barChartDataModel = BarChartDataModel() Column( modifier = modifier.padding( horizontal = Margin.horizontal, vertical = Margin.vertical ) ) { BarChartRow(barChartDataModel = barChartDataModel) DrawLabelLocation( barChartDataModel = barChartDataModel, newLocation = { barChartDataModel.labelLocation = it } ) AddOrRemoveBar(barChartDataModel = barChartDataModel) } } @Composable private fun BarChartRow(barChartDataModel: BarChartDataModel) { Row( modifier = Modifier .fillMaxWidth() .height(280.dp) .padding(vertical = Margin.verticalLarge) ) { BarChart( barChartData = barChartDataModel.barChartData, labelDrawer = barChartDataModel.labelDrawer, yAxisDrawer = SimpleYAxisDrawer(labelValueFormatter = { value -> "your regex here".format( value ) } ) ) } } @Composable private fun DrawLabelLocation( barChartDataModel: BarChartDataModel, newLocation: (DrawLocation) -> Unit ) { val labelDrawLocation = remember(barChartDataModel.labelDrawer) { barChartDataModel.labelLocation } Row( modifier = Modifier .fillMaxWidth() .padding(top = Margin.verticalLarge), verticalAlignment = Alignment.CenterVertically, ) { Row( modifier = Modifier .fillMaxWidth() .padding( horizontal = Margin.horizontal, vertical = Margin.vertical ) .align(Alignment.CenterVertically), horizontalArrangement = Arrangement.SpaceEvenly ) { for (location in DrawLocation.entries) { OutlinedButton( onClick = { newLocation(location) }, border = ButtonDefaults.outlinedButtonBorder.takeIf { labelDrawLocation == location }, ) { Text(text = location.name) } } } } } @Composable private fun AddOrRemoveBar(barChartDataModel: BarChartDataModel) { Row( modifier = Modifier .fillMaxWidth() .padding(vertical = Margin.vertical), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, ) { Button( onClick = { barChartDataModel.removeBar() }, enabled = barChartDataModel.bars.size > 1, shape = CircleShape ) { Icon( imageVector = Icons.Default.Delete, contentDescription = "Remove bar from BarChart" ) } Row( modifier = Modifier.padding(horizontal = Margin.horizontal), verticalAlignment = Alignment.CenterVertically ) { Text(text = "Bars: ") Text( text = barChartDataModel.bars.size.toString(), style = TextStyle( fontWeight = FontWeight.ExtraBold, fontSize = 18.sp ) ) } Button( onClick = { barChartDataModel.addBar() }, enabled = barChartDataModel.bars.size < 7, shape = CircleShape ) { Icon( imageVector = Icons.Default.Add, contentDescription = "Add bar into BarChart" ) } } } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/screen/line/LineChartDataModel.kt ================================================ package me.bytebeats.views.charts.app.ui.screen.line import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import me.bytebeats.views.charts.line.LineChartData import me.bytebeats.views.charts.line.LineChartData.Point import me.bytebeats.views.charts.line.render.point.EmptyPointDrawer import me.bytebeats.views.charts.line.render.point.FilledCircularPointDrawer import me.bytebeats.views.charts.line.render.point.HollowCircularPointDrawer import me.bytebeats.views.charts.line.render.point.IPointDrawer import kotlin.random.Random /** * Created by bytebeats on 2021/9/30 : 17:49 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ class LineChartDataModel { var lineChartData by mutableStateOf( LineChartData( points = listOf( Point(randomYValue(), "Label 1"), Point(randomYValue(), "Label 2"), Point(randomYValue(), "Label 3"), Point(randomYValue(), "Label 4"), Point(randomYValue(), "Label 5"), Point(randomYValue(), "Label 6"), Point(randomYValue(), "Label 7") ) ) ) var horizontalOffset by mutableFloatStateOf(5F) var pointDrawerType by mutableStateOf(PointDrawerType.Hollow) val pointDrawer: IPointDrawer get() { return when (pointDrawerType) { PointDrawerType.None -> EmptyPointDrawer PointDrawerType.Filled -> FilledCircularPointDrawer() PointDrawerType.Hollow -> HollowCircularPointDrawer() } } private fun randomYValue(): Float = Random.Default.nextInt(45, 145).toFloat() } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/screen/line/LineChartScreen.kt ================================================ package me.bytebeats.views.charts.app.ui.screen.line import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import me.bytebeats.views.charts.app.ui.ScreenRouter import me.bytebeats.views.charts.app.ui.theme.Margin import me.bytebeats.views.charts.line.LineChart /** * Created by bytebeats on 2021/9/30 : 17:55 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun LineChartScreen() { Scaffold( topBar = { TopAppBar( navigationIcon = { IconButton( onClick = { ScreenRouter.navigateHome() } ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Go back home" ) } }, title = { Text(text = "Line Chart") } ) } ) { paddingValues -> LineChartContent(Modifier.padding(paddingValues)) } } @Composable private fun LineChartContent( modifier: Modifier = Modifier ) { val lineChartData = LineChartDataModel() Column( modifier = modifier.padding( horizontal = Margin.horizontal, vertical = Margin.vertical ) ) { LineChartRow(lineChartDataModel = lineChartData) HorizontalOffsetSelector(lineChartDataModel = lineChartData) OffsetProgress(lineChartDataModel = lineChartData) } } @Composable private fun LineChartRow(lineChartDataModel: LineChartDataModel) { Box( modifier = Modifier .height(250.dp) .fillMaxSize() ) { LineChart( lineChartData = lineChartDataModel.lineChartData, horizontalOffset = lineChartDataModel.horizontalOffset, pointDrawer = lineChartDataModel.pointDrawer ) } } @Composable private fun HorizontalOffsetSelector(lineChartDataModel: LineChartDataModel) { val pointDrawType = lineChartDataModel.pointDrawerType Column( modifier = Modifier.padding( horizontal = Margin.horizontal, vertical = Margin.vertical ), horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = "Point Drawer") Row( modifier = Modifier .fillMaxWidth() .align(Alignment.CenterHorizontally) .padding( horizontal = Margin.horizontal, vertical = Margin.vertical ), horizontalArrangement = Arrangement.SpaceEvenly ) { for (drawerType in PointDrawerType.entries) { OutlinedButton( onClick = { lineChartDataModel.pointDrawerType = drawerType }, border = ButtonDefaults.outlinedButtonBorder.takeIf { pointDrawType == drawerType }, ) { Text(text = drawerType.name) } } } } } @Composable private fun OffsetProgress(lineChartDataModel: LineChartDataModel) { Column( modifier = Modifier.padding(horizontal = Margin.horizontal), horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = "Offset") Row( modifier = Modifier .fillMaxWidth() .padding( horizontal = Margin.horizontal, vertical = Margin.vertical ) .align(Alignment.CenterHorizontally) ) { Slider( value = lineChartDataModel.horizontalOffset, onValueChange = { lineChartDataModel.horizontalOffset = it }, valueRange = 0F.rangeTo(25F) ) } } } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/screen/line/PointDrawerType.kt ================================================ package me.bytebeats.views.charts.app.ui.screen.line /** * Created by bytebeats on 2021/9/30 : 17:48 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ enum class PointDrawerType { None, Filled, Hollow; } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/screen/pie/PieChartDataModel.kt ================================================ package me.bytebeats.views.charts.app.ui.screen.pie import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import me.bytebeats.views.charts.pie.PieChartData import kotlin.random.Random /** * Created by bytebeats on 2021/9/30 : 12:03 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ class PieChartDataModel { private val colors = mutableListOf( Color(0XFFF44336), Color(0XFFE91E63), Color(0XFF9C27B0), Color(0XFF673AB7), Color(0XFF3F51B5), Color(0XFF03A9F4), Color(0XFF009688), Color(0XFFCDDC39), Color(0XFFFFC107), Color(0XFFFF5722), Color(0XFF795548), Color(0XFF9E9E9E), Color(0XFF607D8B) ) var sliceThickness by mutableFloatStateOf(25F) var pieChartData by mutableStateOf( PieChartData( slices = listOf( PieChartData.Slice( value = randomLength(), color = randomColor() ), PieChartData.Slice( value = randomLength(), color = randomColor() ), PieChartData.Slice( value = randomLength(), color = randomColor() ) ) ) ) val slices get() = pieChartData.slices internal fun addSlice() { pieChartData = pieChartData.copy( slices = slices.toMutableList().apply { add( PieChartData.Slice( value = randomLength(), color = randomColor() ) ) }.toList() ) } internal fun removeSlice() { pieChartData = pieChartData.copy( slices = slices.toMutableList().apply { val lastSlice = slices.last() colors.add(lastSlice.color) remove(lastSlice) }.toList() ) } private fun randomLength(): Float = Random.Default.nextInt(10, 30).toFloat() private fun randomColor(): Color { val randomIndex = Random.Default.nextInt(colors.size) return colors.removeAt(randomIndex) } } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/screen/pie/PieChartScreen.kt ================================================ package me.bytebeats.views.charts.app.ui.screen.pie import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.bytebeats.views.charts.app.ui.ScreenRouter import me.bytebeats.views.charts.app.ui.theme.Margin import me.bytebeats.views.charts.pie.PieChart import me.bytebeats.views.charts.pie.render.SimpleSliceDrawer /** * Created by bytebeats on 2021/9/30 : 15:50 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun PieChartScreen() { Scaffold( topBar = { TopAppBar( navigationIcon = { IconButton( onClick = { ScreenRouter.navigateHome() } ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Go back Home" ) } }, title = { Text(text = "Pie Chart") }) } ) { paddingValues -> PieChartScreenContent(Modifier.padding(paddingValues)) } } @Composable private fun PieChartScreenContent( modifier: Modifier = Modifier ) { val pieChartDataModel = remember { PieChartDataModel() } Column( modifier = modifier.padding( horizontal = Margin.horizontal, vertical = Margin.vertical ) ) { PieChartRow(pieChartDataModel = pieChartDataModel) SliceThicknessRow( sliceThickness = pieChartDataModel.sliceThickness, onValueUpdated = { pieChartDataModel.sliceThickness = it }, ) AddOrRemoveSliceRow(pieChartDataModel = pieChartDataModel) } } @Composable private fun PieChartRow(pieChartDataModel: PieChartDataModel) { Row( modifier = Modifier .fillMaxWidth() .height(150.dp) .padding(vertical = Margin.vertical) ) { PieChart( pieChartData = pieChartDataModel.pieChartData, sliceDrawer = SimpleSliceDrawer(sliceThickness = pieChartDataModel.sliceThickness) ) } } @Composable private fun SliceThicknessRow(sliceThickness: Float, onValueUpdated: (Float) -> Unit) { Row( modifier = Modifier .fillMaxWidth() .padding(vertical = Margin.verticalLarge), verticalAlignment = Alignment.CenterVertically ) { Text( text = "Slice Thickness: ", modifier = Modifier .align(Alignment.CenterVertically) .padding(end = Margin.horizontal) ) Slider( value = sliceThickness, onValueChange = onValueUpdated, valueRange = 10F.rangeTo(100F) ) } } @Composable private fun AddOrRemoveSliceRow(pieChartDataModel: PieChartDataModel) { Row( modifier = Modifier .fillMaxWidth() .padding(top = Margin.vertical), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Button( onClick = { pieChartDataModel.removeSlice() }, enabled = pieChartDataModel.slices.size > 3, shape = CircleShape ) { Icon( imageVector = Icons.Filled.Delete, contentDescription = "Remove slice from PieChart" ) } Row( modifier = Modifier.padding(horizontal = Margin.horizontal), verticalAlignment = Alignment.CenterVertically ) { Text(text = "Slices: ") Text( text = pieChartDataModel.slices.count().toString(), style = TextStyle( fontWeight = FontWeight.ExtraBold, fontSize = 18.sp ) ) } Button( onClick = { pieChartDataModel.addSlice() }, enabled = pieChartDataModel.slices.size < 9, shape = CircleShape ) { Icon( imageVector = Icons.Filled.Add, contentDescription = "Add Slice to PieChart" ) } } } @Preview @Composable private fun PieChartPreview() = PieChartScreen() ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/theme/Color.kt ================================================ package me.bytebeats.views.charts.app.ui.theme import androidx.compose.ui.graphics.Color val Purple200 = Color(0xFFBB86FC) val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) val Teal200 = Color(0xFF03DAC5) ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/theme/Margin.kt ================================================ package me.bytebeats.views.charts.app.ui.theme import androidx.compose.ui.unit.dp /** * Created by bytebeats on 2021/9/30 : 11:52 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ internal object Margin { val horizontal = 15.dp val horizontalLarge = 30.dp val vertical = 15.dp val verticalLarge = 30.dp } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/theme/Shape.kt ================================================ package me.bytebeats.views.charts.app.ui.theme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp val Shapes = Shapes( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(4.dp), large = RoundedCornerShape(0.dp) ) ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/theme/Theme.kt ================================================ package me.bytebeats.views.charts.app.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable private val DarkColorPalette = darkColorScheme( primary = Purple200, primaryContainer = Purple700, secondary = Teal200 ) private val LightColorPalette = lightColorScheme( primary = Purple500, primaryContainer = Purple700, secondary = Teal200 /* Other default colors to override background = Color.White, surface = Color.White, onPrimary = Color.White, onSecondary = Color.Black, onBackground = Color.Black, onSurface = Color.Black, */ ) @Composable fun ComposeChartsTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { val colors = if (darkTheme) { DarkColorPalette } else { LightColorPalette } MaterialTheme( colorScheme = colors, typography = Typography, shapes = Shapes, content = content ) } ================================================ FILE: app/src/main/java/me/bytebeats/views/charts/app/ui/theme/Type.kt ================================================ package me.bytebeats.views.charts.app.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // Set of Material typography styles to start with val Typography = Typography( bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp ) /* Other default text styles to override button = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.W500, fontSize = 14.sp ), caption = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 12.sp ) */ ) ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #FFBB86FC #FF6200EE #FF3700B3 #FF03DAC5 #FF018786 #FF000000 #FFFFFFFF ================================================ FILE: app/src/main/res/values/strings.xml ================================================ compose-charts ================================================ FILE: app/src/main/res/values/themes.xml ================================================ ================================================ FILE: build.gradle.kts ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false alias(libs.plugins.android.library) apply false } ================================================ FILE: charts/.gitignore ================================================ /build/ .idea/ out/ ================================================ FILE: charts/build.gradle.kts ================================================ import com.android.build.gradle.LibraryExtension import org.gradle.jvm.tasks.Jar import org.jetbrains.dokka.gradle.DokkaTask import java.net.URI plugins { alias(libs.plugins.android.library) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.dokka) alias(libs.plugins.detekt.gradle.plugin) id("maven-publish") id("signing") } group = getProperty("GROUP_ID") version = getProperty("COMPOSE_CHARTS_VERSION") android { namespace = "me.bytebeats.views.charts" compileSdk = 34 defaultConfig { minSdk = 24 lint { targetSdk = 34 } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = libs.versions.ktCompilerExt.get() } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } dependencies { implementation(libs.androidx.ui) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) debugImplementation(libs.androidx.ui.tooling) } val sourcesJar by tasks.registering(Jar::class) { archiveClassifier.set("sources") if (project.plugins.hasPlugin(libs.plugins.android.library.get().pluginId)) { val libExt = checkNotNull(project.extensions.findByType(LibraryExtension::class.java)) val libMainSourceSet = libExt.sourceSets.getByName("main") from(libMainSourceSet.java.srcDirs) } else { val sourceSetExt = checkNotNull(project.extensions.findByType(SourceSetContainer::class.java)) val mainSourceSet = sourceSetExt.getByName("main") from(mainSourceSet.java.srcDirs) } } tasks.withType(GenerateModuleMetadata::class).configureEach { dependsOn(sourcesJar) } tasks.dokkaHtml { outputDirectory.set(layout.buildDirectory.dir("dokka")) moduleName.set(getProperty("MODULE_NAME")) dokkaSourceSets { configureEach { suppress = false offlineMode = false includeNonPublic = false skipDeprecated = true skipEmptyPackages = true noStdlibLink = true noJdkLink = true noAndroidSdkLink = false jdkVersion = JavaVersion.VERSION_1_8.ordinal + 1 } } } val dokkaHtml by tasks.getting(DokkaTask::class) val javadocJar by tasks.registering(Jar::class) { dependsOn(dokkaHtml) archiveClassifier.set("javadoc") from(dokkaHtml.outputDirectory) } fun Project.getProperty(key: String?, default: String? = null): String { checkPropertyKey(key) return properties[key]?.toString() ?: System.getProperty(key!!, default) } fun checkPropertyKey(key: String?) { if (key == null) { throw NullPointerException("key can't be null") } if (key.isBlank()) { throw IllegalArgumentException("key can't be blank") } } fun Project.checkSigningKey(signingKey: String?) { checkPropertyKey(signingKey) signingKey?.let { key -> if (hasProperty(key).not() && System.getProperties().containsKey(key).not()) { throw IllegalStateException("$signingKey has to be declared in local.properties or ~/.gradle/gradle.properties") } } } fun Project.getRepoUrl(): URI { val isSnapshot = getProperty("COMPOSE_CHARTS_VERSION").contains("SNAPSHOT") val releaseUrl = getProperty( "RELEASES_REPO_URL", "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" ) val snapshotUrl = getProperty( "SNAPSHOTS_REPO_URL", "https://s01.oss.sonatype.org/content/repositories/snapshots/" ) return uri(if (isSnapshot) snapshotUrl else releaseUrl) } afterEvaluate { publishing { publications { // 1. configure repositories repositories { maven { name = project.getProperty("REPO_NAME") url = project.getRepoUrl() credentials { username = project.getProperty("ossrhUsername", "") password = project.getProperty("ossrhPassword", "") } } } // 2. configure publication val publicationName = project.getProperty("PUBLICATION_NAME", "release") create(publicationName) { if (project.plugins.hasPlugin(libs.plugins.android.library.get().pluginId)) { from(components["release"]) } else { from(components["java"]) } artifact(sourcesJar.get()) artifact(javadocJar.get()) pom { groupId = project.getProperty("GROUP_ID") artifactId = project.getProperty("COMPOSE_CHARTS_ARTIFACT_ID") version = project.getProperty("COMPOSE_CHARTS_VERSION") inceptionYear = project.getProperty("COMPOSE_CHARTS_INCEPTION_YEAR") name = project.getProperty("MODULE_NAME") description = project.getProperty("COMPOSE_CHARTS_DESCRIPTION") url = project.getProperty("COMPOSE_CHARTS_URL") packaging = project.getProperty("COMPOSE_CHARTS_PACKAGING") scm { url = project.getProperty("SCM_URL") connection = project.getProperty("SCM_CONNECTION") developerConnection = project.getProperty("SCM_DEVELOPER_CONNECTION") } organization { name = project.getProperty("ORGANIZATION_NAME", "") url = project.getProperty("ORGANIZATION_URL", "") } developers { developer { id = project.getProperty("DEVELOPER_ID") name = project.getProperty("DEVELOPER_NAME") url = project.getProperty("DEVELOPER_URL") email = project.getProperty("DEVELOPER_EMAIL") } } licenses { license { name = project.getProperty("LICENSE_NAME") url = project.getProperty("LICENSE_URL") distribution = project.getProperty("LICENCE_DIST") } } issueManagement { system = project.getProperty("ISSUE_SYSTEM") url = project.getProperty("ISSUE_URL") } contributors { contributor { name = project.getProperty("CONTRIBUTOR_NAME") email = project.getProperty("CONTRIBUTOR_EMAIL") url = project.getProperty("CONTRIBUTOR_URL") roles.set(listOf("Master", "Maintainer", "Developer")) timezone = project.getProperty("CONTRIBUTOR_TIMEZONE") } } ciManagement { system = project.getProperty("CI_SYSTEM") url = project.getProperty("CI_URL") } distributionManagement { downloadUrl = getProperty("RELEASES_REPO_URL") } } } // 3. sign the artifacts signing { // Choose one of both ways to sign the aar // Signing with gpg // and with its signing.keyId & signing.password & signing.secretKeyRingFile // declared in local.properties or ~/.gradle/gradle.properties // checkSigningKey("signing.keyId") // checkSigningKey("signing.password") // checkSigningKey("signing.secretKeyRingFile") sign(publishing.publications.getByName(publicationName)) // or signing with CI/CD // and with its signingKeyId & signingKeyPassword & signingKey // declared in local.properties or ~/.gradle/gradle.properties // checkSigningKey("signingKeyId") // checkSigningKey("signingKey") // checkSigningKey("signingKeyPassword") // val signingKeyId = getProperty("signingKeyId") // val signingKey = getProperty("signingKey") // val signingKeyPassword = getProperty("signingKeyPassword") // useInMemoryPgpKeys(signingKeyId, signingKey, signingKeyPassword) // sign(publishing.publications.getByName(publicationName)) } } } } ================================================ FILE: charts/consumer-rules.pro ================================================ ================================================ FILE: charts/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: charts/src/main/AndroidManifest.xml ================================================ ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/Animations.kt ================================================ package me.bytebeats.views.charts import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.TweenSpec /** * Created by bytebeats on 2021/9/24 : 10:53 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ fun simpleChartAnimation(): AnimationSpec = TweenSpec(durationMillis = 500) ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/AxisLine.kt ================================================ package me.bytebeats.views.charts import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** * Created by bytebeats on 2021/9/24 : 10:47 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class AxisLine( val thickness: Dp = 1.5.dp, val color: Color = Color.Gray ) { private val mPaint by lazy { Paint().apply { color = this@AxisLine.color style = PaintingStyle.Stroke } } @Suppress("UndocumentedPublicFunction") fun paint(density: Density) { mPaint.strokeWidth = thickness.value * density.density } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/Colors.kt ================================================ package me.bytebeats.views.charts import androidx.compose.ui.graphics.Color import me.bytebeats.views.charts.util.FLOAT_0_5 import me.bytebeats.views.charts.util.FLOAT_255 /** * Created by bytebeats on 2021/9/24 : 10:51 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ fun Color.toLegacyInt(): Int { return android.graphics.Color.argb( (alpha * FLOAT_255 + FLOAT_0_5).toInt(), (red * FLOAT_255 + FLOAT_0_5).toInt(), (green * FLOAT_255 + FLOAT_0_5).toInt(), (blue * FLOAT_255 + FLOAT_0_5).toInt() ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/TypeAlias.kt ================================================ package me.bytebeats.views.charts /** * Created by bytebeats on 2021/9/25 : 15:37 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ typealias LabelFormatter = (value: Float) -> String typealias AxisLabelFormatter = (value: Any?) -> String ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/BarChart.kt ================================================ package me.bytebeats.views.charts.bar import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import me.bytebeats.views.charts.bar.render.bar.IBarDrawer import me.bytebeats.views.charts.bar.render.bar.SimpleBarDrawer import me.bytebeats.views.charts.bar.render.label.ILabelDrawer import me.bytebeats.views.charts.bar.render.label.SimpleLabelDrawer import me.bytebeats.views.charts.bar.render.xaxis.IXAxisDrawer import me.bytebeats.views.charts.bar.render.xaxis.SimpleXAxisDrawer import me.bytebeats.views.charts.bar.render.yaxis.IYAxisDrawer import me.bytebeats.views.charts.bar.render.yaxis.SimpleYAxisDrawer import me.bytebeats.views.charts.simpleChartAnimation /** * Created by bytebeats on 2021/9/25 : 15:56 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ @Composable fun BarChart( barChartData: BarChartData, modifier: Modifier = Modifier, animation: AnimationSpec = simpleChartAnimation(), barDrawer: IBarDrawer = SimpleBarDrawer(), xAxisDrawer: IXAxisDrawer = SimpleXAxisDrawer(), yAxisDrawer: IYAxisDrawer = SimpleYAxisDrawer(), labelDrawer: ILabelDrawer = SimpleLabelDrawer() ) { val transitionAnimation = remember(barChartData.bars) { Animatable(initialValue = 0F) } LaunchedEffect(barChartData.bars) { transitionAnimation.animateTo(1F, animationSpec = animation) } val progress = transitionAnimation.value Canvas( modifier = modifier .fillMaxSize() .drawBehind { drawIntoCanvas { canvas -> val (xAxisArea, yAxisArea) = axisAreas( drawScope = this, totalSize = size, xAxisDrawer = xAxisDrawer, labelDrawer = labelDrawer ) val barDrawableArea = barDrawableArea(xAxisArea) yAxisDrawer.drawAxisLine( drawScope = this, canvas = canvas, drawableArea = yAxisArea ) xAxisDrawer.drawXAxisLine( drawScope = this, canvas = canvas, drawableArea = xAxisArea ) barChartData.forEachWithArea( this, barDrawableArea, progress, labelDrawer ) { barArea, bar -> barDrawer.drawBar(drawScope = this, canvas, barArea, bar) } } } ) { drawIntoCanvas { canvas -> val (xAxisArea, yAxisArea) = axisAreas( drawScope = this, totalSize = size, xAxisDrawer = xAxisDrawer, labelDrawer = labelDrawer ) val barDrawableArea = barDrawableArea(xAxisArea) barChartData.forEachWithArea( this, barDrawableArea, progress, labelDrawer ) { barArea, bar -> labelDrawer.drawLabel( drawScope = this, canvas = canvas, label = bar.label, barArea = barArea, xAxisArea = xAxisArea ) } yAxisDrawer.drawAxisLabels( drawScope = this, canvas = canvas, minValue = barChartData.minY, maxValue = barChartData.maxY, drawableArea = yAxisArea ) } } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/BarChartData.kt ================================================ package me.bytebeats.views.charts.bar import androidx.compose.ui.graphics.Color import me.bytebeats.views.charts.util.FLOAT_100 /** * Created by bytebeats on 2021/9/25 : 13:52 E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class BarChartData( val bars: List, val padBy: Float = 10F, val startAtZero: Boolean = true, val maxBarValue: Float = bars.maxOf { it.value } ) { init { require(padBy in 0F..FLOAT_100) { "padBy must be between 0F and 100F, included" } require(maxBarValue >= bars.maxOf { it.value }) { "maxBarValue must be at least the value of the highest bar" } } private val yMinMaxValues: Pair get() { val minValue = bars.minOf { it.value } val maxValue = maxBarValue return minValue to maxValue } val maxY: Float get() = yMinMaxValues.second + (yMinMaxValues.second - yMinMaxValues.first) * padBy / FLOAT_100 val minY: Float get() = if (startAtZero) 0F else yMinMaxValues.first - (yMinMaxValues.second - yMinMaxValues.first) * padBy / FLOAT_100 data class Bar( val value: Float, val color: Color, val label: String ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/BarCharts.kt ================================================ package me.bytebeats.views.charts.bar import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.dp import me.bytebeats.views.charts.bar.render.label.ILabelDrawer import me.bytebeats.views.charts.bar.render.xaxis.IXAxisDrawer import me.bytebeats.views.charts.util.FLOAT_10 import me.bytebeats.views.charts.util.FLOAT_100 /** * Created by bytebeats on 2021/9/25 : 13:57 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ internal fun axisAreas( drawScope: DrawScope, totalSize: Size, xAxisDrawer: IXAxisDrawer, labelDrawer: ILabelDrawer ): Pair { with(drawScope) { val yAxisTop = labelDrawer.requiredAboveBarHeight(drawScope) val yAxisRight = 50.dp.toPx().coerceAtMost(size.width * FLOAT_10 / FLOAT_100) val xAxisRight = totalSize.width val xAxisTop = totalSize.height - xAxisDrawer.requiredHeight(drawScope) return Rect( left = yAxisRight, top = xAxisTop, right = xAxisRight, bottom = totalSize.height ) to Rect( left = 0F, top = yAxisTop, right = yAxisRight, bottom = xAxisTop ) } } internal fun barDrawableArea(xAxisArea: Rect): Rect = Rect( left = xAxisArea.left, top = 0F, right = xAxisArea.right, bottom = xAxisArea.top ) internal fun BarChartData.forEachWithArea( drawScope: DrawScope, barDrawableArea: Rect, progress: Float, labelDrawer: ILabelDrawer, block: (barArea: Rect, bar: BarChartData.Bar) -> Unit ) { val barCount = bars.size val widthOfBarArea = barDrawableArea.width / barCount val offsetOfBar = widthOfBarArea * 0.2F bars.forEachIndexed { index, bar -> val left = barDrawableArea.left + index * widthOfBarArea val height = barDrawableArea.height val barHeight = (height - labelDrawer.requiredAboveBarHeight(drawScope)) * progress val barArea = Rect( left = left + offsetOfBar, top = barDrawableArea.bottom - bar.value / maxBarValue * barHeight, right = left + widthOfBarArea - offsetOfBar, bottom = barDrawableArea.bottom ) block(barArea, bar) } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/render/bar/IBarDrawer.kt ================================================ package me.bytebeats.views.charts.bar.render.bar import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope import me.bytebeats.views.charts.bar.BarChartData /** * Created by bytebeats on 2021/9/25 : 15:53 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface IBarDrawer { /** * Draw bar * * @param drawScope the scope to draw * @param canvas the Canvas to draw on * @param barArea the bar area to draw * @param bar the bar data */ fun drawBar( drawScope: DrawScope, canvas: Canvas, barArea: Rect, bar: BarChartData.Bar ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/render/bar/SimpleBarDrawer.kt ================================================ package me.bytebeats.views.charts.bar.render.bar import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.drawscope.DrawScope import me.bytebeats.views.charts.bar.BarChartData /** * Created by bytebeats on 2021/9/25 : 15:54 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ class SimpleBarDrawer : IBarDrawer { private val mBarPaint by lazy { Paint().apply { isAntiAlias = true } } override fun drawBar( drawScope: DrawScope, canvas: Canvas, barArea: Rect, bar: BarChartData.Bar ) { canvas.drawRect(barArea, mBarPaint.apply { color = bar.color }) } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/render/label/ILabelDrawer.kt ================================================ package me.bytebeats.views.charts.bar.render.label import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/25 : 13:59 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface ILabelDrawer { /** * Required x axis height * * @param drawScope the scope to draw in * @return required height */ fun requiredXAxisHeight(drawScope: DrawScope): Float = 0F /** * Required above bar height * * @param drawScope the scope to draw in * @return required height */ fun requiredAboveBarHeight(drawScope: DrawScope): Float = 0F /** * Draw label * * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param label the label to draw on the axis * @param barArea the area to draw a bar * @param xAxisArea the x axis area to draw */ fun drawLabel( drawScope: DrawScope, canvas: Canvas, label: Any?, barArea: Rect, xAxisArea: Rect ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/render/label/SimpleLabelDrawer.kt ================================================ package me.bytebeats.views.charts.bar.render.label import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import me.bytebeats.views.charts.AxisLabelFormatter import me.bytebeats.views.charts.toLegacyInt import me.bytebeats.views.charts.util.FLOAT_1_5 /** * Created by bytebeats on 2021/9/25 : 14:01 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class SimpleLabelDrawer( val drawLocation: DrawLocation = DrawLocation.Inside, val labelTextSize: TextUnit = 12.sp, val labelTextColor: Color = Color.Black, val axisLabelFormatter: AxisLabelFormatter = { value -> "$value" } ) : ILabelDrawer { private val mLabelTextArea: Float? = null private val mPaint by lazy { android.graphics.Paint().apply { textAlign = android.graphics.Paint.Align.CENTER color = labelTextColor.toLegacyInt() } } override fun requiredAboveBarHeight(drawScope: DrawScope): Float = when (drawLocation) { DrawLocation.Outside -> FLOAT_1_5 * labelTextHeight(drawScope) else -> 0F } override fun requiredXAxisHeight(drawScope: DrawScope): Float = when (drawLocation) { DrawLocation.XAxis -> labelTextHeight(drawScope) else -> 0F } override fun drawLabel( drawScope: DrawScope, canvas: Canvas, label: Any?, barArea: Rect, xAxisArea: Rect ) { with(drawScope) { val xCenter = barArea.left + barArea.width / 2 val yCenter = when (drawLocation) { DrawLocation.Inside -> (barArea.top + barArea.bottom) / 2 DrawLocation.Outside -> barArea.top - labelTextSize.toPx() / 2 DrawLocation.XAxis -> barArea.bottom + labelTextHeight(drawScope) } val labelValue = axisLabelFormatter(label) canvas.nativeCanvas.drawText(labelValue, xCenter, yCenter, paint(drawScope)) } } private fun labelTextHeight(drawScope: DrawScope): Float = with(drawScope) { mLabelTextArea ?: (FLOAT_1_5 * labelTextSize.toPx()) } private fun paint(drawScope: DrawScope): android.graphics.Paint = with(drawScope) { mPaint.apply { textSize = labelTextSize.toPx() } } enum class DrawLocation { Inside, Outside, XAxis; } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/render/xaxis/IXAxisDrawer.kt ================================================ package me.bytebeats.views.charts.bar.render.xaxis import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/25 : 14:16 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface IXAxisDrawer { /** * Required height * * @param drawScope the draw scope to require height * @return required height */ fun requiredHeight(drawScope: DrawScope): Float /** * Draw x axis line * * @param drawScope the scope to draw * @param canvas the canvas to draw on * @param drawableArea the area to draw a drawable */ fun drawXAxisLine( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/render/xaxis/SimpleXAxisDrawer.kt ================================================ package me.bytebeats.views.charts.bar.render.xaxis import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import me.bytebeats.views.charts.util.FLOAT_1_5 /** * Created by bytebeats on 2021/9/25 : 14:18 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class SimpleXAxisDrawer( val axisLineThickness: Dp = 1.dp, val axisLineColor: Color = Color.Black ) : IXAxisDrawer { private val mPaint by lazy { Paint().apply { isAntiAlias = true color = axisLineColor style = PaintingStyle.Stroke } } override fun requiredHeight(drawScope: DrawScope): Float = with(drawScope) { FLOAT_1_5 * axisLineThickness.toPx() } override fun drawXAxisLine( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect ) { with(drawScope) { val lineThickness = axisLineThickness.toPx() val y = drawableArea.top + lineThickness / 2F canvas.drawLine( p1 = Offset(x = drawableArea.left, y = y), p2 = Offset(x = drawableArea.right, y = y), paint = mPaint.apply { strokeWidth = lineThickness }) } } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/render/yaxis/IYAxisDrawer.kt ================================================ package me.bytebeats.views.charts.bar.render.yaxis import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/25 : 14:26 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface IYAxisDrawer { /** * Draw axis line * * @param drawScope the scope to draw * @param canvas the canvas to draw on * @param drawableArea the area to draw a drawable */ fun drawAxisLine( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect ) /** * Draw axis labels * * @param drawScope the scope to draw * @param canvas the canvas to draw on * @param drawableArea the drawable area * @param minValue the min value of the y axis data * @param maxValue the max value of the y axis data */ fun drawAxisLabels( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect, minValue: Float, maxValue: Float ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/bar/render/yaxis/SimpleYAxisDrawer.kt ================================================ package me.bytebeats.views.charts.bar.render.yaxis import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.bytebeats.views.charts.LabelFormatter import me.bytebeats.views.charts.toLegacyInt import kotlin.math.roundToInt /** * Created by bytebeats on 2021/9/25 : 14:27 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class SimpleYAxisDrawer( val labelTextSize: TextUnit = 12.sp, val labelTextColor: Color = Color.Black, val drawLabelEvery: Int = 3, val labelValueFormatter: LabelFormatter = { value -> "%.1f".format(value) }, val axisLineThickness: Dp = 1.dp, val axisLineColor: Color = Color.Black ) : IYAxisDrawer { private val mAxisLinePaint by lazy { Paint().apply { isAntiAlias = true color = axisLineColor style = PaintingStyle.Stroke } } private val mTextPaint by lazy { android.graphics.Paint().apply { isAntiAlias = true color = labelTextColor.toLegacyInt() } } private val mTextBounds = android.graphics.Rect() override fun drawAxisLine( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect ) { with(drawScope) { val lineThickness = axisLineThickness.toPx() val x = drawableArea.right - lineThickness / 2F canvas.drawLine( p1 = Offset(x = x, y = drawableArea.top), p2 = Offset(x = x, y = drawableArea.bottom), paint = mAxisLinePaint.apply { strokeWidth = lineThickness } ) } } override fun drawAxisLabels( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect, minValue: Float, maxValue: Float ) { with(drawScope) { val labelPaint = mTextPaint.apply { textSize = labelTextSize.toPx() textAlign = android.graphics.Paint.Align.RIGHT } val minLabelHeight = labelTextSize.toPx() * drawLabelEvery.toFloat() val totalHeight = drawableArea.height val labelCount = (drawableArea.height / minLabelHeight).roundToInt().coerceAtLeast(2) for (i in 0..labelCount) { val value = minValue + i * (maxValue - minValue) / labelCount val label = labelValueFormatter(value) val x = drawableArea.right - axisLineThickness.toPx() - labelTextSize.toPx() / 2F labelPaint.getTextBounds(label, 0, label.length, mTextBounds) val y = drawableArea.bottom - i * (totalHeight / labelCount) + mTextBounds.height() / 2F canvas.nativeCanvas.drawText(label, x, y, labelPaint) } } } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/LineChart.kt ================================================ package me.bytebeats.views.charts.line import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import me.bytebeats.views.charts.line.render.line.EmptyLineShader import me.bytebeats.views.charts.line.render.line.ILineDrawer import me.bytebeats.views.charts.line.render.line.ILineShader import me.bytebeats.views.charts.line.render.line.SolidLineDrawer import me.bytebeats.views.charts.line.render.point.FilledCircularPointDrawer import me.bytebeats.views.charts.line.render.point.IPointDrawer import me.bytebeats.views.charts.line.render.xaxis.IXAxisDrawer import me.bytebeats.views.charts.line.render.xaxis.SimpleXAxisDrawer import me.bytebeats.views.charts.line.render.yaxis.IYAxisDrawer import me.bytebeats.views.charts.line.render.yaxis.SimpleYAxisDrawer import me.bytebeats.views.charts.simpleChartAnimation /** * Created by bytebeats on 2021/9/25 : 12:55 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ @Composable fun LineChart( lineChartData: LineChartData, modifier: Modifier = Modifier, animation: AnimationSpec = simpleChartAnimation(), pointDrawer: IPointDrawer = FilledCircularPointDrawer(), lineDrawer: ILineDrawer = SolidLineDrawer(), lineShader: ILineShader = EmptyLineShader, xAxisDrawer: IXAxisDrawer = SimpleXAxisDrawer(), yAxisDrawer: IYAxisDrawer = SimpleYAxisDrawer(), horizontalOffset: Float = 5F, ) { check(horizontalOffset in 0F..25F) { "Horizontal Offset is the percentage offset from side, and must be between 0 and 25, included." } val transitionAnimation = remember(lineChartData.points) { Animatable(initialValue = 0F) } LaunchedEffect(lineChartData.points) { transitionAnimation.snapTo(0F) transitionAnimation.animateTo(1F, animationSpec = animation) } Canvas( modifier = modifier.fillMaxSize() ) { drawIntoCanvas { canvas -> val yAxisDrawableArea = computeYAxisDrawableArea( xAxisLabelSize = xAxisDrawer.requireHeight(this), size = size ) val xAxisDrawableArea = computeXAxisDrawableArea( yAxisWidth = yAxisDrawableArea.width, labelHeight = xAxisDrawer.requireHeight(this), size = size ) val xAxisLabelsDrawableArea = computeXAxisLabelsDrawableArea( xAxisDrawableArea = xAxisDrawableArea, offset = horizontalOffset ) val chartDrawableArea = computeDrawableArea( xAxisDrawableArea = xAxisDrawableArea, yAxisDrawableArea = yAxisDrawableArea, size = size, offset = horizontalOffset ) lineDrawer.drawLine( drawScope = this, canvas = canvas, linePath = computeLinePath( drawableArea = chartDrawableArea, lineChartData = lineChartData, transitionProgress = transitionAnimation.value ) ) lineShader.fillLine( drawScope = this, canvas = canvas, fillPath = computeFillPath( drawableArea = chartDrawableArea, lineChartData = lineChartData, transitionProgress = transitionAnimation.value ) ) lineChartData.points.forEachIndexed { index, point -> withProgress( index = index, lineChartData = lineChartData, transitionProgress = transitionAnimation.value ) { pointDrawer.drawPoint( drawScope = this, canvas = canvas, center = computePointLocation( drawableArea = chartDrawableArea, lineChartData = lineChartData, point = point, index = index ) ) } } xAxisDrawer.drawXAxisLine( drawScope = this, drawableArea = xAxisDrawableArea, canvas = canvas ) xAxisDrawer.drawXAxisLabels( drawScope = this, canvas = canvas, drawableArea = xAxisLabelsDrawableArea, labels = lineChartData.points.map { it.label }) yAxisDrawer.drawAxisLine( drawScope = this, drawableArea = yAxisDrawableArea, canvas = canvas ) yAxisDrawer.drawAxisLabels( drawScope = this, canvas = canvas, drawableArea = yAxisDrawableArea, minValue = lineChartData.minY, maxValue = lineChartData.maxY ) } } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/LineChartData.kt ================================================ package me.bytebeats.views.charts.line import me.bytebeats.views.charts.util.FLOAT_100 /** * Created by bytebeats on 2021/9/24 : 19:39 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class LineChartData( val points: List, val padBy: Float = 20F,// percentage we pad yValue by val startAtZero: Boolean = false ) { init { require(padBy in 0F..FLOAT_100) { "padBy must be between 0F and 100F, included" } } private val yMinMaxValues: Pair get() { val minValue = points.minOf { it.value } val maxValue = points.maxOf { it.value } return minValue to maxValue } val maxY: Float get() = yMinMaxValues.second + (yMinMaxValues.second - yMinMaxValues.first) * padBy / FLOAT_100 val minY: Float get() = if (startAtZero) 0F else yMinMaxValues.first - (yMinMaxValues.second - yMinMaxValues.first) * padBy / FLOAT_100 val yRange: Float get() = maxY - minY data class Point( val value: Float, val label: String ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/LineCharts.kt ================================================ package me.bytebeats.views.charts.line import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Path import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import me.bytebeats.views.charts.util.FLOAT_10 import me.bytebeats.views.charts.util.FLOAT_100 /** * Created by bytebeats on 2021/9/24 : 19:26 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ internal fun computeDrawableArea( xAxisDrawableArea: Rect, yAxisDrawableArea: Rect, size: Size, offset: Float ): Rect { val horizontalOffset = xAxisDrawableArea.width * offset / FLOAT_100 return Rect( left = yAxisDrawableArea.right + horizontalOffset, top = 0F, bottom = xAxisDrawableArea.top, right = size.width - horizontalOffset ) } internal fun computeXAxisDrawableArea( yAxisWidth: Float, labelHeight: Float, size: Size ): Rect { val top = size.height - labelHeight return Rect( left = yAxisWidth, top = top, right = size.width, bottom = size.height ) } internal fun computeXAxisLabelsDrawableArea( xAxisDrawableArea: Rect, offset: Float ): Rect { val horizontalOffset = xAxisDrawableArea.width * offset / FLOAT_100 return Rect( left = xAxisDrawableArea.left + horizontalOffset, top = xAxisDrawableArea.top, right = xAxisDrawableArea.right - horizontalOffset, bottom = xAxisDrawableArea.bottom ) } internal fun Density.computeYAxisDrawableArea( xAxisLabelSize: Float, size: Size ): Rect { val right = 50.dp.toPx().coerceAtMost(size.width * FLOAT_10 / FLOAT_100) // 50dp or 10% of chart view width return Rect( left = 0F, top = 0F, right = right, bottom = size.height - xAxisLabelSize ) } internal fun computePointLocation( drawableArea: Rect, lineChartData: LineChartData, point: LineChartData.Point, index: Int ): Offset { val dx = index.toFloat() / (lineChartData.points.size - 1) val dy = (point.value - lineChartData.minY) / lineChartData.yRange return Offset( x = dx * drawableArea.width + drawableArea.left, y = drawableArea.height - dy * drawableArea.height ) } internal fun withProgress( index: Int, lineChartData: LineChartData, transitionProgress: Float, progressListener: (progress: Float) -> Unit ) { val size = lineChartData.points.size val toIndex = (size * transitionProgress).toInt() + 1 if (index == toIndex) { val sizeF = lineChartData.points.size.toFloat() val divider = 1F / sizeF val down = (index - 1) * divider progressListener((transitionProgress - down) / divider) } else if (index < toIndex) { progressListener(1F) } } internal fun computeLinePath( drawableArea: Rect, lineChartData: LineChartData, transitionProgress: Float ): Path = Path().apply { var prePointLocation: Offset? = null lineChartData.points.forEachIndexed { index, point -> withProgress(index, lineChartData, transitionProgress) { progress -> val pointLocation = computePointLocation(drawableArea, lineChartData, point, index) if (index == 0) { moveTo(pointLocation.x, pointLocation.y) } else { if (progress <= 1F) { val preX = prePointLocation?.x ?: 0F val preY = prePointLocation?.y ?: 0F val tx = (pointLocation.x - preX) * progress + preX val ty = (pointLocation.y - preY) * progress + preY lineTo(tx, ty) } else { lineTo(pointLocation.x, pointLocation.y) } } prePointLocation = pointLocation } } } internal fun computeFillPath( drawableArea: Rect, lineChartData: LineChartData, transitionProgress: Float ): Path = Path().apply { moveTo(drawableArea.left, drawableArea.bottom) var prePointX: Float? = null var prePointLocation: Offset? = null lineChartData.points.forEachIndexed { index, point -> withProgress(index, lineChartData, transitionProgress) { progress -> val pointLocation = computePointLocation(drawableArea, lineChartData, point, index) if (index == 0) { lineTo(drawableArea.left, pointLocation.y) lineTo(pointLocation.x, pointLocation.y) } else { prePointX = if (progress <= 1F) { val preX = prePointLocation?.x ?: 0F val preY = prePointLocation?.y ?: 0F val tx = (pointLocation.x - preX) * progress + preX val ty = (pointLocation.y - preY) * progress + preY lineTo(tx, ty) tx } else { lineTo(pointLocation.x, pointLocation.y) pointLocation.x } } prePointLocation = pointLocation } } prePointX?.let { lineTo(it, drawableArea.bottom) lineTo(drawableArea.left, drawableArea.bottom) } ?: lineTo(drawableArea.left, drawableArea.bottom) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/line/EmptyLineShader.kt ================================================ package me.bytebeats.views.charts.line.render.line import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/25 : 12:50 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ object EmptyLineShader : ILineShader { override fun fillLine( drawScope: DrawScope, canvas: Canvas, fillPath: Path ) { // do nothing here } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/line/GradientLineShader.kt ================================================ package me.bytebeats.views.charts.line.render.line import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/25 : 12:52 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class GradientLineShader( val colors: List = listOf(Color.Blue, Color.Transparent) ) : ILineShader { private val mBrush = Brush.verticalGradient(colors) override fun fillLine( drawScope: DrawScope, canvas: Canvas, fillPath: Path ) { drawScope.drawPath(path = fillPath, brush = mBrush) } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/line/ILineDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.line import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/25 : 12:45 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface ILineDrawer { /** * Draw line * * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param linePath the line path to draw */ fun drawLine( drawScope: DrawScope, canvas: Canvas, linePath: Path ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/line/ILineShader.kt ================================================ package me.bytebeats.views.charts.line.render.line import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/25 : 12:49 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface ILineShader { /** * fill slice * * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param fillPath the path to fill */ fun fillLine( drawScope: DrawScope, canvas: Canvas, fillPath: Path ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/line/SolidLineDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.line import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** * Created by bytebeats on 2021/9/25 : 12:46 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class SolidLineDrawer( val thickness: Dp = 3.dp, val color: Color = Color.Cyan ) : ILineDrawer { private val mPaint by lazy { Paint().apply { color = this@SolidLineDrawer.color style = PaintingStyle.Stroke isAntiAlias = true } } override fun drawLine( drawScope: DrawScope, canvas: Canvas, linePath: Path ) { val lineThickness = with(drawScope) { thickness.toPx() } canvas.drawPath( path = linePath, paint = mPaint.apply { strokeWidth = lineThickness } ) } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/line/SolidLineShader.kt ================================================ package me.bytebeats.views.charts.line.render.line import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/25 : 12:51 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class SolidLineShader( val color: Color = Color.Blue ) : ILineShader { override fun fillLine( drawScope: DrawScope, canvas: Canvas, fillPath: Path ) { drawScope.drawPath( path = fillPath, color = color ) } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/point/EmptyPointDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.point import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/24 : 20:24 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ object EmptyPointDrawer : IPointDrawer { override fun drawPoint( drawScope: DrawScope, canvas: Canvas, center: Offset ) { //empty point, we do nothing here. } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/point/FilledCircularPointDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.point import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** * Created by bytebeats on 2021/9/24 : 20:34 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class FilledCircularPointDrawer( val diameter: Dp = 8.dp, val color: Color = Color.Blue ) : IPointDrawer { private val mPaint by lazy { Paint().apply { color = this@FilledCircularPointDrawer.color style = PaintingStyle.Fill isAntiAlias = true } } override fun drawPoint( drawScope: DrawScope, canvas: Canvas, center: Offset ) { with(drawScope as Density) { canvas.drawCircle(center, diameter.toPx() / 2F, paint = mPaint) } } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/point/HollowCircularPointDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.point import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** * Created by bytebeats on 2021/9/24 : 20:38 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class HollowCircularPointDrawer( val diameter: Dp = 8.dp, val lineThickness: Dp = 2.dp, val color: Color = Color.Blue ) : IPointDrawer { private val mPaint by lazy { Paint().apply { color = this@HollowCircularPointDrawer.color style = PaintingStyle.Stroke isAntiAlias = true } } override fun drawPoint( drawScope: DrawScope, canvas: Canvas, center: Offset ) { with(drawScope as Density) { canvas.drawCircle( center = center, radius = diameter.toPx() / 2F, paint = mPaint.apply { strokeWidth = lineThickness.toPx() }) } } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/point/IPointDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.point import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/24 : 20:22 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface IPointDrawer { /** * Draw point * * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param center the center point to draw a point */ fun drawPoint( drawScope: DrawScope, canvas: Canvas, center: Offset ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/xaxis/IXAxisDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.xaxis import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/24 : 20:45 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface IXAxisDrawer { /** * require height * * @param drawScope the scope to draw in */ fun requireHeight(drawScope: DrawScope): Float /** * Draw x axis line * * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param drawableArea the area to draw a drawable */ fun drawXAxisLine( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect ) /** * Draw labels in x axis * * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param drawableArea the area to draw * @param labels the labels to draw on axis */ fun drawXAxisLabels( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect, labels: List<*> ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/xaxis/SimpleXAxisDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.xaxis import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.bytebeats.views.charts.AxisLabelFormatter import me.bytebeats.views.charts.toLegacyInt import me.bytebeats.views.charts.util.FLOAT_1_5 /** * Created by bytebeats on 2021/9/24 : 20:50 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class SimpleXAxisDrawer( val labelTextSize: TextUnit = 12.sp, val labelTextColor: Color = Color.Black, val drawLabelEvery: Int = 1,// draw label text every $drawLabelEvery, like 1, 2, 3 and so on. val axisLineThickness: Dp = 1.dp, val axisLineColor: Color = Color.Black, val axisLabelFormatter: AxisLabelFormatter = { value -> "$value" } ) : IXAxisDrawer { private val mAxisLinePaint by lazy { Paint().apply { isAntiAlias = true color = axisLineColor style = PaintingStyle.Stroke } } private val mTextPaint by lazy { android.graphics.Paint().apply { isAntiAlias = true color = labelTextColor.toLegacyInt() } } override fun requireHeight(drawScope: DrawScope): Float = with(drawScope) { FLOAT_1_5 * (labelTextSize.toPx() + axisLineThickness.toPx()) } override fun drawXAxisLine( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect ) { with(drawScope) { val lineThickness = axisLineThickness.toPx() val y = drawableArea.top + lineThickness / 2F canvas.drawLine( p1 = Offset(x = drawableArea.left, y = y), p2 = Offset(x = drawableArea.right, y = y), paint = mAxisLinePaint.apply { strokeWidth = lineThickness }) } } override fun drawXAxisLabels( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect, labels: List<*> ) { with(drawScope) { val labelPaint = mTextPaint.apply { textSize = labelTextSize.toPx() textAlign = android.graphics.Paint.Align.CENTER } val labelIncrements = drawableArea.width / (labels.size - 1) labels.forEachIndexed { index, label -> if (index.rem(drawLabelEvery) == 0) { val labelValue = axisLabelFormatter(label) val x = drawableArea.left + labelIncrements * index val y = drawableArea.bottom canvas.nativeCanvas.drawText(labelValue, x, y, labelPaint) } } } } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/yaxis/IYAxisDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.yaxis import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope /** * Created by bytebeats on 2021/9/24 : 21:06 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface IYAxisDrawer { /** * Draw axis line * * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param drawableArea the area to draw a drawable */ fun drawAxisLine( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect ) /** * Draw axis labels * * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param drawableArea the area to draw * @param minValue the min value of the y axis data * @param maxValue the max value of the y axis data */ fun drawAxisLabels( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect, minValue: Float, maxValue: Float ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/line/render/yaxis/SimpleYAxisDrawer.kt ================================================ package me.bytebeats.views.charts.line.render.yaxis import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import me.bytebeats.views.charts.LabelFormatter import me.bytebeats.views.charts.toLegacyInt import kotlin.math.roundToInt /** * Created by bytebeats on 2021/9/24 : 21:08 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class SimpleYAxisDrawer( val labelTextSize: TextUnit = 12.sp, val labelTextColor: Color = Color.Black, val drawLabelEvery: Int = 1, val labelValueFormatter: LabelFormatter = { value -> "%.1f".format(value) }, val axisLineThickness: Dp = 1.dp, val axisLineColor: Color = Color.Black, ) : IYAxisDrawer { private val mAxisLinePaint by lazy { Paint().apply { isAntiAlias = true color = axisLineColor style = PaintingStyle.Stroke } } private val mTextPaint by lazy { android.graphics.Paint().apply { isAntiAlias = true color = labelTextColor.toLegacyInt() } } private val mTextBounds = android.graphics.Rect() override fun drawAxisLine( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect ) = with(drawScope) { val lineThickness = axisLineThickness.toPx() val x = drawableArea.right - lineThickness / 2F canvas.drawLine( p1 = Offset(x = x, y = drawableArea.top), p2 = Offset(x = x, y = drawableArea.bottom), paint = mAxisLinePaint.apply { strokeWidth = lineThickness }) } override fun drawAxisLabels( drawScope: DrawScope, canvas: Canvas, drawableArea: Rect, minValue: Float, maxValue: Float ) { with(drawScope) { val labelPaint = mTextPaint.apply { textSize = labelTextSize.toPx() textAlign = android.graphics.Paint.Align.RIGHT } val minLabelHeight = labelTextSize.toPx() * drawLabelEvery.toFloat() val totalHeight = drawableArea.height val labelCount = (drawableArea.height / minLabelHeight).roundToInt().coerceAtLeast(2) for (i in 0..labelCount) { val value = minValue + i * (maxValue - minValue) / labelCount val label = labelValueFormatter(value) val x = drawableArea.right - axisLineThickness.toPx() - labelTextSize.toPx() / 2F labelPaint.getTextBounds(label, 0, label.length, mTextBounds) val y = drawableArea.bottom - i * (totalHeight / labelCount) - mTextBounds.height() / 2F canvas.nativeCanvas.drawText(label, x, y, labelPaint) } } } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/pie/PieChart.kt ================================================ package me.bytebeats.views.charts.pie import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.tooling.preview.Preview import me.bytebeats.views.charts.pie.render.ISliceDrawer import me.bytebeats.views.charts.pie.render.SimpleSliceDrawer import me.bytebeats.views.charts.simpleChartAnimation /** * Created by bytebeats on 2021/9/24 : 15:34 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ @Composable fun PieChart( pieChartData: PieChartData, modifier: Modifier = Modifier, animation: AnimationSpec = simpleChartAnimation(), sliceDrawer: ISliceDrawer = SimpleSliceDrawer() ) { val transitionProgress = remember(pieChartData.slices) { Animatable(initialValue = 0F) } LaunchedEffect(pieChartData.slices) { transitionProgress.animateTo(1F, animationSpec = animation) } DrawChart( pieChartData = pieChartData, modifier = modifier.fillMaxSize(), progress = transitionProgress.value, sliceDrawer = sliceDrawer ) } @Composable private fun DrawChart( pieChartData: PieChartData, modifier: Modifier, progress: Float, sliceDrawer: ISliceDrawer ) { val slices = pieChartData.slices Canvas( modifier = modifier ) { drawIntoCanvas { var startArc = 0F slices.forEach { slice -> val arc = calculateAngle( sliceLength = slice.value, totalLength = pieChartData.totalLength, progress = progress ) sliceDrawer.drawSlice( drawScope = this, canvas = drawContext.canvas, area = size, startAngle = startArc, sweepAngle = arc, slice = slice ) startArc += arc } } } } @Preview @Composable private fun PieChartPreview() = PieChart( pieChartData = PieChartData( slices = listOf( PieChartData.Slice(25F, Color.Red), PieChartData.Slice(45F, Color.Green), PieChartData.Slice(20F, Color.Blue), ) ) ) ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/pie/PieChartData.kt ================================================ package me.bytebeats.views.charts.pie import androidx.compose.ui.graphics.Color /** * Created by bytebeats on 2021/9/24 : 14:32 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ data class PieChartData(val slices: List) { internal val totalLength: Float get() { return slices.map { it.value }.sum() } data class Slice( val value: Float, val color: Color ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/pie/PieCharts.kt ================================================ package me.bytebeats.views.charts.pie import me.bytebeats.views.charts.util.FLOAT_360 /** * Created by bytebeats on 2021/9/24 : 14:27 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ internal fun calculateAngle( sliceLength: Float, totalLength: Float, progress: Float ): Float = FLOAT_360 * sliceLength * progress / totalLength ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/pie/render/ISliceDrawer.kt ================================================ package me.bytebeats.views.charts.pie.render import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.drawscope.DrawScope import me.bytebeats.views.charts.pie.PieChartData /** * Created by bytebeats on 2021/9/24 : 14:30 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ interface ISliceDrawer { /** * Draw slice * @param drawScope the scope to draw in * @param canvas the canvas to draw on * @param area the area to draw * @param startAngle the start angle to draw a slice * @param sweepAngle the sweep angle to draw a slice * @param slice the slice data to draw */ fun drawSlice( drawScope: DrawScope, canvas: Canvas, area: Size, startAngle: Float, sweepAngle: Float, slice: PieChartData.Slice ) } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/pie/render/SimpleSliceDrawer.kt ================================================ package me.bytebeats.views.charts.pie.render import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.drawscope.DrawScope import me.bytebeats.views.charts.pie.PieChartData import me.bytebeats.views.charts.util.FLOAT_200 /** * Created by bytebeats on 2021/9/24 : 14:36 * E-mail: happychinapc@gmail.com * Quote: Peasant. Educated. Worker */ class SimpleSliceDrawer(private val sliceThickness: Float = 25F) : ISliceDrawer { init { require(sliceThickness in 10F..100F) { "Thickness must be between 10 and 100, included" } } private val sectorPaint by lazy { Paint().apply { isAntiAlias = true style = PaintingStyle.Stroke } } private fun computeSectorThickness(area: Size): Float { val minSize = area.width.coerceAtMost(area.height) return sliceThickness * minSize / FLOAT_200 } private fun computeDrawableArea(area: Size): Rect { val sliceThicknessOffset = computeSectorThickness(area) / 2F val horizontalOffset = (area.width - area.height) / 2F return Rect( left = sliceThicknessOffset + horizontalOffset, top = sliceThicknessOffset, right = area.width - sliceThicknessOffset - horizontalOffset, bottom = area.height - sliceThicknessOffset ) } override fun drawSlice( drawScope: DrawScope, canvas: Canvas, area: Size, startAngle: Float, sweepAngle: Float, slice: PieChartData.Slice ) { val sliceThickness = computeSectorThickness(area) val drawableArea = computeDrawableArea(area) canvas.drawArc( rect = drawableArea, paint = sectorPaint.apply { color = slice.color strokeWidth = sliceThickness }, startAngle = startAngle, sweepAngle = sweepAngle, useCenter = false, ) } } ================================================ FILE: charts/src/main/java/me/bytebeats/views/charts/util/Floats.kt ================================================ package me.bytebeats.views.charts.util internal const val FLOAT_10 = 10F internal const val FLOAT_100 = 100F internal const val FLOAT_200 = 200F internal const val FLOAT_255 = 255F internal const val FLOAT_360 = 360F internal const val FLOAT_1_5 = 1.5F internal const val FLOAT_0_5 = .5F ================================================ FILE: config/detekt/detekt.yml ================================================ build: maxIssues: 0 excludeCorrectable: false weights: # complexity: 2 # LongParameterList: 1 # style: 1 # comments: 1 config: validation: true warningsAsErrors: false checkExhaustiveness: false # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' excludes: '' processors: active: true exclude: - 'DetektProgressListener' - 'KtFileCountProcessor' - 'PackageCountProcessor' - 'ClassCountProcessor' - 'FunctionCountProcessor' - 'PropertyCountProcessor' - 'ProjectComplexityProcessor' - 'ProjectCognitiveComplexityProcessor' - 'ProjectLLOCProcessor' - 'ProjectCLOCProcessor' - 'ProjectLOCProcessor' - 'ProjectSLOCProcessor' - 'LicenseHeaderLoaderExtension' console-reports: active: true exclude: - 'ProjectStatisticsReport' - 'ComplexityReport' - 'NotificationReport' - 'FindingsReport' - 'FileBasedFindingsReport' # - 'LiteFindingsReport' output-reports: active: true exclude: # - 'TxtOutputReport' # - 'XmlOutputReport' # - 'HtmlOutputReport' # - 'MdOutputReport' - 'SarifOutputReport' comments: active: true AbsentOrWrongFileLicense: active: false licenseTemplateFile: 'license.template' licenseTemplateIsRegex: false CommentOverPrivateFunction: active: false CommentOverPrivateProperty: active: false DeprecatedBlockTag: active: false EndOfSentenceFormat: active: false endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' KDocReferencesNonPublicProperty: active: false excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] OutdatedDocumentation: active: false matchTypeParameters: true matchDeclarationsOrder: true allowParamOnConstructorProperties: false UndocumentedPublicClass: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] searchInNestedClass: false searchInInnerClass: false searchInInnerObject: true searchInInnerInterface: true searchInProtectedClass: false UndocumentedPublicFunction: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] searchProtectedFunction: false ignoreAnnotated: - 'Composable' - 'Preview' ignoreFunction: - '[a-z][a-zA-Z0-9]*' UndocumentedPublicProperty: active: false excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] searchProtectedProperty: false complexity: active: true CognitiveComplexMethod: active: false threshold: 15 ComplexCondition: active: true threshold: 4 ComplexInterface: active: false threshold: 10 includeStaticDeclarations: false includePrivateDeclarations: false ignoreOverloaded: false CyclomaticComplexMethod: active: true threshold: 15 ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false ignoreNestingFunctions: false nestingFunctions: - 'also' - 'apply' - 'forEach' - 'isNotNull' - 'ifNull' - 'let' - 'run' - 'use' - 'with' LabeledExpression: active: false ignoredLabels: [ ] LargeClass: active: true threshold: 600 LongMethod: active: true threshold: 100 LongParameterList: active: true functionThreshold: 30 constructorThreshold: 10 ignoreDefaultParameters: true ignoreDataClasses: true ignoreAnnotatedParameter: [ ] MethodOverloading: active: false threshold: 6 NamedArguments: active: true threshold: 3 ignoreArgumentsMatchingNames: true NestedBlockDepth: active: true threshold: 4 NestedScopeFunctions: active: true threshold: 3 functions: - 'kotlin.apply' - 'kotlin.run' - 'kotlin.with' - 'kotlin.let' - 'kotlin.also' ReplaceSafeCallChainWithRun: active: false StringLiteralDuplication: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/internal/**' ] thresholdInFiles: 20 thresholdInClasses: 30 thresholdInInterfaces: 20 thresholdInObjects: 20 thresholdInEnums: 20 ignoreDeprecated: false ignorePrivate: true ignoreOverridden: true ignoreAnnotatedFunctions: [ 'Preview' ] coroutines: active: true GlobalCoroutineUsage: active: false InjectDispatcher: active: true dispatcherNames: - 'IO' - 'Default' - 'Unconfined' RedundantSuspendModifier: active: true SleepInsteadOfDelay: active: true SuspendFunSwallowedCancellation: active: false SuspendFunWithCoroutineScopeReceiver: active: false SuspendFunWithFlowReturnType: active: true empty-blocks: active: true EmptyCatchBlock: active: true allowedExceptionNameRegex: '_|(ignore|expected).*' EmptyClassBlock: active: true EmptyDefaultConstructor: active: true EmptyDoWhileBlock: active: true EmptyElseBlock: active: true EmptyFinallyBlock: active: true EmptyForBlock: active: true EmptyFunctionBlock: active: true ignoreOverridden: false EmptyIfBlock: active: true EmptyInitBlock: active: true EmptyKtFile: active: true EmptySecondaryConstructor: active: true EmptyTryBlock: active: true EmptyWhenBlock: active: true EmptyWhileBlock: active: true exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: true methodNames: - 'equals' - 'finalize' - 'hashCode' - 'toString' InstanceOfCheckForException: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] NotImplementedDeclaration: active: true ObjectExtendsThrowable: active: false PrintStackTrace: active: true RethrowCaughtException: active: true ReturnFromFinally: active: true ignoreLabeled: false SwallowedException: active: true ignoredExceptionTypes: - 'InterruptedException' - 'MalformedURLException' - 'NumberFormatException' - 'ParseException' allowedExceptionNameRegex: '_|(ignore|expected).*' ThrowingExceptionFromFinally: active: true ThrowingExceptionInMain: active: false ThrowingExceptionsWithoutMessageOrCause: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] exceptions: - 'ArrayIndexOutOfBoundsException' - 'Exception' - 'IllegalArgumentException' - 'IllegalMonitorStateException' - 'IllegalStateException' - 'IndexOutOfBoundsException' - 'NullPointerException' - 'RuntimeException' - 'Throwable' ThrowingNewInstanceOfSameException: active: true TooGenericExceptionCaught: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] exceptionNames: - 'ArrayIndexOutOfBoundsException' - 'Error' - 'Exception' - 'IllegalMonitorStateException' - 'IndexOutOfBoundsException' - 'NullPointerException' - 'RuntimeException' - 'Throwable' allowedExceptionNameRegex: '_|(ignore|expected).*' TooGenericExceptionThrown: active: true exceptionNames: - 'Error' - 'Exception' - 'RuntimeException' - 'Throwable' naming: active: true BooleanPropertyNaming: active: true allowedPattern: '^(is|has|are)' ClassNaming: active: true classPattern: '[A-Z][a-zA-Z0-9]*' ConstructorParameterNaming: active: true parameterPattern: '[a-z][A-Za-z0-9]*' privateParameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' EnumNaming: active: true enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false forbiddenName: [ ] FunctionMaxLength: active: true maximumFunctionNameLength: 30 FunctionMinLength: active: true minimumFunctionNameLength: 3 FunctionNaming: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] functionPattern: '[a-zA-Z][a-zA-Z0-9]*' excludeClassPattern: '$^' ignoreAnnotated: - 'Composable' FunctionParameterNaming: active: true parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' InvalidPackageDeclaration: active: true rootPackage: '' requireRootInDeclaration: false LambdaParameterNaming: active: true parameterPattern: '[a-z][A-Za-z0-9]*|_' MatchingDeclarationName: active: true mustBeFirst: true MemberNameEqualsClassName: active: true ignoreOverridden: true NoNameShadowing: active: true NonBooleanPropertyPrefixedWithIs: active: true ObjectPropertyNaming: active: true constantPattern: '[A-Za-z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' PackageNaming: active: true packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' TopLevelPropertyNaming: active: true constantPattern: '[A-Z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' VariableMaxLength: active: true maximumVariableNameLength: 64 VariableMinLength: active: true minimumVariableNameLength: 1 VariableNaming: active: true variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' performance: active: true ArrayPrimitive: active: true CouldBeSequence: active: false threshold: 3 ForEachOnRange: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] SpreadOperator: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] UnnecessaryPartOfBinaryExpression: active: true UnnecessaryTemporaryInstantiation: active: true potential-bugs: active: true AvoidReferentialEquality: active: true forbiddenTypePatterns: - 'kotlin.String' CastNullableToNonNullableType: active: true CastToNullableType: active: true Deprecation: active: false DontDowncastCollectionTypes: active: false DoubleMutabilityForCollection: active: true mutableTypes: - 'kotlin.collections.MutableList' - 'kotlin.collections.MutableMap' - 'kotlin.collections.MutableSet' - 'java.util.ArrayList' - 'java.util.LinkedHashSet' - 'java.util.HashSet' - 'java.util.LinkedHashMap' - 'java.util.HashMap' ElseCaseInsteadOfExhaustiveWhen: active: true ignoredSubjectTypes: [ ] EqualsAlwaysReturnsTrueOrFalse: active: true EqualsWithHashCodeExist: active: true ExitOutsideMain: active: false ExplicitGarbageCollectionCall: active: true HasPlatformType: active: true IgnoredReturnValue: active: true restrictToConfig: true returnValueAnnotations: - 'CheckResult' - '*.CheckResult' - 'CheckReturnValue' - '*.CheckReturnValue' ignoreReturnValueAnnotations: - 'CanIgnoreReturnValue' - '*.CanIgnoreReturnValue' returnValueTypes: - 'kotlin.sequences.Sequence' - 'kotlinx.coroutines.flow.*Flow' - 'java.util.stream.*Stream' ignoreFunctionCall: [ ] ImplicitDefaultLocale: active: true ImplicitUnitReturnType: active: false allowExplicitReturnType: true InvalidRange: active: true IteratorHasNextCallsNextMethod: active: true IteratorNotThrowingNoSuchElementException: active: true LateinitUsage: active: false excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: active: true MissingPackageDeclaration: active: false excludes: [ '**/*.kts' ] NullCheckOnMutableProperty: active: false NullableToStringCall: active: false PropertyUsedBeforeDeclaration: active: false UnconditionalJumpStatementInLoop: active: false UnnecessaryNotNullCheck: active: false UnnecessaryNotNullOperator: active: true UnnecessarySafeCall: active: true UnreachableCatchBlock: active: true UnreachableCode: active: true UnsafeCallOnNullableType: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] UnsafeCast: active: true UnusedUnaryOperator: active: true UselessPostfixExpression: active: true WrongEqualsTypeParameter: active: true style: active: true AlsoCouldBeApply: active: false BracesOnIfStatements: active: false singleLine: 'never' multiLine: 'always' BracesOnWhenStatements: active: false singleLine: 'necessary' multiLine: 'consistent' CanBeNonNullable: active: false CascadingCallWrapping: active: false includeElvis: true ClassOrdering: active: false CollapsibleIfStatements: active: false DataClassContainsFunctions: active: false conversionFunctionPrefix: - 'to' allowOperators: false DataClassShouldBeImmutable: active: false DestructuringDeclarationWithTooManyEntries: active: true maxDestructuringEntries: 3 DoubleNegativeLambda: active: false negativeFunctions: - reason: 'Use `takeIf` instead.' value: 'takeUnless' - reason: 'Use `all` instead.' value: 'none' negativeFunctionNameParts: - 'not' - 'non' EqualsNullCall: active: true EqualsOnSignatureLine: active: false ExplicitCollectionElementAccessMethod: active: false ExplicitItLambdaParameter: active: true ExpressionBodySyntax: active: false includeLineWrapping: false ForbiddenAnnotation: active: false annotations: - reason: 'it is a java annotation. Use `Suppress` instead.' value: 'java.lang.SuppressWarnings' - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' value: 'java.lang.Deprecated' - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' value: 'java.lang.annotation.Documented' - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' value: 'java.lang.annotation.Target' - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' value: 'java.lang.annotation.Retention' - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' value: 'java.lang.annotation.Repeatable' - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' value: 'java.lang.annotation.Inherited' ForbiddenComment: active: true comments: - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' value: 'FIXME:' - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' value: 'STOPSHIP:' - reason: 'Forbidden TODO todo marker in comment, please do the changes.' value: 'TODO:' allowedPatterns: '' ForbiddenImport: active: false imports: [ ] forbiddenPatterns: '' ForbiddenMethodCall: active: false methods: - reason: 'print does not allow you to configure the output stream. Use a logger instead.' value: 'kotlin.io.print' - reason: 'println does not allow you to configure the output stream. Use a logger instead.' value: 'kotlin.io.println' ForbiddenSuppress: active: false rules: [ ] ForbiddenVoid: active: true ignoreOverridden: false ignoreUsageInGenerics: false FunctionOnlyReturningConstant: active: true ignoreOverridableFunction: true ignoreActualFunction: true excludedFunctions: [ ] LoopWithTooManyJumpStatements: active: true maxJumpCount: 1 MagicNumber: active: false excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] ignoreNumbers: - '-1' - '0' - '1' - '2' ignoreHashCodeFunction: true ignorePropertyDeclaration: true ignoreLocalVariableDeclaration: true ignoreConstantDeclaration: true ignoreCompanionObjectPropertyDeclaration: true ignoreAnnotation: true ignoreNamedArgument: true ignoreEnums: true ignoreRanges: true ignoreExtensionFunctions: true ignoreAnnotated: - 'Preview' MandatoryBracesLoops: active: false MaxChainedCallsOnSameLine: active: false maxChainedCalls: 5 MaxLineLength: active: true maxLineLength: 120 excludePackageStatements: true excludeImportStatements: true excludeCommentStatements: false excludeRawStrings: true MayBeConst: active: true ModifierOrder: active: true MultilineLambdaItParameter: active: false MultilineRawStringIndentation: active: false indentSize: 4 trimmingMethods: - 'trimIndent' - 'trimMargin' NestedClassesVisibility: active: true NewLineAtEndOfFile: active: true NoTabs: active: false NullableBooleanCheck: active: false ObjectLiteralToLambda: active: true OptionalAbstractKeyword: active: true OptionalUnit: active: false PreferToOverPairSyntax: active: false ProtectedMemberInFinalClass: active: true RedundantExplicitType: active: false RedundantHigherOrderMapUsage: active: true RedundantVisibilityModifierRule: active: false ReturnCount: active: true max: 2 excludedFunctions: - 'equals' excludeLabeled: false excludeReturnFromLambda: true excludeGuardClauses: false SafeCast: active: true SerialVersionUIDInSerializableClass: active: true SpacingBetweenPackageAndImports: active: false StringShouldBeRawString: active: false maxEscapedCharacterCount: 2 ignoredCharacters: [ ] ThrowsCount: active: true max: 2 excludeGuardClauses: false TrailingWhitespace: active: false TrimMultilineRawString: active: false trimmingMethods: - 'trimIndent' - 'trimMargin' UnderscoresInNumericLiterals: active: false acceptableLength: 4 allowNonStandardGrouping: false UnnecessaryAbstractClass: active: true UnnecessaryAnnotationUseSiteTarget: active: false UnnecessaryApply: active: true UnnecessaryBackticks: active: false UnnecessaryBracesAroundTrailingLambda: active: false UnnecessaryFilter: active: true UnnecessaryInheritance: active: true UnnecessaryInnerClass: active: false UnnecessaryLet: active: false UnnecessaryParentheses: active: false allowForUnclearPrecedence: false UntilInsteadOfRangeTo: active: false UnusedImports: active: false UnusedParameter: active: true allowedNames: 'ignored|expected' UnusedPrivateClass: active: true UnusedPrivateMember: active: true allowedNames: '' ignoreAnnotated: - 'Preview' UnusedPrivateProperty: active: true allowedNames: '_|ignored|expected|serialVersionUID' UseAnyOrNoneInsteadOfFind: active: true UseArrayLiteralsInAnnotations: active: true UseCheckNotNull: active: true UseCheckOrError: active: true UseDataClass: active: false allowVars: false UseEmptyCounterpart: active: false UseIfEmptyOrIfBlank: active: false UseIfInsteadOfWhen: active: false ignoreWhenContainingVariableDeclaration: false UseIsNullOrEmpty: active: true UseLet: active: false UseOrEmpty: active: true UseRequire: active: true UseRequireNotNull: active: true UseSumOfInsteadOfFlatMapSize: active: false UselessCallOnNotNull: active: true UtilityClassWithPublicConstructor: active: true VarCouldBeVal: active: true ignoreLateinitVar: false WildcardImport: active: true excludeImports: - 'java.util.*' ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] agp = "8.3.2" composeCharts = "0.2.1" kotlin = "1.9.24" coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" lifecycleRuntimeKtx = "2.8.1" activityCompose = "1.9.0" compose = "1.6.7" material3 = "1.2.1" ktCompilerExt = "1.5.14" dokka = "1.9.20" detekt = "1.23.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } compose-charts = { module = "io.github.bytebeats:compose-charts", version.ref = "composeCharts" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "compose" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } android-library = { id = "com.android.library", version.ref = "agp" } jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } detekt-gradle-plugin = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Thu Sep 23 21:39:47 CST 2021 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true # POM related configuration GROUP_ID=io.github.bytebeats # developer related configuration DEVELOPER_ID=bytebeats DEVELOPER_NAME=Chen Pan DEVELOPER_URL=https://github.com/bytebeats DEVELOPER_EMAIL=happychinapc@gmail.com # SCM related configuration SCM_CONNECTION=scm:git:github.com/bytebeats/compose-charts.git SCM_DEVELOPER_CONNECTION=scm:git:ssh:github.com/bytebeats/compose-charts.git SCM_URL=https://github.com/bytebeats/compose-charts/tree/master # License related configuration LICENSE_NAME=The Apache License, Version 2.0 LICENSE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt LICENCE_DIST=repo # License related configuration ISSUE_SYSTEM=GitHub ISSUE_URL=https://github.com/bytebeats/compose-charts/issues # organization related configuration #ORGANIZATION_NAME= #ORGANIZATION_URL= # Contributors related configuration CONTRIBUTOR_NAME=bytebeats CONTRIBUTOR_EMAIL=happychinapc@gmail.com CONTRIBUTOR_URL=https://github.com/bytebeats CONTRIBUTOR_TIMEZONE=China/Beijing # CI related configuration CI_SYSTEM=GitHub CI_URL=https://github.com/bytebeats/compose-charts/actions/ # Library related configuration PUBLICATION_NAME=ComposeCharts MODULE_NAME=ComposeCharts REPO_NAME=SonaTypeMavenCentral COMPOSE_CHARTS_ARTIFACT_ID=compose-charts COMPOSE_CHARTS_VERSION=0.2.1 COMPOSE_CHARTS_PACKAGING=aar COMPOSE_CHARTS_INCEPTION_YEAR=2021 COMPOSE_CHARTS_URL=https://github.com/bytebeats/compose-charts COMPOSE_CHARTS_DESCRIPTION=compose-charts: Simple Jetpack Compose Charts for multi-platform. Including Android, Web, Desktop. RELEASES_REPO_URL=https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ SNAPSHOTS_REPO_URL=https://s01.oss.sonatype.org/content/repositories/snapshots/ ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { repositories { google { content { includeGroupByRegex("com\\.android.*") includeGroupByRegex("com\\.google.*") includeGroupByRegex("androidx.*") } } mavenCentral() gradlePluginPortal() maven { url = uri("https://repo1.maven.org/maven2/") } mavenLocal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "compose-charts" include(":app") include(":charts")