diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..157722f --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +build/* +/buildSrc/build/* diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e6a7a34 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +charty-library \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..29e1ffa --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2a4d5b5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..3856dba --- /dev/null +++ b/Readme.md @@ -0,0 +1,38 @@ +## Charty : Elementary Chart library for Compose + +![Charty](img/charty-banner.png) + +Chart Library built using Jetpack Compose and is highly customizable. Updates coming soon! +_Made with ❤️ for Android Developers by Himanshu_ + +[![Github Followers](https://img.shields.io/github/followers/hi-manshu?label=Follow&style=social)](https://github.com/hi-manshu) +[![Twitter Follow](https://img.shields.io/twitter/follow/hi_man_shoe?label=Follow&style=social)](https://twitter.com/hi_man_shoe) + +## Implementation + +### Gradle setup + +In `build.gradle` of app module, include this dependency + +```gradle +dependencies { + implementation("com.himanshoe:charty:1.0.0-alpha01") +} +``` + +## Documentation +You can find the detail implementation of the following: + +- [BarChart](docs/BarChart.md) +- [GroupedBarChart](docs/GroupedBarChart.md) +- [CircleChart](docs/CircleChart.md) +- [PointChart](docs/PointChart.md) +- [LineChart](docs/LineChart.md) +- [PieChart](docs/PieChart.md) +- [CurveLineChart](docs/CurveLineChart.md) + + +### Contribution +Please feel free to fork it and open a PR. + + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..18e3185 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + id(Plugins.application) + kotlin(Plugins.android) +} + +android { + compileSdk = ModuleExtension.compileSdkVersion + + defaultConfig { + applicationId = ModuleExtension.App.applicationId + minSdk = ModuleExtension.DefaultConfigs.minSdkVersion + targetSdk = ModuleExtension.DefaultConfigs.targetSdkVersion + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = ModuleExtension.DefaultConfigs.testInstrumentationRunner + vectorDrawables { + useSupportLibrary = true + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile(ModuleExtension.DefaultConfigs.defaultProguardOptimizeFileName), + ModuleExtension.DefaultConfigs.proGuardRules + ) + } + create("staging") { + initWith(getByName("debug")) + matchingFallbacks.add("debug") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = ModuleExtension.jvmTarget + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose + } +} + +dependencies { + //implementation(project(":charty")) + implementation("com.himanshoe:charty:1.0.0-alpha01") + implementation(Deps.Compose.ui) + implementation(Deps.Compose.material) + implementation(Deps.Compose.activity) + implementation(Deps.Jetpack.Core.ktx) + implementation(Deps.Android.materialDesign) + testImplementation(Deps.Test.jUnit) + androidTestImplementation(Deps.AndroidTest.jUnitExtensions) + androidTestImplementation(Deps.AndroidTest.espressoCore) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# 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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/himanshoe/charty/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/himanshoe/charty/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..57e3459 --- /dev/null +++ b/app/src/androidTest/java/com/himanshoe/charty/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.himanshoe.charty + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.himanshoe.charty", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7c8156f --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/himanshoe/charty/MainActivity.kt b/app/src/main/java/com/himanshoe/charty/MainActivity.kt new file mode 100644 index 0000000..11df077 --- /dev/null +++ b/app/src/main/java/com/himanshoe/charty/MainActivity.kt @@ -0,0 +1,175 @@ +package com.himanshoe.charty + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.himanshoe.charty.bar.BarChart +import com.himanshoe.charty.bar.GroupedBarChart +import com.himanshoe.charty.bar.model.BarData +import com.himanshoe.charty.bar.model.GroupedBarData +import com.himanshoe.charty.circle.CircleChart +import com.himanshoe.charty.circle.model.CircleData +import com.himanshoe.charty.line.CurveLineChart +import com.himanshoe.charty.line.LineChart +import com.himanshoe.charty.line.model.LineData +import com.himanshoe.charty.pie.PieChart +import com.himanshoe.charty.point.PointChart +import com.himanshoe.charty.point.model.PointData + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + LazyColumn(Modifier.fillMaxSize()) { + item { + CurveLineChart( + modifier = Modifier + .padding(top = 100.dp) + .size(width = 500.dp, height = 300.dp) + .padding(20.dp), + colors = listOf( + Color.Green, + Color.Black, + ), + lineData = listOf( + LineData(10F, 35F), + LineData(20F, 25F), + LineData(50F, 100F), + LineData(20F, 25F), + + ) + ) + } + item { + BarChart( + modifier = Modifier + .padding(top = 100.dp) + .size(width = 500.dp, height = 300.dp) + .padding(20.dp), + onBarClick = { + }, + colors = listOf(Color.Green, Color.Black), + barData = listOf( + BarData(10F, 35F), + BarData(20F, 25F), + BarData(10F, 50F), + BarData(100F, 10F), + BarData(10F, 15F), + BarData(50F, 100F), + BarData(20F, 25F), + ) + ) + } + + item { + GroupedBarChart( + modifier = Modifier + .size(width = 500.dp, height = 300.dp) + .padding(20.dp), + groupedBarData = listOf( + GroupedBarData( + listOf( + BarData(10F, 35F), + BarData(20F, 25F), + BarData(10F, 50F), + ), + colors = listOf(Color.Black, Color.Green, Color.Yellow) + ), + GroupedBarData( + listOf( + BarData(10F, 35F), + BarData(20F, 25F), + BarData(10F, 50F), + ), + colors = listOf(Color.Black, Color.Green, Color.Yellow) + ), + GroupedBarData( + listOf( + BarData(10F, 35F), + BarData(20F, 25F), + BarData(10F, 50F), + ), + colors = listOf(Color.Black, Color.Green, Color.Yellow) + ), + ), + ) + } + item { + PieChart( + modifier = Modifier + .scale(1f) + .size(400.dp) + .padding(20.dp), + data = listOf(20F, 50F, 100F, 70F, 20F, 50F, 100F, 70F), + isDonut = true, + valueTextColor = Color.Black, + onSectionClicked = { percent, value -> + } + ) + } + item { + CircleChart( + modifier = Modifier + .scale(1f) + .size(400.dp) + .padding(20.dp), + circleData = listOf( + CircleData(10F, 235F, Color.Green), + CircleData(10F, 135F, Color.Green), + CircleData(10F, 315F, Color.Green), + CircleData(20F, 50F, Color.Green), + CircleData(30F, 315F) + ), + color = Color.Yellow + ) + } + + item { + LineChart( + modifier = Modifier + .padding(top = 100.dp) + .size(width = 500.dp, height = 300.dp) + .padding(20.dp), + colors = listOf(Color.Green, Color.Black), + lineData = listOf( + LineData(10F, 35F), + LineData(20F, 25F), + LineData(10F, 50F), + LineData(100F, 10F), + LineData(10F, 15F), + LineData(50F, 100F), + LineData(20F, 25F), + ) + ) + } + + item { + PointChart( + modifier = Modifier + .padding(top = 100.dp) + .size(width = 500.dp, height = 300.dp) + .padding(20.dp), + colors = listOf(Color.Green, Color.Black), + pointData = listOf( + PointData(10F, 35F), + PointData(20F, 25F), + PointData(10F, 50F), + PointData(100F, 10F), + PointData(10F, 15F), + PointData(50F, 100F), + PointData(20F, 25F), + ) + ) + } + } + } + } +} diff --git a/app/src/main/java/com/himanshoe/charty/ui/theme/Color.kt b/app/src/main/java/com/himanshoe/charty/ui/theme/Color.kt new file mode 100644 index 0000000..7347736 --- /dev/null +++ b/app/src/main/java/com/himanshoe/charty/ui/theme/Color.kt @@ -0,0 +1,8 @@ +package com.himanshoe.charty.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) diff --git a/app/src/main/java/com/himanshoe/charty/ui/theme/Shape.kt b/app/src/main/java/com/himanshoe/charty/ui/theme/Shape.kt new file mode 100644 index 0000000..3a993af --- /dev/null +++ b/app/src/main/java/com/himanshoe/charty/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.himanshoe.charty.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) diff --git a/app/src/main/java/com/himanshoe/charty/ui/theme/Theme.kt b/app/src/main/java/com/himanshoe/charty/ui/theme/Theme.kt new file mode 100644 index 0000000..a807427 --- /dev/null +++ b/app/src/main/java/com/himanshoe/charty/ui/theme/Theme.kt @@ -0,0 +1,47 @@ +package com.himanshoe.charty.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = 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 ChartylibraryTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} diff --git a/app/src/main/java/com/himanshoe/charty/ui/theme/Type.kt b/app/src/main/java/com/himanshoe/charty/ui/theme/Type.kt new file mode 100644 index 0000000..b2b0490 --- /dev/null +++ b/app/src/main/java/com/himanshoe/charty/ui/theme/Type.kt @@ -0,0 +1,28 @@ +package com.himanshoe.charty.ui.theme + +import androidx.compose.material.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( + body1 = 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 + ) + */ +) diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..7706ab9 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6b78462 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6b78462 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..ca1931b --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..cacd6c0 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + charty-library + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..57d0610 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..148c18b --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..0c4f95c --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/test/java/com/himanshoe/charty/ExampleUnitTest.kt b/app/src/test/java/com/himanshoe/charty/ExampleUnitTest.kt new file mode 100644 index 0000000..77df319 --- /dev/null +++ b/app/src/test/java/com/himanshoe/charty/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.himanshoe.charty + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..40241ed --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,36 @@ +buildscript { + repositories { + google() + mavenCentral() + maven("https://plugins.gradle.org/m2/") + + } + dependencies { + classpath(Deps.Gradle.androidGradlePlugin) + classpath(Deps.Gradle.kotlinGradlePlugin) + classpath(Deps.Gradle.vanniktechGradlePlugin) + classpath(Deps.Gradle.detektGradlePlugin) + classpath(Deps.Gradle.ktlintGradlePlugin) + } +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} +subprojects { + apply(plugin = Plugins.ktlint) + apply(plugin = Plugins.detekt) + +// detekt { +// config = files("$rootDir/${ModuleExtension.FilePath.detekt}") +// buildUponDefaultConfig = true +// } +} +allprojects { + pluginManager.withPlugin(Plugins.vanniktechPublish) { + extensions.getByType(com.vanniktech.maven.publish.MavenPublishPluginExtension::class.java) + .apply { + sonatypeHost = com.vanniktech.maven.publish.SonatypeHost.S01 + } + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..1c0ea9a --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `kotlin-dsl` + kotlin("jvm") version "1.6.10" +} + +repositories { + mavenCentral() +} diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt new file mode 100644 index 0000000..5c59aef --- /dev/null +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -0,0 +1,57 @@ +object Plugins { + val application by lazy { "com.android.application" } + val library by lazy { "com.android.library" } + val android by lazy { "android" } + val kotlinAndroid by lazy { "kotlin-android" } + val vanniktechPublish by lazy { "com.vanniktech.maven.publish" } + val ktlint by lazy { "org.jlleitschuh.gradle.ktlint" } + val detekt by lazy { "io.gitlab.arturbosch.detekt" } +} + +object Deps { + + object Compose { + val ui by lazy { "androidx.compose.ui:ui:${Versions.compose}" } + val uiTooling by lazy { "androidx.compose.ui:ui-tooling:${Versions.compose}" } + val uiToolingPreview by lazy { "androidx.compose.ui:ui-tooling-preview:${Versions.compose}" } + val material by lazy { "androidx.compose.material:material:${Versions.compose}" } + val activity by lazy { "androidx.activity:activity-compose:${Versions.activity}" } + val viewmodel by lazy { "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1" } + } + + object Android { + val materialDesign by lazy { "com.google.android.material:material:${Versions.material}" } + } + + object Test { + val jUnit by lazy { "junit:junit:${Versions.jUnit}" } + } + + object AndroidTest { + val jUnitExtensions by lazy { "androidx.test.ext:junit:${Versions.jUnitExtensions}" } + val espressoCore by lazy { "androidx.test.espresso:espresso-core:${Versions.espresso}" } + val uiTestJunit by lazy { "androidx.compose.ui:ui-test-junit4:${Versions.compose}" } + } + + object DateTime { + val date by lazy { "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0" } + } + + object Jetpack { + object Core { + val ktx by lazy { "androidx.core:core-ktx:${Versions.core}" } + } + } + + object Desugar { + val jdk by lazy { "com.android.tools:desugar_jdk_libs:1.1.5" } + } + + object Gradle { + val kotlinGradlePlugin by lazy { "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" } + val androidGradlePlugin by lazy { "com.android.tools.build:gradle:${Versions.androidGradlePlugin}" } + val vanniktechGradlePlugin by lazy { "com.vanniktech:gradle-maven-publish-plugin:${Versions.vanniktechGradlePlugin}" } + val ktlintGradlePlugin by lazy { "org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktlint}" } + val detektGradlePlugin by lazy { "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${Versions.detekt}" } + } +} diff --git a/buildSrc/src/main/kotlin/ModuleExtension.kt b/buildSrc/src/main/kotlin/ModuleExtension.kt new file mode 100644 index 0000000..1f30169 --- /dev/null +++ b/buildSrc/src/main/kotlin/ModuleExtension.kt @@ -0,0 +1,22 @@ +object ModuleExtension { + const val compileSdkVersion = 32 + const val jvmTarget = "11" + + object DefaultConfigs { + const val minSdkVersion = 21 + const val targetSdkVersion = 32 + const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + const val defaultConsumerProguardFiles = "consumer-rules.pro" + const val proGuardRules = "proguard-rules.pro" + const val defaultProguardOptimizeFileName = "proguard-android-optimize.txt" + } + + object App { + const val applicationId = "com.himanshoe.sample" + } + + object FilePath { + const val gitHooks = "gradle/scripts/git-hooks.gradle.kts" + const val detekt = "gradle/config/detekt.yml" + } +} diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 0000000..72331b1 --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,14 @@ +object Versions { + const val compose = "1.2.0" + const val androidGradlePlugin = "7.2.0" + const val vanniktechGradlePlugin = "0.18.0" + const val kotlin = "1.7.0" + const val activity = "1.3.1" + const val material = "1.4.0" + const val jUnit = "4.13.2" + const val jUnitExtensions = "1.1.3" + const val espresso = "3.4.0" + const val core = "1.7.0" + const val ktlint = "10.2.0" + const val detekt = "1.19.0" +} diff --git a/charty/.gitignore b/charty/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/charty/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/charty/build.gradle.kts b/charty/build.gradle.kts new file mode 100644 index 0000000..4507c20 --- /dev/null +++ b/charty/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + id(Plugins.library) + id(Plugins.kotlinAndroid) +} + +android { + compileSdk = ModuleExtension.compileSdkVersion + + defaultConfig { + minSdk = ModuleExtension.DefaultConfigs.minSdkVersion + targetSdk = ModuleExtension.DefaultConfigs.targetSdkVersion + + testInstrumentationRunner = ModuleExtension.DefaultConfigs.testInstrumentationRunner + consumerProguardFiles(ModuleExtension.DefaultConfigs.defaultConsumerProguardFiles) + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile(ModuleExtension.DefaultConfigs.defaultProguardOptimizeFileName), + ModuleExtension.DefaultConfigs.proGuardRules + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = ModuleExtension.jvmTarget + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose + } +} + +dependencies { + implementation(Deps.Compose.ui) + implementation(Deps.Compose.material) + implementation(Deps.Compose.uiToolingPreview) + implementation(Deps.Compose.activity) + + debugApi(Deps.Compose.uiTooling) + + testImplementation(Deps.Test.jUnit) + androidTestImplementation(Deps.AndroidTest.jUnitExtensions) + androidTestImplementation(Deps.AndroidTest.espressoCore) + androidTestApi(Deps.AndroidTest.uiTestJunit) +} + +plugins.apply(Plugins.vanniktechPublish) diff --git a/charty/consumer-rules.pro b/charty/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/charty/gradle.properties b/charty/gradle.properties new file mode 100644 index 0000000..8672943 --- /dev/null +++ b/charty/gradle.properties @@ -0,0 +1,17 @@ +# Maven +POM_ARTIFACT_ID=charty +POM_NAME=charty +POM_DESCRIPTION=An Elementary Compose Chart library. +POM_PACKAGING=aar +POM_INCEPTION_YEAR=2022 +GROUP=com.himanshoe +VERSION_NAME=1.0.0-alpha01 +VERSION_CODE=1 +POM_URL=https://github.com/hi-manshu +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo +POM_SCM_URL=https://github.com/hi-manshu +POM_DEVELOPER_ID=hi-manshu +POM_DEVELOPER_NAME=Himanshu Singh +POM_DEVELOPER_URL=https://github.com/hi-manshu diff --git a/charty/proguard-rules.pro b/charty/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/charty/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# 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 \ No newline at end of file diff --git a/charty/src/androidTest/java/com/himanshoe/charty/ExampleInstrumentedTest.kt b/charty/src/androidTest/java/com/himanshoe/charty/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d223975 --- /dev/null +++ b/charty/src/androidTest/java/com/himanshoe/charty/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.himanshoe.charty + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.himanshoe.charty.test", appContext.packageName) + } +} diff --git a/charty/src/main/AndroidManifest.xml b/charty/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5cc4e17 --- /dev/null +++ b/charty/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/charty/src/main/java/com/himanshoe/charty/bar/BarChart.kt b/charty/src/main/java/com/himanshoe/charty/bar/BarChart.kt new file mode 100644 index 0000000..fcc0c8b --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/bar/BarChart.kt @@ -0,0 +1,108 @@ +package com.himanshoe.charty.bar + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import com.himanshoe.charty.bar.common.calculations.getTopLeft +import com.himanshoe.charty.bar.common.calculations.getTopRight +import com.himanshoe.charty.bar.common.component.drawBarLabel +import com.himanshoe.charty.bar.config.BarConfig +import com.himanshoe.charty.bar.config.BarConfigDefaults +import com.himanshoe.charty.bar.model.BarData +import com.himanshoe.charty.bar.model.maxYValue +import com.himanshoe.charty.common.axis.AxisConfig +import com.himanshoe.charty.common.axis.AxisConfigDefaults +import com.himanshoe.charty.common.axis.xAxis +import com.himanshoe.charty.common.axis.yAxis +import com.himanshoe.charty.common.dimens.ChartDimens +import com.himanshoe.charty.common.dimens.ChartDimensDefaults + +@Composable +fun BarChart( + barData: List, + color: Color, + onBarClick: (BarData) -> Unit, + modifier: Modifier = Modifier, + barDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + barConfig: BarConfig = BarConfigDefaults.barConfigDimesDefaults() +) { + BarChart( + barData = barData, + colors = listOf(color, color), + onBarClick = onBarClick, + modifier = modifier, + barDimens = barDimens, + axisConfig = axisConfig, + barConfig = barConfig + ) +} + +@Composable +fun BarChart( + barData: List, + colors: List, + onBarClick: (BarData) -> Unit, + modifier: Modifier = Modifier, + barDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + barConfig: BarConfig = BarConfigDefaults.barConfigDimesDefaults() +) { + + val maxYValueState = rememberSaveable { mutableStateOf(barData.maxYValue()) } + val clickedBar = remember { + mutableStateOf(Offset(-10F, -10F)) + } + + val maxYValue = maxYValueState.value + val barWidth = remember { mutableStateOf(0F) } + + Canvas( + modifier = modifier + .drawBehind { + if (axisConfig.showAxes) { + xAxis(axisConfig, maxYValue) + yAxis(axisConfig) + } + } + .padding(horizontal = barDimens.horizontalPadding) + .pointerInput(Unit) { + detectTapGestures(onPress = { offset -> + clickedBar.value = offset + }) + } + ) { + barWidth.value = size.width.div(barData.count().times(1.2F)) + val yScalableFactor = size.height.div(maxYValue) + + barData.forEachIndexed { index, data -> + val topLeft = getTopLeft(index, barWidth, size, data, yScalableFactor) + val topRight = getTopRight(index, barWidth, size, data, yScalableFactor) + val barHeight = data.yValue.times(yScalableFactor) + + if (clickedBar.value.x in (topLeft.x..topRight.x)) { + onBarClick(data) + } + drawRoundRect( + cornerRadius = CornerRadius(if (barConfig.hasRoundedCorner) barHeight else 0F), + topLeft = topLeft, + brush = Brush.linearGradient(colors), + size = Size(barWidth.value, barHeight) + ) + // draw label + drawBarLabel(data, barWidth.value, barHeight, topLeft) + } + } +} diff --git a/charty/src/main/java/com/himanshoe/charty/bar/GroupedBarChart.kt b/charty/src/main/java/com/himanshoe/charty/bar/GroupedBarChart.kt new file mode 100644 index 0000000..8a702e2 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/bar/GroupedBarChart.kt @@ -0,0 +1,91 @@ +package com.himanshoe.charty.bar + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import com.himanshoe.charty.bar.common.calculations.getTopLeft +import com.himanshoe.charty.bar.common.calculations.getTopRight +import com.himanshoe.charty.bar.common.component.drawBarLabel +import com.himanshoe.charty.bar.config.BarConfig +import com.himanshoe.charty.bar.config.BarConfigDefaults +import com.himanshoe.charty.bar.model.BarData +import com.himanshoe.charty.bar.model.GroupedBarData +import com.himanshoe.charty.bar.model.maxYValue +import com.himanshoe.charty.bar.model.totalItems +import com.himanshoe.charty.common.axis.AxisConfig +import com.himanshoe.charty.common.axis.AxisConfigDefaults +import com.himanshoe.charty.common.axis.xAxis +import com.himanshoe.charty.common.axis.yAxis +import com.himanshoe.charty.common.dimens.ChartDimens +import com.himanshoe.charty.common.dimens.ChartDimensDefaults + +@Composable +fun GroupedBarChart( + groupedBarData: List, + modifier: Modifier = Modifier, + onBarClick: (BarData) -> Unit = {}, + barDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + barConfig: BarConfig = BarConfigDefaults.barConfigDimesDefaults() +) { + val barWidth = remember { mutableStateOf(0F) } + val maxYValueState = rememberSaveable { mutableStateOf(groupedBarData.maxYValue()) } + val clickedBar = remember { + mutableStateOf(Offset(-10F, -10F)) + } + val maxYValue = maxYValueState.value + + val totalItems: Int = groupedBarData.totalItems() + Canvas( + modifier = modifier + .drawBehind { + if (axisConfig.showAxes) { + xAxis(axisConfig, maxYValue) + yAxis(axisConfig) + } + } + .padding(horizontal = barDimens.horizontalPadding) + .pointerInput(Unit) { + detectTapGestures(onPress = { offset -> + clickedBar.value = offset + }) + } + ) { + barWidth.value = size.width.div(totalItems.times(1.2F)) + val yScalableFactor = size.height.div(maxYValue) + val groupedBarDataColor: List = groupedBarData.flatMap { it.colors } + val groupedBarDataCount = groupedBarData.flatMap { it.barData }.count() + + if (groupedBarDataColor.count() != groupedBarDataCount) throw Exception("Total colors cannot be more then $groupedBarDataCount") + + groupedBarData.flatMap { it.barData } + .forEachIndexed { index, data -> + val topLeft = getTopLeft(index, barWidth, size, data, yScalableFactor) + val topRight = getTopRight(index, barWidth, size, data, yScalableFactor) + val barHeight = data.yValue.times(yScalableFactor) + + if (clickedBar.value.x in (topLeft.x..topRight.x)) { + onBarClick(data) + } + drawRoundRect( + cornerRadius = CornerRadius(if (barConfig.hasRoundedCorner) barHeight else 0F), + topLeft = topLeft, + color = groupedBarDataColor[index], + size = Size(barWidth.value, barHeight) + ) + // draw label + drawBarLabel(data, barWidth.value, barHeight, topLeft) + } + } +} diff --git a/charty/src/main/java/com/himanshoe/charty/bar/common/calculations/BarCalculations.kt b/charty/src/main/java/com/himanshoe/charty/bar/common/calculations/BarCalculations.kt new file mode 100644 index 0000000..ff01734 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/bar/common/calculations/BarCalculations.kt @@ -0,0 +1,28 @@ +package com.himanshoe.charty.bar.common.calculations + +import androidx.compose.runtime.MutableState +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import com.himanshoe.charty.bar.model.BarData + +internal fun getTopLeft( + index: Int, + barWidth: MutableState, + size: Size, + barData: BarData, + yScalableFactor: Float +) = Offset( + x = index.times(barWidth.value.times(1.2F)), + y = size.height.minus(barData.yValue.times(yScalableFactor)) +) + +internal fun getTopRight( + index: Int, + barWidth: MutableState, + size: Size, + barData: BarData, + yChunck: Float +) = Offset( + x = index.plus(1).times(barWidth.value.times(1.2F)), + y = size.height.minus(barData.yValue.times(yChunck)) +) diff --git a/charty/src/main/java/com/himanshoe/charty/bar/common/component/Labels.kt b/charty/src/main/java/com/himanshoe/charty/bar/common/component/Labels.kt new file mode 100644 index 0000000..e85daaf --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/bar/common/component/Labels.kt @@ -0,0 +1,29 @@ +package com.himanshoe.charty.bar.common.component + +import android.graphics.Paint +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import com.himanshoe.charty.bar.model.BarData + +internal fun DrawScope.drawBarLabel( + data: BarData, + barWidth: Float, + barHeight: Float, + topLeft: Offset +) { + drawIntoCanvas { + it.nativeCanvas.apply { + drawText( + data.xValue.toString(), + topLeft.x.plus(barWidth.div(2)), + topLeft.y.plus(barHeight.plus(barWidth.div(2))), + Paint().apply { + textSize = size.width.div(30) + textAlign = Paint.Align.CENTER + } + ) + } + } +} diff --git a/charty/src/main/java/com/himanshoe/charty/bar/config/BarConfig.kt b/charty/src/main/java/com/himanshoe/charty/bar/config/BarConfig.kt new file mode 100644 index 0000000..5318923 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/bar/config/BarConfig.kt @@ -0,0 +1,12 @@ +package com.himanshoe.charty.bar.config + +data class BarConfig( + val hasRoundedCorner: Boolean = false +) + +internal object BarConfigDefaults { + + fun barConfigDimesDefaults() = BarConfig( + hasRoundedCorner = false + ) +} diff --git a/charty/src/main/java/com/himanshoe/charty/bar/model/BarData.kt b/charty/src/main/java/com/himanshoe/charty/bar/model/BarData.kt new file mode 100644 index 0000000..35e18dc --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/bar/model/BarData.kt @@ -0,0 +1,7 @@ +package com.himanshoe.charty.bar.model + +data class BarData(val xValue: Any, val yValue: Float) + +internal fun List.maxYValue() = maxOf { + it.yValue +} diff --git a/charty/src/main/java/com/himanshoe/charty/bar/model/GroupedBarData.kt b/charty/src/main/java/com/himanshoe/charty/bar/model/GroupedBarData.kt new file mode 100644 index 0000000..9db5251 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/bar/model/GroupedBarData.kt @@ -0,0 +1,13 @@ +package com.himanshoe.charty.bar.model + +import androidx.compose.ui.graphics.Color + +data class GroupedBarData(val barData: List, val colors: List = List(barData.count()) { Color.Transparent }) + +internal fun List.totalItems(): Int = this.sumOf { + it.barData.count() +} + +internal fun List.maxYValue() = maxOf { + it.barData.maxYValue() +} diff --git a/charty/src/main/java/com/himanshoe/charty/circle/CircleChart.kt b/charty/src/main/java/com/himanshoe/charty/circle/CircleChart.kt new file mode 100644 index 0000000..877a76f --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/circle/CircleChart.kt @@ -0,0 +1,88 @@ +package com.himanshoe.charty.circle + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import com.himanshoe.charty.circle.config.CircleConfig +import com.himanshoe.charty.circle.config.CircleConfigDefaults +import com.himanshoe.charty.circle.model.CircleData +import com.himanshoe.charty.circle.model.maxYValue + +@Composable +fun CircleChart( + circleData: List, + modifier: Modifier = Modifier, + color: Color, + isAnimated: Boolean = true, + config: CircleConfig = CircleConfigDefaults.circleConfigDefaults() +) { + CircleChart( + circleData = circleData, + colors = listOf(color, color), + modifier = modifier, + config = config, + isAnimated = isAnimated, + ) +} + +@Composable +fun CircleChart( + circleData: List, + modifier: Modifier = Modifier, + colors: List, + config: CircleConfig = CircleConfigDefaults.circleConfigDefaults(), + isAnimated: Boolean +) { + val maxYValueState = rememberSaveable { mutableStateOf(circleData.maxYValue()) } + val maxYValue = maxYValueState.value + val angleFactor = if (config.maxValue != null) 360.div(config.maxValue) else 360.div(maxYValue) + + val animatedFactor = remember { + Animatable(initialValue = 0f) + } + + LaunchedEffect(key1 = true) { + animatedFactor.animateTo( + targetValue = angleFactor, + animationSpec = tween(1000) + ) + } + + Canvas(modifier = modifier) { + val scaleFactor = size.width.div(circleData.count()) + val sizeArc = size.div(scaleFactor) + + circleData.forEachIndexed { index, circleData -> + val circleColor: List = + if (circleData.color != null) listOf(circleData.color, circleData.color) else colors + val arcWidth = sizeArc.width.plus(index.times(scaleFactor)) + val arcHeight = sizeArc.height.plus(index.times(scaleFactor)) + val factor = if (isAnimated) animatedFactor.value else angleFactor + + drawArc( + brush = Brush.linearGradient(circleColor), + startAngle = config.startPosition.angle, + sweepAngle = factor.times(circleData.yValue), + topLeft = Offset( + (size.width - arcWidth).div(2f), + (size.height - arcHeight).div(2f) + ), + useCenter = false, + style = Stroke(width = scaleFactor.div(2.5F), cap = StrokeCap.Round), + size = Size(arcWidth, arcHeight) + ) + } + } +} diff --git a/charty/src/main/java/com/himanshoe/charty/circle/config/CircleConfig.kt b/charty/src/main/java/com/himanshoe/charty/circle/config/CircleConfig.kt new file mode 100644 index 0000000..13494cf --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/circle/config/CircleConfig.kt @@ -0,0 +1,14 @@ +package com.himanshoe.charty.circle.config + +data class CircleConfig( + val startPosition: StartPosition = StartPosition.Top, + val maxValue: Float? +) + +internal object CircleConfigDefaults { + + fun circleConfigDefaults() = CircleConfig( + startPosition = StartPosition.Custom(30F), + maxValue = null, + ) +} diff --git a/charty/src/main/java/com/himanshoe/charty/circle/config/StartPosition.kt b/charty/src/main/java/com/himanshoe/charty/circle/config/StartPosition.kt new file mode 100644 index 0000000..189375b --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/circle/config/StartPosition.kt @@ -0,0 +1,9 @@ +package com.himanshoe.charty.circle.config + +sealed class StartPosition(val angle: Float) { + object Top : StartPosition(270F) + object Right : StartPosition(0F) + object Bottom : StartPosition(900F) + object End : StartPosition(180F) + data class Custom(val customAngle: Float) : StartPosition(customAngle) +} diff --git a/charty/src/main/java/com/himanshoe/charty/circle/model/CircleData.kt b/charty/src/main/java/com/himanshoe/charty/circle/model/CircleData.kt new file mode 100644 index 0000000..343159d --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/circle/model/CircleData.kt @@ -0,0 +1,9 @@ +package com.himanshoe.charty.circle.model + +import androidx.compose.ui.graphics.Color + +data class CircleData(val xValue: Any, val yValue: Float, val color: Color? = null) + +fun List.maxYValue() = maxOf { + it.yValue +} diff --git a/charty/src/main/java/com/himanshoe/charty/common/axis/Axis.kt b/charty/src/main/java/com/himanshoe/charty/common/axis/Axis.kt new file mode 100644 index 0000000..d6376c6 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/common/axis/Axis.kt @@ -0,0 +1,58 @@ +package com.himanshoe.charty.common.axis + +import android.graphics.Paint +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import java.text.DecimalFormat + +internal fun DrawScope.xAxis(axisConfig: AxisConfig, maxValue: Float) { + val graphYAxisEndPoint = size.height.div(4) + val pathEffect = PathEffect.dashPathEffect(floatArrayOf(40f, 20f), 0f) + val labelChunck = maxValue.div(4) + + repeat(5) { index -> + val yAxisEndPoint = graphYAxisEndPoint.times(index) + if (axisConfig.showUnitLabels) { + drawIntoCanvas { + it.nativeCanvas.apply { + drawText( + getLabelText(labelChunck.times(4.minus(index))), + 0F.minus(25), + yAxisEndPoint.minus(10), + Paint().apply { + textSize = size.width.div(30) + textAlign = Paint.Align.CENTER + } + ) + } + } + } + if (index != 0) { + drawLine( + start = Offset(x = 0f, y = yAxisEndPoint), + end = Offset(x = size.width, y = yAxisEndPoint), + color = axisConfig.xAxisColor, + pathEffect = pathEffect, + alpha = 0.2F, + strokeWidth = size.width.div(200) + ) + } + } +} + +internal fun DrawScope.yAxis(axisConfig: AxisConfig) { + val pathEffect = PathEffect.dashPathEffect(floatArrayOf(40f, 20f), 0f) + drawLine( + start = Offset(x = 0f, y = 0F), + end = Offset(x = 0F, y = size.height), + color = axisConfig.yAxisColor, + pathEffect = pathEffect, + alpha = 0.2F, + strokeWidth = size.width.div(200) + ) +} + +private fun getLabelText(value: Float) = DecimalFormat("#.##").format(value).toString() diff --git a/charty/src/main/java/com/himanshoe/charty/common/axis/AxisConfig.kt b/charty/src/main/java/com/himanshoe/charty/common/axis/AxisConfig.kt new file mode 100644 index 0000000..23a3871 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/common/axis/AxisConfig.kt @@ -0,0 +1,20 @@ +package com.himanshoe.charty.common.axis + +import androidx.compose.ui.graphics.Color + +data class AxisConfig( + val showAxes: Boolean, + val showUnitLabels: Boolean, + val xAxisColor: Color = Color.LightGray, + val yAxisColor: Color = Color.LightGray, +) + +internal object AxisConfigDefaults { + + fun axisConfigDefaults() = AxisConfig( + xAxisColor = Color.LightGray, + showAxes = true, + showUnitLabels = true, + yAxisColor = Color.LightGray, + ) +} diff --git a/charty/src/main/java/com/himanshoe/charty/common/dimens/ChartDimens.kt b/charty/src/main/java/com/himanshoe/charty/common/dimens/ChartDimens.kt new file mode 100644 index 0000000..e843d0b --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/common/dimens/ChartDimens.kt @@ -0,0 +1,15 @@ +package com.himanshoe.charty.common.dimens + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class ChartDimens( + val horizontalPadding: Dp +) + +internal object ChartDimensDefaults { + + fun chartDimesDefaults() = ChartDimens( + horizontalPadding = 4.dp + ) +} diff --git a/charty/src/main/java/com/himanshoe/charty/line/CurveLineChart.kt b/charty/src/main/java/com/himanshoe/charty/line/CurveLineChart.kt new file mode 100644 index 0000000..776af05 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/line/CurveLineChart.kt @@ -0,0 +1,191 @@ +package com.himanshoe.charty.line + +import android.graphics.PointF +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.graphics.drawscope.Stroke +import com.himanshoe.charty.common.axis.AxisConfig +import com.himanshoe.charty.common.axis.AxisConfigDefaults +import com.himanshoe.charty.common.axis.xAxis +import com.himanshoe.charty.common.axis.yAxis +import com.himanshoe.charty.common.dimens.ChartDimens +import com.himanshoe.charty.common.dimens.ChartDimensDefaults +import com.himanshoe.charty.line.config.CurveLineConfig +import com.himanshoe.charty.line.config.CurveLineConfigDefaults +import com.himanshoe.charty.line.model.LineData +import com.himanshoe.charty.line.model.maxYValue + +@Composable +fun CurveLineChart( + lineData: List, + color: Color, + modifier: Modifier = Modifier, + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + curveLineConfig: CurveLineConfig = CurveLineConfigDefaults.curveLineConfigDefaults() +) { + CurveLineChart( + modifier = modifier, + lineData = lineData, + colors = listOf(color, color), + chartDimens = chartDimens, + axisConfig = axisConfig, + curveLineConfig = curveLineConfig + ) +} + +@Composable +fun CurveLineChart( + lineData: List, + colors: List, + modifier: Modifier = Modifier, + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + curveLineConfig: CurveLineConfig = CurveLineConfigDefaults.curveLineConfigDefaults() +) { + val graphPathPoints = mutableListOf() + val backgroundPathPoints = mutableListOf() + val lineBound = remember { mutableStateOf(0F) } + val maxYValueState = rememberSaveable { mutableStateOf(lineData.maxYValue()) } + val maxYValue = maxYValueState.value + + Canvas( + modifier = modifier + .fillMaxSize() + .padding(horizontal = chartDimens.horizontalPadding) + .drawBehind { + if (axisConfig.showAxes) { + xAxis(axisConfig, maxYValue) + yAxis(axisConfig) + } + }, + onDraw = { + val xScaleFactor = size.width.div(lineData.size) + val yScaleFactor = size.height.div(maxYValue) + val canvasSize = size + val radius = size.width.div(70) + + lineBound.value = size.width.div(lineData.count().times(1.2F)) + + val lineDataItems: List = lineData.mapIndexed { index, data -> + dataToOffSet( + index = index, + bound = lineBound.value, + size = size, + data = data, + scaleFactor = yScaleFactor + ) + }.toMutableList().also { + it.add(Offset(canvasSize.width, canvasSize.height)) + } + val offsetItems: List = mutableListOf().apply { + add(Offset(0f, canvasSize.height)) + addAll(lineDataItems) + } + + val xValues = offsetItems.map { it.x } + val pointsPath = Path() + offsetItems.forEachIndexed { index, offset -> + val canDrawCircle = curveLineConfig.hasDotMarker && index != 0 && index != offsetItems.size.minus(1) + if (canDrawCircle) { + drawCircle( + color = curveLineConfig.dotColor, + radius = radius, + center = Offset(offset.x, offset.y) + ) + } + if (index > 0) { + storePoints( + graphPathPoints, + backgroundPathPoints, + offset, + offsetItems[index.minus(1)] + ) + } + } + + pointsPath.apply { + reset() + moveTo(offsetItems.first().x, offsetItems.first().y) + (0.until(offsetItems.size .minus(1))).forEach { index -> + cubicTo( + graphPathPoints[index].x, graphPathPoints[index].y, + backgroundPathPoints[index].x, backgroundPathPoints[index].y, + offsetItems[index.plus(1)].x, offsetItems[index.plus(1)].y + ) + } + } + + val backgroundPath = android.graphics.Path(pointsPath.asAndroidPath()) + .asComposePath() + .apply { + lineTo(xScaleFactor.times(xValues.last()), size.height.minus(yScaleFactor)) + lineTo(xScaleFactor, size.height - yScaleFactor) + close() + } + drawPath( + path = backgroundPath, + brush = Brush.verticalGradient( + colors = colors, + endY = size.height - yScaleFactor + ), + ) + drawPath( + path = pointsPath, + color = Color.Black, + style = Stroke( + width = 5F, + cap = StrokeCap.Round + ) + ) + } + ) +} + +private fun storePoints( + controlPoints1: MutableList, + controlPoints2: MutableList, + firstOffset: Offset, + previousOffset: Offset +) { + controlPoints1.add( + PointF( + (firstOffset.x + previousOffset.x) / 2, + previousOffset.y + ) + ) + controlPoints2.add( + PointF( + (firstOffset.x + previousOffset.x) / 2, + firstOffset.y + ) + ) +} + +private fun dataToOffSet( + index: Int, + bound: Float, + size: Size, + data: LineData, + scaleFactor: Float, +): Offset { + val startX = index.times(bound.times(1.2F)) + val endX = index.plus(1).times(bound.times(1.2F)) + val y = size.height.minus(data.yValue.times(scaleFactor)) + return Offset(((startX.plus(endX)).div(2F)), y) +} diff --git a/charty/src/main/java/com/himanshoe/charty/line/LineChart.kt b/charty/src/main/java/com/himanshoe/charty/line/LineChart.kt new file mode 100644 index 0000000..a5203a8 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/line/LineChart.kt @@ -0,0 +1,147 @@ +package com.himanshoe.charty.line + +import android.graphics.Paint +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import com.himanshoe.charty.common.axis.AxisConfig +import com.himanshoe.charty.common.axis.AxisConfigDefaults +import com.himanshoe.charty.common.axis.xAxis +import com.himanshoe.charty.common.axis.yAxis +import com.himanshoe.charty.common.dimens.ChartDimens +import com.himanshoe.charty.common.dimens.ChartDimensDefaults +import com.himanshoe.charty.line.config.LineConfig +import com.himanshoe.charty.line.config.LineConfigDefaults +import com.himanshoe.charty.line.model.LineData +import com.himanshoe.charty.line.model.maxYValue + +@Composable +fun LineChart( + lineData: List, + color: Color, + modifier: Modifier = Modifier, + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + lineConfig: LineConfig = LineConfigDefaults.lineConfigDefaults() +) { + LineChart( + lineData = lineData, + colors = listOf(color, color), + modifier = modifier, + chartDimens = chartDimens, + axisConfig = axisConfig, + lineConfig = lineConfig + ) +} + +@Composable +fun LineChart( + lineData: List, + colors: List, + modifier: Modifier = Modifier, + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + lineConfig: LineConfig = LineConfigDefaults.lineConfigDefaults() +) { + val maxYValueState = rememberSaveable { mutableStateOf(lineData.maxYValue()) } + val maxYValue = maxYValueState.value + val lineBound = remember { mutableStateOf(0F) } + + Canvas( + modifier = modifier + .drawBehind { + if (axisConfig.showAxes) { + xAxis(axisConfig, maxYValue) + yAxis(axisConfig) + } + } + .padding(horizontal = chartDimens.horizontalPadding) + + ) { + lineBound.value = size.width.div(lineData.count().times(1.2F)) + val scaleFactor = size.height.div(maxYValue) + val brush = Brush.linearGradient(colors) + val radius = size.width.div(70) + val strokeWidth = size.width.div(100) + val path = Path().apply { + moveTo(0f, size.height) + } + + val lastIndex = lineData.size - 1 + lineData.forEachIndexed { index, data -> + val centerOffset = dataToOffSet(index, lineBound.value, size, data, scaleFactor) + val drawnPath = path.lineTo(centerOffset.x, centerOffset.y) + when (index) { + lastIndex -> drawnPath.also { + path.lineTo(size.width, size.height) + } + else -> drawnPath + } + if (lineConfig.hasDotMarker) { + drawCircle( + center = centerOffset, + radius = radius, + brush = brush + ) + } + drawXLabel(data, centerOffset, radius) + } + val stroke = if (lineConfig.hasSmoothCurve) { + Stroke( + width = strokeWidth, + pathEffect = PathEffect.cornerPathEffect(strokeWidth) + ) + } else { + Stroke(width = strokeWidth) + } + drawPath( + path = path, + brush = brush, + style = stroke, + ) + } +} + +private fun DrawScope.drawXLabel(data: LineData, centerOffset: Offset, radius: Float) { + drawIntoCanvas { + it.nativeCanvas.apply { + drawText( + data.xValue.toString(), + centerOffset.x, + size.height.plus(radius.times(4)), + Paint().apply { + textSize = size.width.div(30) + textAlign = Paint.Align.CENTER + } + ) + } + } +} + +private fun dataToOffSet( + index: Int, + bound: Float, + size: Size, + data: LineData, + yChunck: Float +): Offset { + val startX = index.times(bound.times(1.2F)) + val endX = index.plus(1).times(bound.times(1.2F)) + val y = size.height.minus(data.yValue.times(yChunck)) + return Offset(((startX.plus(endX)).div(2F)), y) +} diff --git a/charty/src/main/java/com/himanshoe/charty/line/config/CurveLineConfig.kt b/charty/src/main/java/com/himanshoe/charty/line/config/CurveLineConfig.kt new file mode 100644 index 0000000..90f9963 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/line/config/CurveLineConfig.kt @@ -0,0 +1,16 @@ +package com.himanshoe.charty.line.config + +import androidx.compose.ui.graphics.Color + +data class CurveLineConfig( + val hasDotMarker: Boolean, + val dotColor: Color, +) + +internal object CurveLineConfigDefaults { + + fun curveLineConfigDefaults() = CurveLineConfig( + hasDotMarker = true, + dotColor = Color.Green + ) +} diff --git a/charty/src/main/java/com/himanshoe/charty/line/config/LineConfig.kt b/charty/src/main/java/com/himanshoe/charty/line/config/LineConfig.kt new file mode 100644 index 0000000..9d25ae8 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/line/config/LineConfig.kt @@ -0,0 +1,14 @@ +package com.himanshoe.charty.line.config + +data class LineConfig( + val hasSmoothCurve: Boolean = true, + val hasDotMarker: Boolean +) + +internal object LineConfigDefaults { + + fun lineConfigDefaults() = LineConfig( + hasSmoothCurve = true, + hasDotMarker = true + ) +} diff --git a/charty/src/main/java/com/himanshoe/charty/line/model/LineData.kt b/charty/src/main/java/com/himanshoe/charty/line/model/LineData.kt new file mode 100644 index 0000000..406469e --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/line/model/LineData.kt @@ -0,0 +1,7 @@ +package com.himanshoe.charty.line.model + +data class LineData(val xValue: Any, val yValue: Float) + +fun List.maxYValue() = maxOf { + it.yValue +} diff --git a/charty/src/main/java/com/himanshoe/charty/pie/PieChart.kt b/charty/src/main/java/com/himanshoe/charty/pie/PieChart.kt new file mode 100644 index 0000000..9192901 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/pie/PieChart.kt @@ -0,0 +1,185 @@ +package com.himanshoe.charty.pie + +import android.graphics.Paint +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import com.himanshoe.charty.pie.config.PieConfig +import com.himanshoe.charty.pie.config.PieConfigDefaults +import kotlin.math.min +import kotlin.math.roundToInt + +@Composable +fun PieChart( + data: List, + modifier: Modifier = Modifier, + config: PieConfig = PieConfigDefaults.pieConfigDefaults(data.count()), + isDonut: Boolean = true, + valueTextColor: Color = Color.Black, + onSectionClicked: (Float, Float) -> Unit = { _, _ -> } +) { + + if (data.isEmpty()) return + + val total = data.sum() + val proportions = data.map { + it.times(100).div(total) + } + val angleProgress = proportions.map { + 360.times(it).div(100) + } + + val currentProgressSize = mutableListOf().apply { + add(angleProgress.first()) + }.also { + (1.until(angleProgress.size)).forEach { x -> + it.add(angleProgress[x].plus(it[x.minus(1)])) + } + } + + val currentPie = remember { mutableStateOf(-1) } + var startAngle = 270f + + BoxWithConstraints(modifier = modifier) { + + val sideSize = min(constraints.maxWidth, constraints.maxHeight) + val padding = (sideSize.times(if (isDonut) 30 else 20)).div(100f) + + val pathPortion = remember { Animatable(initialValue = 0f) } + LaunchedEffect(true) { pathPortion.animateTo(1f, animationSpec = tween(1000)) } + + val size = Size(sideSize.toFloat().minus(padding), sideSize.toFloat().minus(padding)) + + Canvas( + modifier = Modifier + .width(sideSize.dp) + .height(sideSize.dp) + .handlePiePortionClick(sideSize, currentProgressSize, currentPie) { + onSectionClicked(proportions[it], data[it]) + } + ) { + + angleProgress.forEachIndexed { index, arcProgress -> + drawPie( + color = config.colors[index], + startAngle = startAngle, + arcProgress = arcProgress.times(pathPortion.value), + size = size, + padding = padding, + isDonut = isDonut, + isActive = currentPie.value == index + ) + startAngle += arcProgress + } + + if (currentPie.value != -1 && isDonut) { + drawPieSection(proportions, currentPie.value, valueTextColor, sideSize) + } + } + } +} + +fun DrawScope.drawPieSection( + proportions: List, + currentPieValue: Int, + percentColor: Color, + sideSize: Int +) { + drawContext.canvas.nativeCanvas.apply { + val fontSize = size.width.div(20).toDp().toPx() + + drawText( + "${proportions[currentPieValue].roundToInt()}%", + (sideSize.div(2)) + fontSize.div(4), (sideSize.div(2)) + fontSize.div(3), + Paint().apply { + color = percentColor.toArgb() + textSize = fontSize + textAlign = Paint.Align.CENTER + } + ) + } +} + +private fun Modifier.handlePiePortionClick( + sideSize: Int, + currentProgressSize: MutableList, + currentPie: MutableState, + onIndexSelected: (Int) -> Unit +): Modifier = pointerInput(true) { + detectTapGestures { offset -> + val clickedAngle = convertTouchEventPointToAngle( + sideSize.toFloat(), + sideSize.toFloat(), + offset.x, + offset.y + ) + currentProgressSize.forEachIndexed { index, item -> + if (clickedAngle <= item) { + if (currentPie.value != index) { + currentPie.value = index + } + onIndexSelected(currentPie.value) + + return@detectTapGestures + } + } + } +} + +private fun DrawScope.drawPie( + color: Color, + startAngle: Float, + arcProgress: Float, + size: Size, + padding: Float, + isDonut: Boolean = false, + isActive: Boolean = false +): Path { + + return Path().apply { + drawArc( + color = color, + startAngle = startAngle, + sweepAngle = arcProgress, + useCenter = !isDonut, + size = size, + style = if (isDonut) + Stroke( + width = if (isActive) size.width.div(1.5F) else size.width.div(2), + ) else Fill, + + topLeft = Offset(padding / 2, padding / 2) + ) + } +} + +private fun convertTouchEventPointToAngle( + width: Float, + height: Float, + xPos: Float, + yPos: Float +): Double { + val x = xPos - (width * 0.5f) + val y = yPos - (height * 0.5f) + + var angle = Math.toDegrees(kotlin.math.atan2(y.toDouble(), x.toDouble()) + Math.PI / 2) + angle = if (angle < 0) angle + 360 else angle + return angle +} diff --git a/charty/src/main/java/com/himanshoe/charty/pie/config/PieConfig.kt b/charty/src/main/java/com/himanshoe/charty/pie/config/PieConfig.kt new file mode 100644 index 0000000..3454066 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/pie/config/PieConfig.kt @@ -0,0 +1,24 @@ +package com.himanshoe.charty.pie.config + +import androidx.compose.ui.graphics.Color +import kotlin.random.Random + +data class PieConfig( + val colors: List, +) + +internal object PieConfigDefaults { + + fun pieConfigDefaults(count: Int) = PieConfig( + colors = generateRandomColor(count), + ) +} + +private fun generateRandomColor(count: Int): List { + val colors = mutableListOf() + repeat(count) { + val color = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)) + colors.add(color) + } + return colors.toList() +} diff --git a/charty/src/main/java/com/himanshoe/charty/point/PointChart.kt b/charty/src/main/java/com/himanshoe/charty/point/PointChart.kt new file mode 100644 index 0000000..f9af12c --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/point/PointChart.kt @@ -0,0 +1,126 @@ +package com.himanshoe.charty.point + +import android.graphics.Paint +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import com.himanshoe.charty.common.axis.AxisConfig +import com.himanshoe.charty.common.axis.AxisConfigDefaults +import com.himanshoe.charty.common.axis.xAxis +import com.himanshoe.charty.common.axis.yAxis +import com.himanshoe.charty.common.dimens.ChartDimens +import com.himanshoe.charty.common.dimens.ChartDimensDefaults +import com.himanshoe.charty.point.cofig.PointConfig +import com.himanshoe.charty.point.cofig.PointConfigDefaults +import com.himanshoe.charty.point.cofig.PointType +import com.himanshoe.charty.point.model.PointData +import com.himanshoe.charty.point.model.maxYValue + +@Composable +fun PointChart( + pointData: List, + colors: List, + modifier: Modifier = Modifier, + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + pointConfig: PointConfig = PointConfigDefaults.pointConfigDefaults() +) { + val maxYValueState = rememberSaveable { mutableStateOf(pointData.maxYValue()) } + val maxYValue = maxYValueState.value + val pointBound = remember { mutableStateOf(0F) } + + Canvas( + modifier = modifier + .drawBehind { + if (axisConfig.showAxes) { + xAxis(axisConfig, maxYValue) + yAxis(axisConfig) + } + } + .padding(horizontal = chartDimens.horizontalPadding) + + ) { + pointBound.value = size.width.div(pointData.count().times(1.2F)) + val yChunck = size.height.div(maxYValue) + val brush = Brush.linearGradient(colors) + val radius = size.width.div(70) + pointData.forEachIndexed { index, data -> + val centerOffset = dataToOffSet(index, pointBound.value, size, data, yChunck) + val style = when (pointConfig.pointType) { + is PointType.Stroke -> Stroke(width = size.width.div(100)) + else -> Fill + } + + drawCircle( + center = centerOffset, + style = style, + radius = radius, + brush = brush + ) + // draw label + drawXLabel(data, centerOffset, radius) + } + } +} + +private fun DrawScope.drawXLabel(data: PointData, centerOffset: Offset, radius: Float) { + drawIntoCanvas { + it.nativeCanvas.apply { + drawText( + data.xValue.toString(), + centerOffset.x, + size.height.plus(radius.times(4)), + Paint().apply { + textSize = size.width.div(30) + textAlign = Paint.Align.CENTER + } + ) + } + } +} + +@Composable +fun PointChart( + pointData: List, + color: Color, + modifier: Modifier = Modifier, + chartDimens: ChartDimens = ChartDimensDefaults.chartDimesDefaults(), + axisConfig: AxisConfig = AxisConfigDefaults.axisConfigDefaults(), + pointConfig: PointConfig = PointConfigDefaults.pointConfigDefaults() +) { + PointChart( + pointData = pointData, + colors = listOf(color, color), + modifier = modifier, + chartDimens = chartDimens, + axisConfig = axisConfig, + pointConfig = pointConfig + ) +} + +private fun dataToOffSet( + index: Int, + bound: Float, + size: Size, + data: PointData, + yChunck: Float +): Offset { + val startX = index.times(bound.times(1.2F)) + val endX = index.plus(1).times(bound.times(1.2F)) + val y = size.height.minus(data.yValue.times(yChunck)) + return Offset(((startX.plus(endX)).div(2F)), y) +} diff --git a/charty/src/main/java/com/himanshoe/charty/point/cofig/PointConfig.kt b/charty/src/main/java/com/himanshoe/charty/point/cofig/PointConfig.kt new file mode 100644 index 0000000..a1b4fc7 --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/point/cofig/PointConfig.kt @@ -0,0 +1,17 @@ +package com.himanshoe.charty.point.cofig + +data class PointConfig( + val pointType: PointType, +) + +internal object PointConfigDefaults { + + fun pointConfigDefaults() = PointConfig( + pointType = PointType.Stroke + ) +} + +sealed interface PointType { + object Fill : PointType + object Stroke : PointType +} diff --git a/charty/src/main/java/com/himanshoe/charty/point/model/PointData.kt b/charty/src/main/java/com/himanshoe/charty/point/model/PointData.kt new file mode 100644 index 0000000..36b297d --- /dev/null +++ b/charty/src/main/java/com/himanshoe/charty/point/model/PointData.kt @@ -0,0 +1,7 @@ +package com.himanshoe.charty.point.model + +data class PointData(val xValue: Any, val yValue: Float) + +fun List.maxYValue() = maxOf { + it.yValue +} diff --git a/charty/src/test/java/com/himanshoe/charty/ExampleUnitTest.kt b/charty/src/test/java/com/himanshoe/charty/ExampleUnitTest.kt new file mode 100644 index 0000000..77df319 --- /dev/null +++ b/charty/src/test/java/com/himanshoe/charty/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.himanshoe.charty + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/docs/BarChart.md b/docs/BarChart.md new file mode 100644 index 0000000..5caa4a5 --- /dev/null +++ b/docs/BarChart.md @@ -0,0 +1,40 @@ +## BarChart + +> A bar chart or bar graph is a chart or graph that presents categorical data with rectangular bars with heights or lengths proportional to the values that they represent + +### Using BarChart in your project: + +1. When you want a gradient shade + +```kotlin + BarChart( + modifier = Modifier, + onBarClick = { // handle Click for individual bar} + colors = // colors + barData = // list of BarData + ) +``` + +2. When you want a solid shade: + +```kotlin + BarChart( + modifier = Modifier, + onBarClick = { // handle Click for individual bar}, + color = // colors + barData = // list of BarData + ) +``` + +### Creating Data Set: + +to create a data set you need to pass List of `BarData`, where `BarData` looks like: +```kotlin +data class BarData(val xValue: Any, val yValue: Float) +``` +Here, `xValue` will be used as Labels and `yValue` will represent the bars. + +### Additional Configuration (Optional) +- To add padding the the chart, you can also use `ChartDimens` +- To edit Config of the Axis, to suit your need to use `AxisConfig` +- To edit Individual Bar config of it having corner radius you need to use `BarConfig` diff --git a/docs/CircleChart.md b/docs/CircleChart.md new file mode 100644 index 0000000..f441a68 --- /dev/null +++ b/docs/CircleChart.md @@ -0,0 +1,38 @@ +## CircleChart + +> A circle chart or bar graph is a chart or graph that presents categorical data with circular progress + +### Using CircleChart in your project: + +1. When you want a gradient shade + +```kotlin + CircleChart( + modifier = Modifier, + isAnimated = true/false, + colors = // colors, + circleData = // list of CircleData + ) +``` + +2. When you want a solid shade: +```kotlin + CircleChart( + modifier = Modifier, + isAnimated = true/false, + color = // color + circleData = // list of CircleData + ) +``` + +### Creating Data Set: + +to create a data set you need to pass List of `CircleData`, where `CircleData` looks like: +```kotlin +data class CircleData(val xValue: Any, val yValue: Float, val color: Color? = null) +``` +Here, `xValue` will be used as label, `yValue` will act as progress and `color` is optional parameter but if provided that will represent the individual color of the progress + +### Additional Configuration (Optional) +- To change the start position of the Circle, you can use `StartPosition` with either of provided values or your custom angle. +- You can also provide a `maxValue` to wrap the max progress of the chart. diff --git a/docs/CurveLineChart.md b/docs/CurveLineChart.md new file mode 100644 index 0000000..83573ae --- /dev/null +++ b/docs/CurveLineChart.md @@ -0,0 +1,38 @@ +## CurveLineChart + +> A curve line chart chart or graph that presents categorical data with line progress views with curved lines with heights proportional to the values that they represent + +### Using CurveLineChart in your project: + +1. When you want a gradient shade + +```kotlin + CurveLineChart( + modifier = Modifier, + colors = // colors + lineData = // list of LineData + ) +``` + +2. When you want a solid shade: + +```kotlin + CurveLineChart( + modifier = Modifier, + color = // colors + lineData = // list of LineData + ) +``` + +### Creating Data Set: + +to create a data set you need to pass List of `LineData`, where `LineData` looks like: +```kotlin +data class LineData(val xValue: Any, val yValue: Float) +``` +Here, `xValue` will be used as Labels and `yValue` will represent the progress. + +### Additional Configuration (Optional) +- To add padding the the chart, you can also use `ChartDimens` +- To edit Config of the Axis, to suit your need to use `AxisConfig` +- To edit the line representation like `smooth curve` or having additional view of `dot market` use `LineConfig` diff --git a/docs/GroupedBarChart.md b/docs/GroupedBarChart.md new file mode 100644 index 0000000..864689f --- /dev/null +++ b/docs/GroupedBarChart.md @@ -0,0 +1,29 @@ +## GroupedBarChart + +> grouped bar charts are Bar charts in which multiple sets of data items are compared, with a single color used to denote a specific series across all sets. + +### Using GroupedBarChart in your project: + +1. When you want a gradient shade + +```kotlin + GroupedBarChart( + modifier = Modifier, + onBarClick = { // handle Click for individual bar}, + colors = // colors + groupedBarData = // list of GroupedBarData + ) +``` + +### Creating Data Set: + +to create a data set you need to pass List of `GroupedBarData`, where `GroupedBarData` looks like: +```kotlin +data class GroupedBarData(val barData: List, val colors: List) +``` +Here, `barData` will be used as Creating group of bar and `colors` will represent the colors of the individual bar. + +### Additional Configuration (Optional) +- To add padding the the chart, you can also use `ChartDimens` +- To edit Config of the Axis, to suit your need to use `AxisConfig` +- To edit Individual Bar config of it having corner radius you need to use `BarConfig` diff --git a/docs/LineChart.md b/docs/LineChart.md new file mode 100644 index 0000000..482d237 --- /dev/null +++ b/docs/LineChart.md @@ -0,0 +1,38 @@ +## LineChart + +> A line chart chart or graph that presents categorical data with line progress views with heights proportional to the values that they represent + +### Using LineChart in your project: + +1. When you want a gradient shade + +```kotlin + LineChart( + modifier = Modifier, + colors = // colors + lineData = // list of LineData + ) +``` + +2. When you want a solid shade: + +```kotlin + LineChart( + modifier = Modifier, + color = // colors + lineData = // list of LineData + ) +``` + +### Creating Data Set: + +to create a data set you need to pass List of `LineData`, where `LineData` looks like: +```kotlin +data class LineData(val xValue: Any, val yValue: Float) +``` +Here, `xValue` will be used as Labels and `yValue` will represent the progress. + +### Additional Configuration (Optional) +- To add padding the the chart, you can also use `ChartDimens` +- To edit Config of the Axis, to suit your need to use `AxisConfig` +- To edit the line representation like `smooth curve` or having additional view of `dot market` use `LineConfig` diff --git a/docs/PieChart.md b/docs/PieChart.md new file mode 100644 index 0000000..07e8cf1 --- /dev/null +++ b/docs/PieChart.md @@ -0,0 +1,29 @@ +## PieChart + +### Using PieChart in your project: +> A PieChart is a graph in which a circle is divided into sectors that each represent a proportion of the whole. + +1. When you want a gradient shade + +```kotlin + PieChart( + modifier = Modifier, + isDonut = true/false, + onSectionClicked = {}, + valueTextColor = //Color, + colors = // colors + data = // list of Floats , + ) +``` + +### Creating Data Set: + +to create a data set you need to pass List of `Float`, where it will draw the value in proportion. + +### Additional Configuration (Optional) +- To add your color set use `PieConfig` to pass on your set of color, make sure you use colors of total size of data. +- To make the chart looks a donut make `isDonut` as `true` + + +### Mention: +- Muñoz : For base work inspiration for PieChart library diff --git a/docs/PointChart.md b/docs/PointChart.md new file mode 100644 index 0000000..2464178 --- /dev/null +++ b/docs/PointChart.md @@ -0,0 +1,36 @@ +## PointChart + +### Using PointChart in your project: + +1. When you want a gradient shade + +```kotlin + PointChart( + modifier = Modifier, + colors = // colors + pointData = // list of PointData + ) +``` + +2. When you want a solid shade: + +```kotlin + PointChart( + modifier = Modifier, + color = // colors + pointData = // list of PointData + ) +``` + +### Creating Data Set: + +to create a data set you need to pass List of `PointData`, where `PointData` looks like: +```kotlin +data class PointData(val xValue: Any, val yValue: Float) +``` +Here, `xValue` will be used as Labels and `yValue` will represent the progress. + +### Additional Configuration (Optional) +- To add padding the the chart, you can also use `ChartDimens` +- To edit Config of the Axis, to suit your need to use `AxisConfig` +- To edit the point representation use `PointConfig` diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3c7a8bd --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# 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 +# 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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f36ac86 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Aug 09 18:18:18 CEST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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 diff --git a/img/banner.png b/img/banner.png new file mode 100644 index 0000000..37db192 Binary files /dev/null and b/img/banner.png differ diff --git a/img/charty-banner.png b/img/charty-banner.png new file mode 100644 index 0000000..b8aaa12 Binary files /dev/null and b/img/charty-banner.png differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..af98932 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "charty-library" +include(":app") +include(":charty")