的
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,76 @@
|
||||
import com.vanniktech.maven.publish.SonatypeHost
|
||||
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.dokka'
|
||||
id 'com.vanniktech.maven.publish' version "0.25.3"
|
||||
|
||||
}
|
||||
apply from: '../quality/static-check.gradle'
|
||||
|
||||
android {
|
||||
namespace 'com.himanshoe.charty'
|
||||
compileSdk 33
|
||||
|
||||
defaultConfig {
|
||||
minSdk 24
|
||||
targetSdk 33
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.4.7'
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.4.7'
|
||||
}
|
||||
}
|
||||
mavenPublishing {
|
||||
publishToMavenCentral(SonatypeHost.S01)
|
||||
signAllPublications()
|
||||
}
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
|
||||
//Compose
|
||||
implementation platform('androidx.compose:compose-bom:2023.05.01')
|
||||
implementation 'androidx.compose.ui:ui'
|
||||
implementation 'androidx.compose.ui:ui-graphics'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
debugImplementation "androidx.compose.ui:ui-tooling"
|
||||
implementation 'androidx.compose.ui:ui-util'
|
||||
implementation 'androidx.compose.material:material-icons-extended'
|
||||
|
||||
//time
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.4.0'
|
||||
lintChecks('com.slack.lint.compose:compose-lint-checks:1.2.0')
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
||||
dokkaPlugin("org.jetbrains.dokka:android-documentation-plugin:1.8.20")
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# /***************
|
||||
# * Charty Library : Android
|
||||
# *
|
||||
# * Copyright (c) 2023. Charty Contributor
|
||||
# ****************/
|
||||
#
|
||||
# Maven
|
||||
POM_ARTIFACT_ID=charty
|
||||
POM_NAME=charty
|
||||
POM_DESCRIPTION=An Elementary Compose Chart library.
|
||||
POM_PACKAGING=aar
|
||||
POM_INCEPTION_YEAR=2023
|
||||
GROUP=com.himanshoe
|
||||
VERSION_NAME=2.0.0-alpha01
|
||||
VERSION_CODE=2
|
||||
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
|
||||
Vendored
+21
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ /***************
|
||||
~ * Charty Library : Android
|
||||
~ *
|
||||
~ * Copyright (c) 2023. Charty Contributor
|
||||
~ ****************/
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.area
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.himanshoe.charty.area.model.AreaData
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.ComposeList
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.toComposeList
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
|
||||
/**
|
||||
* A composable function that displays an area chart based on the provided area data.
|
||||
*
|
||||
* @param areaData The collection of area data containing labels and points that define the areas on the chart.
|
||||
* @param modifier Modifier for applying styling and positioning to the chart.
|
||||
* @param axisConfig Configuration for the chart axes, including color and stroke width.
|
||||
* @param padding Padding around the chart content to create spacing.
|
||||
*/
|
||||
@Composable
|
||||
fun AreaChart(
|
||||
areaData: ComposeList<AreaData>,
|
||||
modifier: Modifier = Modifier,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
padding: Dp = 16.dp,
|
||||
) {
|
||||
val items = areaData.data.flatMap { it.points }
|
||||
val maxValue = items.maxOrNull() ?: 0F
|
||||
val minValue = items.minOrNull() ?: 0f
|
||||
var chartWidth by remember { mutableStateOf(0F) }
|
||||
var chartHeight by remember { mutableStateOf(0F) }
|
||||
|
||||
ChartSurface(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
padding = padding,
|
||||
chartData = items.toComposeList(),
|
||||
content = {
|
||||
Column {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
|
||||
val xStep = width / (areaData.data[0].points.size - 1)
|
||||
val yStep = height / (maxValue - minValue)
|
||||
|
||||
areaData.data.fastForEach { data ->
|
||||
val dataPoints = data.points
|
||||
val dataPointsPath = Path()
|
||||
|
||||
dataPointsPath.moveTo(0f, height - ((dataPoints[0] - minValue) * yStep))
|
||||
|
||||
for (i in 1 until dataPoints.size) {
|
||||
val x = i * xStep
|
||||
val y = height - ((dataPoints[i] - minValue) * yStep)
|
||||
dataPointsPath.lineTo(x, y)
|
||||
}
|
||||
|
||||
dataPointsPath.lineTo(width, height)
|
||||
dataPointsPath.lineTo(0f, height)
|
||||
|
||||
drawPath(
|
||||
path = dataPointsPath,
|
||||
color = data.color,
|
||||
style = Fill,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.area.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Data class representing an area on the AreaChart.
|
||||
*
|
||||
* @param xValue The x-value of the area. It can be of any type (e.g., String, Int, Float, etc.).
|
||||
* @param points The list of y-values that define the area's shape. Each y-value corresponds to a specific data point.
|
||||
* @param color The color of the area.
|
||||
*/
|
||||
@Immutable
|
||||
data class AreaData(
|
||||
val xValue: Any,
|
||||
val points: List<Float>,
|
||||
val color: Color
|
||||
)
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.bar
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.bar.model.BarData
|
||||
import com.himanshoe.charty.common.ChartDataCollection
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.maxYValue
|
||||
import com.himanshoe.charty.common.minYValue
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawXAxisLabels
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
|
||||
/**
|
||||
* A composable function that displays a bar chart based on the provided data collection.
|
||||
*
|
||||
* @param dataCollection The collection of chart data representing the bars in the chart.
|
||||
* @param modifier Modifier for applying styling and positioning to the chart.
|
||||
* @param barSpacing The spacing between bars in the chart.
|
||||
* @param padding Padding around the chart content to create spacing.
|
||||
* @param barColor The color used to fill the bars in the chart.
|
||||
* @param axisConfig Configuration for the chart axes, including color and stroke width.
|
||||
*/
|
||||
@Composable
|
||||
fun BarChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
barSpacing: Dp = 8.dp,
|
||||
padding: Dp = 16.dp,
|
||||
barColor: Color = Color.Blue,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
) {
|
||||
val points = dataCollection.data
|
||||
|
||||
val screenWidth = with(LocalContext.current) { resources.displayMetrics.widthPixels.toFloat() }
|
||||
var chartBound by remember { mutableStateOf(0F) }
|
||||
var chartWidth by remember { mutableStateOf(0F) }
|
||||
var chartHeight by remember { mutableStateOf(0F) }
|
||||
|
||||
ChartSurface(
|
||||
padding = padding,
|
||||
chartData = dataCollection,
|
||||
modifier = modifier,
|
||||
axisConfig = axisConfig
|
||||
) {
|
||||
val data = dataCollection.data
|
||||
|
||||
androidx.compose.foundation.Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
chartBound = chartWidth.div(
|
||||
points
|
||||
.count()
|
||||
.times(1.2F)
|
||||
)
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val maxValue = dataCollection.maxYValue()
|
||||
val barCount = data.size
|
||||
val minValue = dataCollection.minYValue()
|
||||
val range = maxValue - minValue
|
||||
val availableWidth = chartWidth - (barSpacing.toPx() * (barCount - 1))
|
||||
val maxBarWidth = availableWidth / barCount
|
||||
|
||||
data.fastForEachIndexed { index, barData ->
|
||||
val barWidth = maxBarWidth.coerceAtMost(screenWidth / barCount)
|
||||
|
||||
val barHeight =
|
||||
(barData.yValue - minValue) / range * (chartHeight - axisConfig.axisStroke)
|
||||
|
||||
val startX = index * (barWidth + barSpacing.toPx())
|
||||
val startY = chartHeight - barHeight
|
||||
|
||||
if (barData is BarData) {
|
||||
drawRect(
|
||||
color = barColor,
|
||||
topLeft = Offset(startX, startY),
|
||||
size = Size(barWidth, barHeight)
|
||||
)
|
||||
|
||||
if (data.count() < 14) {
|
||||
drawXAxisLabels(
|
||||
data = barData.xValue,
|
||||
center = Offset(startX + barWidth / 2, startY),
|
||||
count = data.count(),
|
||||
padding = padding.toPx(),
|
||||
minLabelCount = axisConfig.minLabelCount,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw NoSuchFieldException("$barData should have color")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable function that displays a bar chart based on the provided data collection.
|
||||
*
|
||||
* @param dataCollection The collection of chart data representing the bars in the chart.
|
||||
* @param modifier Modifier for applying styling and positioning to the chart.
|
||||
* @param barSpacing The spacing between bars in the chart.
|
||||
* @param padding Padding around the chart content to create spacing.
|
||||
* @param axisConfig Configuration for the chart axes, including color and stroke width.
|
||||
*/
|
||||
@Composable
|
||||
fun BarChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
barSpacing: Dp = 8.dp,
|
||||
padding: Dp = 16.dp,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
) {
|
||||
val points = dataCollection.data
|
||||
|
||||
val screenWidth = with(LocalContext.current) { resources.displayMetrics.widthPixels.toFloat() }
|
||||
var chartBound by remember { mutableStateOf(0F) }
|
||||
var chartWidth by remember { mutableStateOf(0F) }
|
||||
var chartHeight by remember { mutableStateOf(0F) }
|
||||
|
||||
ChartSurface(
|
||||
padding = padding,
|
||||
chartData = dataCollection,
|
||||
modifier = modifier,
|
||||
axisConfig = axisConfig
|
||||
) {
|
||||
val data = dataCollection.data
|
||||
|
||||
androidx.compose.foundation.Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
chartBound = chartWidth.div(
|
||||
points
|
||||
.count()
|
||||
.times(1.2F)
|
||||
)
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val maxValue = dataCollection.maxYValue()
|
||||
val barCount = data.size
|
||||
val minValue = if (dataCollection.minYValue() < 0) {
|
||||
dataCollection.minYValue()
|
||||
} else {
|
||||
0F
|
||||
}
|
||||
val range = maxValue - minValue
|
||||
val availableWidth = chartWidth - (barSpacing.toPx() * (barCount - 1))
|
||||
val maxBarWidth = availableWidth / barCount
|
||||
|
||||
data.fastForEachIndexed { index, barData ->
|
||||
val barWidth = maxBarWidth.coerceAtMost(screenWidth / barCount)
|
||||
val barHeight =
|
||||
(barData.yValue - minValue) / range * (chartHeight - axisConfig.axisStroke)
|
||||
|
||||
val startX = index * (barWidth + barSpacing.toPx())
|
||||
val startY = chartHeight - barHeight
|
||||
|
||||
if (barData is BarData) {
|
||||
if (barData.color != null) {
|
||||
drawRect(
|
||||
color = barData.color,
|
||||
topLeft = Offset(startX, startY),
|
||||
size = Size(barWidth, barHeight)
|
||||
)
|
||||
} else {
|
||||
throw NoSuchFieldException("$barData should have color")
|
||||
}
|
||||
|
||||
if (data.count() < 14) {
|
||||
drawXAxisLabels(
|
||||
data = barData.xValue,
|
||||
center = Offset(startX + barWidth / 2, startY),
|
||||
count = data.count(),
|
||||
padding = padding.toPx(),
|
||||
minLabelCount = axisConfig.minLabelCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.bar.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.himanshoe.charty.common.ChartData
|
||||
|
||||
/**
|
||||
* Data class representing a bar in a bar chart.
|
||||
*
|
||||
* @param yValue The value of the bar on the y-axis.
|
||||
* @param xValue The value of the bar on the x-axis. It can be of any type (e.g., String, Int, Float, etc.).
|
||||
* @param color The color of the bar. If not specified, the default color will be used.
|
||||
*/
|
||||
@Immutable
|
||||
data class BarData(override val yValue: Float, override val xValue: Any, val color: Color? = null) :
|
||||
ChartData {
|
||||
override val chartString: String
|
||||
get() = "Bar Chart"
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.bubble
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.bubble.model.BubbleData
|
||||
import com.himanshoe.charty.common.ChartDataCollection
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.maxYValue
|
||||
import com.himanshoe.charty.common.minYValue
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawXAxisLabels
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
import com.himanshoe.charty.line.config.CurvedLineChartColors
|
||||
import com.himanshoe.charty.line.config.CurvedLineChartDefaults
|
||||
|
||||
/**
|
||||
* A composable function that displays a bubble chart based on the provided data collection.
|
||||
*
|
||||
* @param dataCollection The collection of chart data representing the bubbles in the chart.
|
||||
* @param modifier Modifier for applying styling and positioning to the chart.
|
||||
* @param padding Padding around the chart content to create spacing.
|
||||
* @param axisConfig Configuration for the chart axes, including color and stroke width.
|
||||
* @param chartColors Configuration for the colors used in the bubble chart.
|
||||
*/
|
||||
@Composable
|
||||
fun BubbleChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: Dp = 16.dp,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
chartColors: CurvedLineChartColors = CurvedLineChartDefaults.defaultColor(),
|
||||
) {
|
||||
val points = dataCollection.data
|
||||
|
||||
var chartWidth by remember { mutableStateOf(0f) }
|
||||
var chartHeight by remember { mutableStateOf(0f) }
|
||||
|
||||
val horizontalScale = chartWidth.div(points.count())
|
||||
val verticalScale = chartHeight.div((dataCollection.maxYValue() - dataCollection.minYValue()))
|
||||
|
||||
ChartSurface(
|
||||
padding = padding,
|
||||
chartData = dataCollection,
|
||||
modifier = modifier,
|
||||
axisConfig = axisConfig
|
||||
) {
|
||||
val contentColor = Brush.linearGradient(chartColors.contentColor)
|
||||
val backgroundColor = Brush.linearGradient(chartColors.backgroundColors)
|
||||
val data = dataCollection.data.filterIsInstance<BubbleData>()
|
||||
val minYValue = dataCollection.minYValue()
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val maxVolumeSize = data.maxOf { it.volumeSize }
|
||||
|
||||
dataCollection.data.fastForEachIndexed { index, data ->
|
||||
|
||||
if (data is BubbleData) {
|
||||
val bubbleRadius =
|
||||
data.volumeSize / maxVolumeSize * 50 // Adjust the scaling factor here
|
||||
val x = index.toFloat() * horizontalScale + bubbleRadius
|
||||
val y =
|
||||
(chartHeight - ((data.yValue - minYValue) * verticalScale) - bubbleRadius)
|
||||
.coerceIn(0f, chartHeight - bubbleRadius * 2) + bubbleRadius
|
||||
|
||||
drawCircle(
|
||||
color = Color.Blue,
|
||||
radius = bubbleRadius,
|
||||
center = Offset(x, y)
|
||||
)
|
||||
if (points.count() < 14) {
|
||||
drawXAxisLabels(
|
||||
data = data.xValue,
|
||||
center = Offset(x, y),
|
||||
count = points.count(),
|
||||
padding = padding.toPx(),
|
||||
minLabelCount = axisConfig.minLabelCount,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw ClassCastException("Use ChartDataCollection for BubbleData")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.bubble.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.himanshoe.charty.common.ChartData
|
||||
|
||||
/**
|
||||
* Data class representing a bubble in a bubble chart.
|
||||
*
|
||||
* @param xValue The value of the bubble on the x-axis. It can be of any type (e.g., String, Int, Float, etc.).
|
||||
* @param yValue The value of the bubble on the y-axis.
|
||||
* @param volumeSize The size of the bubble, typically representing a third variable.
|
||||
*/
|
||||
@Immutable
|
||||
data class BubbleData(
|
||||
override val xValue: Any,
|
||||
override val yValue: Float,
|
||||
val volumeSize: Float,
|
||||
) : ChartData {
|
||||
override val chartString: String
|
||||
get() = "Bubble Chart"
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.candle
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.candle.config.CandleStickConfig
|
||||
import com.himanshoe.charty.candle.config.CandleStickDefaults
|
||||
import com.himanshoe.charty.candle.model.CandleData
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.ComposeList
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.toComposeList
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
|
||||
/**
|
||||
* Renders a candlestick chart based on the provided candle data.
|
||||
*
|
||||
* @param candleData The list of candlestick data.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param axisConfig The configuration for the chart axes.
|
||||
* @param padding The padding around the chart.
|
||||
* @param candleConfig The configuration for the candlesticks.
|
||||
*/
|
||||
@Composable
|
||||
fun CandleStickChart(
|
||||
candleData: ComposeList<CandleData>,
|
||||
modifier: Modifier = Modifier,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
padding: Dp = 16.dp,
|
||||
candleConfig: CandleStickConfig = CandleStickDefaults.defaultCandleStickConfig(),
|
||||
) {
|
||||
val listOfAxisValues = candleData.data.flatMap { listOf(it.high, it.low, it.open, it.close) }
|
||||
.distinct()
|
||||
.sorted()
|
||||
|
||||
val maxValue = candleData.data.maxOf { maxOf(it.high, it.open, it.close) }
|
||||
val minValue = candleData.data.minOf { minOf(it.low, it.open, it.close) }
|
||||
|
||||
var chartWidth by remember { mutableStateOf(0f) }
|
||||
var chartHeight by remember { mutableStateOf(0f) }
|
||||
|
||||
ChartSurface(
|
||||
modifier = modifier,
|
||||
padding = padding,
|
||||
axisConfig = axisConfig,
|
||||
chartData = listOfAxisValues.toComposeList(),
|
||||
content = {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val innerWidth = chartWidth - padding.toPx()
|
||||
val innerHeight = chartHeight - padding.toPx() * 1.1F
|
||||
|
||||
val candleCount = candleData.data.size
|
||||
val candleWidth = if (candleCount < 15) {
|
||||
innerWidth / 15
|
||||
} else {
|
||||
innerWidth / candleCount
|
||||
}
|
||||
|
||||
candleData.data.fastForEachIndexed { index, data ->
|
||||
val x = padding.toPx() + index * candleWidth
|
||||
val yHigh = calculateYPosition(
|
||||
data.high,
|
||||
maxValue,
|
||||
minValue,
|
||||
innerHeight
|
||||
) + padding.toPx()
|
||||
|
||||
val yLow = calculateYPosition(
|
||||
data.low,
|
||||
maxValue,
|
||||
minValue,
|
||||
innerHeight
|
||||
) + padding.toPx()
|
||||
|
||||
val yOpen = calculateYPosition(
|
||||
data.open,
|
||||
maxValue,
|
||||
minValue,
|
||||
innerHeight
|
||||
) + padding.toPx()
|
||||
|
||||
val yClose = calculateYPosition(
|
||||
data.close,
|
||||
maxValue,
|
||||
minValue,
|
||||
innerHeight
|
||||
) + padding.toPx()
|
||||
|
||||
val wickStartX = x + candleWidth.div(2)
|
||||
val wickEndX = x + candleWidth.div(2)
|
||||
|
||||
// Draw wick
|
||||
drawLine(
|
||||
color = candleConfig.wickColor,
|
||||
start = Offset(wickStartX, yHigh),
|
||||
end = Offset(wickEndX, yLow),
|
||||
strokeWidth = calculateWickStrokeWidth(
|
||||
candleWidth,
|
||||
candleConfig.wickWidthScale
|
||||
)
|
||||
)
|
||||
|
||||
// Draw body
|
||||
val bodyStartY = minOf(yOpen, yClose)
|
||||
val bodyEndY = maxOf(yOpen, yClose)
|
||||
val bodyWidth = calculateBodyWidth(candleWidth)
|
||||
|
||||
drawRect(
|
||||
color = if (data.close >= data.open) candleConfig.positiveColor else candleConfig.negativeColor,
|
||||
topLeft = Offset(x, bodyStartY),
|
||||
size = Size(bodyWidth, bodyEndY - bodyStartY)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun calculateWickStrokeWidth(candleWidth: Float, wickWidthScale: Float) =
|
||||
candleWidth * wickWidthScale
|
||||
|
||||
private fun calculateBodyWidth(candleWidth: Float) = candleWidth * 0.8f
|
||||
|
||||
private fun calculateYPosition(
|
||||
value: Float,
|
||||
maxValue: Float,
|
||||
minValue: Float,
|
||||
chartHeight: Float
|
||||
): Float {
|
||||
return chartHeight * (1 - (value - minValue) / (maxValue - minValue))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.candle.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Configuration options for a candlestick chart.
|
||||
*
|
||||
* @param positiveColor The color of positive (rising) candles.
|
||||
* @param negativeColor The color of negative (falling) candles.
|
||||
* @param wickColor The color of the wicks (lines) connecting the candle bodies.
|
||||
* @param canCandleScale Whether the candles can be scaled.
|
||||
* @param wickWidthScale The scaling factor for the width of the wicks, relative to the candle width.
|
||||
*/
|
||||
@Immutable
|
||||
data class CandleStickConfig(
|
||||
val positiveColor: Color,
|
||||
val negativeColor: Color,
|
||||
val wickColor: Color,
|
||||
val canCandleScale: Boolean,
|
||||
val wickWidthScale: Float = 0.05f,
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.candle.config
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Default configuration options for a candlestick chart.
|
||||
*/
|
||||
object CandleStickDefaults {
|
||||
/**
|
||||
* Returns the default configuration for a candlestick chart.
|
||||
*
|
||||
* @return The default CandleStickConfig.
|
||||
*/
|
||||
fun defaultCandleStickConfig() = CandleStickConfig(
|
||||
Color.Green,
|
||||
Color.Red,
|
||||
Color.Black,
|
||||
false
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.candle.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Data class representing a single candlestick in a candlestick chart.
|
||||
*
|
||||
* @param high The highest value of the candlestick.
|
||||
* @param low The lowest value of the candlestick.
|
||||
* @param open The opening value of the candlestick.
|
||||
* @param close The closing value of the candlestick.
|
||||
*/
|
||||
@Immutable
|
||||
data class CandleData(
|
||||
val high: Float,
|
||||
val low: Float,
|
||||
val open: Float,
|
||||
val close: Float
|
||||
)
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
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.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import com.himanshoe.charty.circle.config.CircleChartConfig
|
||||
import com.himanshoe.charty.circle.config.CircleChartLabelTextConfig
|
||||
import com.himanshoe.charty.circle.config.CircleConfigDefaults
|
||||
import com.himanshoe.charty.circle.model.CircleData
|
||||
import com.himanshoe.charty.common.ChartDataCollection
|
||||
import com.himanshoe.charty.common.maxYValue
|
||||
import com.himanshoe.charty.common.toChartDataCollection
|
||||
|
||||
/**
|
||||
* Renders a circle chart based on the provided data.
|
||||
*
|
||||
* @param dataCollection The collection of chart data.
|
||||
* @param modifier The modifier to apply to the chart.
|
||||
* @param canAnimate Whether the chart should animate its appearance.
|
||||
* @param textLabelTextConfig The configuration for the label text in the chart.
|
||||
* @param config The configuration for the circle chart.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CircleChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
canAnimate: Boolean = true,
|
||||
textLabelTextConfig: CircleChartLabelTextConfig = CircleConfigDefaults.defaultTextLabelConfig(),
|
||||
config: CircleChartConfig = CircleConfigDefaults.circleChartConfig(),
|
||||
) {
|
||||
val maxYValueState = rememberSaveable { mutableStateOf(dataCollection.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)
|
||||
}
|
||||
|
||||
var chartWidth by remember { mutableStateOf(0F) }
|
||||
var chartHeight by remember { mutableStateOf(0F) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
animatedFactor.animateTo(
|
||||
targetValue = angleFactor,
|
||||
animationSpec = tween(1000)
|
||||
)
|
||||
}
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
}
|
||||
) {
|
||||
val scaleFactor = chartWidth.div(dataCollection.data.count())
|
||||
val sizeArc = size.div(scaleFactor)
|
||||
val factor = if (canAnimate) animatedFactor.value else angleFactor
|
||||
|
||||
dataCollection.data.fastForEachIndexed { index, circleData ->
|
||||
if (circleData is CircleData) {
|
||||
val arcWidth = sizeArc.width.plus(index.times(scaleFactor))
|
||||
val arcHeight = sizeArc.height.plus(index.times(scaleFactor))
|
||||
|
||||
drawArc(
|
||||
color = circleData.color,
|
||||
startAngle = config.startAngle.angle,
|
||||
sweepAngle = factor.times(circleData.yValue),
|
||||
topLeft = Offset(
|
||||
(chartWidth - arcWidth).div(2f),
|
||||
(chartHeight - arcHeight).div(2f)
|
||||
),
|
||||
useCenter = false,
|
||||
style = Stroke(width = scaleFactor.div(2.5F), cap = StrokeCap.Round),
|
||||
size = Size(arcWidth, arcHeight)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.showLabel) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
dataCollection.data.fastMap {
|
||||
if (it is CircleData) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(textLabelTextConfig.indicatorSize)
|
||||
.clip(CircleShape)
|
||||
.align(Alignment.CenterVertically)
|
||||
.background(it.color)
|
||||
)
|
||||
Text(
|
||||
text = it.xValue.toString(),
|
||||
fontSize = textLabelTextConfig.textSize,
|
||||
fontStyle = textLabelTextConfig.fontStyle,
|
||||
fontWeight = textLabelTextConfig.fontWeight,
|
||||
fontFamily = textLabelTextConfig.fontFamily,
|
||||
maxLines = textLabelTextConfig.maxLine,
|
||||
overflow = textLabelTextConfig.overflow,
|
||||
modifier = Modifier.padding(
|
||||
end = 8.dp,
|
||||
start = 4.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun CircleChartScreen() {
|
||||
val circleData = listOf(
|
||||
CircleData(10F, 235F, color = Color(0xFFfafa6e)),
|
||||
CircleData(10F, 135F, color = Color(0xFFc4ec74)),
|
||||
CircleData(10F, 315F, color = Color(0xFF92dc7e)),
|
||||
CircleData(20F, 50F, color = Color(0xFF64c987)),
|
||||
CircleData(30F, 315F, color = Color(0xFF39b48e))
|
||||
)
|
||||
CircleChart(
|
||||
modifier = Modifier
|
||||
.scale(1f)
|
||||
.size(400.dp)
|
||||
.padding(20.dp),
|
||||
dataCollection = circleData.toChartDataCollection(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.circle.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.himanshoe.charty.common.config.StartAngle
|
||||
|
||||
/**
|
||||
* Configuration options for a circle chart.
|
||||
*
|
||||
* @param startAngle The start angle of the chart.
|
||||
* @param maxValue The maximum value of the chart.
|
||||
* @param showLabel Whether to show labels on the chart.
|
||||
*/
|
||||
@Immutable
|
||||
data class CircleChartConfig(
|
||||
val startAngle: StartAngle = StartAngle.Zero,
|
||||
val maxValue: Float?,
|
||||
val showLabel: Boolean
|
||||
)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.circle.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Configuration options for the labels in a circle chart.
|
||||
*
|
||||
* @param textSize The size of the text.
|
||||
* @param fontStyle The style of the font.
|
||||
* @param fontWeight The weight of the font.
|
||||
* @param fontFamily The family of the font.
|
||||
* @param indicatorSize The size of the label indicator.
|
||||
* @param maxLine The maximum number of lines for the label text.
|
||||
* @param overflow The text overflow behavior.
|
||||
*/
|
||||
@Immutable
|
||||
data class CircleChartLabelTextConfig(
|
||||
val textSize: TextUnit,
|
||||
val fontStyle: FontStyle? = null,
|
||||
val fontWeight: FontWeight? = null,
|
||||
val fontFamily: FontFamily? = null,
|
||||
val indicatorSize: Dp = 10.dp,
|
||||
val maxLine: Int = 1,
|
||||
val overflow: TextOverflow = TextOverflow.Ellipsis
|
||||
)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.circle.config
|
||||
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.himanshoe.charty.common.config.StartAngle
|
||||
|
||||
/**
|
||||
* Provides default configuration values for a circle chart and its labels.
|
||||
*/
|
||||
object CircleConfigDefaults {
|
||||
/**
|
||||
* Returns the default configuration for a circle chart.
|
||||
*/
|
||||
fun circleChartConfig() = CircleChartConfig(
|
||||
startAngle = StartAngle.Zero,
|
||||
maxValue = null,
|
||||
showLabel = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the default configuration for the labels in a circle chart.
|
||||
*/
|
||||
fun defaultTextLabelConfig() = CircleChartLabelTextConfig(
|
||||
textSize = TextUnit.Unspecified,
|
||||
fontStyle = null,
|
||||
fontWeight = null,
|
||||
fontFamily = null,
|
||||
maxLine = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.circle.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.himanshoe.charty.common.ChartData
|
||||
|
||||
/**
|
||||
* Represents the data for a circle in a chart.
|
||||
*
|
||||
* @param yValue The y-value of the circle.
|
||||
* @param xValue The x-value of the circle.
|
||||
* @param color The color of the circle.
|
||||
*/
|
||||
@Immutable
|
||||
data class CircleData(override val yValue: Float, override val xValue: Any, val color: Color) :
|
||||
ChartData {
|
||||
override val chartString: String
|
||||
get() = "Circle Chart"
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.ui.drawXAxisLabels
|
||||
import com.himanshoe.charty.common.ui.drawYAxisLabels
|
||||
|
||||
@Composable
|
||||
fun ChartSurface(
|
||||
padding: Dp,
|
||||
chartData: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
content: @Composable () -> Unit = {}
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = padding.times(2),
|
||||
bottom = padding.times(2),
|
||||
top = padding.times(2),
|
||||
end = padding
|
||||
)
|
||||
.drawBehind {
|
||||
if (chartData.data.count() >= 14 && axisConfig.showGridLabel) {
|
||||
drawXAxisLabels(
|
||||
data = chartData.data.fastMap { it.xValue },
|
||||
count = chartData.data.count(),
|
||||
padding = padding.toPx(),
|
||||
minLabelCount = axisConfig.minLabelCount
|
||||
)
|
||||
}
|
||||
if (axisConfig.showGridLabel) {
|
||||
val newItems = if (chartData.minYValue() > 0) {
|
||||
listOf(0F) + chartData.data.map { it.yValue }
|
||||
} else {
|
||||
chartData.data.map { it.yValue }
|
||||
}
|
||||
|
||||
drawYAxisLabels(
|
||||
newItems,
|
||||
spacing = padding.toPx(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> ChartSurface(
|
||||
padding: Dp,
|
||||
chartData: ComposeList<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
content: @Composable () -> Unit = {}
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = padding.times(2),
|
||||
bottom = padding.times(2),
|
||||
top = padding.times(2),
|
||||
end = padding
|
||||
)
|
||||
.drawBehind {
|
||||
if (axisConfig.showGridLabel) {
|
||||
drawYAxisLabels(
|
||||
chartData.data as List<Float>,
|
||||
spacing = padding.toPx(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/*
|
||||
* Represents a single data point in a chart.
|
||||
* - xValue: The value on the x-axis.
|
||||
* - yValue: The value on the y-axis.
|
||||
* - chartString: A string representation of the data point.
|
||||
*/
|
||||
interface ChartData {
|
||||
val xValue: Any
|
||||
val yValue: Float
|
||||
val chartString: String
|
||||
}
|
||||
|
||||
/*
|
||||
* Represents a collection of ChartData objects.
|
||||
* - data: The list of ChartData objects.
|
||||
*/
|
||||
@Immutable
|
||||
data class ChartDataCollection(val data: List<ChartData>)
|
||||
|
||||
/*
|
||||
* Extension function to convert a List of ChartData objects to a ChartDataCollection.
|
||||
*/
|
||||
fun List<ChartData>.toChartDataCollection() = ChartDataCollection(this)
|
||||
|
||||
/*
|
||||
* Returns the maximum y-value among all the data points in the ChartDataCollection.
|
||||
*/
|
||||
fun ChartDataCollection.maxYValue() = data.maxOf { it.yValue }
|
||||
|
||||
/*
|
||||
* Returns the minimum y-value among all the data points in the ChartDataCollection.
|
||||
*/
|
||||
fun ChartDataCollection.minYValue() = data.minOf { it.yValue }
|
||||
|
||||
/*
|
||||
* Represents a collection of items for Compose.
|
||||
* - data: The list of items.
|
||||
*/
|
||||
@Immutable
|
||||
data class ComposeList<T>(val data: List<T>)
|
||||
|
||||
/*
|
||||
* Extension function to convert a List to a ComposeList.
|
||||
*/
|
||||
fun <T> List<T>.toComposeList() = ComposeList(this)
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Configuration for the axes and grid lines in a chart.
|
||||
*
|
||||
* @param showAxes Whether to show the axes.
|
||||
* @param showGridLines Whether to show the grid lines.
|
||||
* @param showGridLabel Whether to show the grid labels.
|
||||
* @param axisStroke The stroke width of the axes.
|
||||
* @param minLabelCount The minimum number of labels to display.
|
||||
* @param axisColor The color of the axes.
|
||||
* @param gridColor The color of the grid lines. Defaults to a slightly transparent version of the axis color.
|
||||
*/
|
||||
@Immutable
|
||||
data class AxisConfig(
|
||||
val showAxes: Boolean,
|
||||
val showGridLines: Boolean,
|
||||
val showGridLabel: Boolean,
|
||||
val axisStroke: Float,
|
||||
val minLabelCount: Int,
|
||||
val axisColor: Color,
|
||||
val gridColor: Color = axisColor.copy(alpha = 0.5F),
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Colors used in a chart.
|
||||
*
|
||||
* @param contentColor The color(s) for the content of the chart.
|
||||
* @param backgroundColors The background color(s) of the chart.
|
||||
*/
|
||||
@Immutable
|
||||
data class ChartColors(
|
||||
val contentColor: List<Color> = emptyList(),
|
||||
val backgroundColors: List<Color> = emptyList(),
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.config
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
|
||||
object ChartDefaults {
|
||||
|
||||
/**
|
||||
* Provides default values for the axis configuration.
|
||||
*/
|
||||
fun axisConfigDefaults() = AxisConfig(
|
||||
showGridLabel = true,
|
||||
showAxes = true,
|
||||
showGridLines = true,
|
||||
axisStroke = 2F,
|
||||
axisColor = Color.Black,
|
||||
minLabelCount = 2
|
||||
)
|
||||
|
||||
/**
|
||||
* Provides default colors for the chart.
|
||||
*/
|
||||
fun colorDefaults() = ChartColors(
|
||||
contentColor = listOf(
|
||||
Color(0xffed625d),
|
||||
Color(0xfff79f88)
|
||||
),
|
||||
backgroundColors = listOf(
|
||||
Color.Transparent, Color.Transparent
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Provides default values for text label configuration in the chart.
|
||||
*/
|
||||
fun defaultTextLabelConfig() = ChartyLabelTextConfig(
|
||||
textSize = TextUnit.Unspecified,
|
||||
fontStyle = null,
|
||||
fontWeight = null,
|
||||
fontFamily = null,
|
||||
textColor = Color.Black,
|
||||
maxLine = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents the configuration for the text labels in a chart.
|
||||
*
|
||||
* @property textSize The size of the text labels.
|
||||
* @property textColor The color of the text labels.
|
||||
* @property fontStyle The style of the font (e.g., italic). Defaults to null.
|
||||
* @property fontWeight The weight of the font (e.g., bold). Defaults to null.
|
||||
* @property fontFamily The family of the font (e.g., serif). Defaults to null.
|
||||
* @property indicatorSize The size of the indicator associated with the labels. Defaults to 10.dp.
|
||||
* @property maxLine The maximum number of lines the label can span. Defaults to 1.
|
||||
* @property overflow The behavior when the text exceeds the available space (e.g., ellipsis). Defaults to TextOverflow.Ellipsis.
|
||||
*/
|
||||
@Immutable
|
||||
data class ChartyLabelTextConfig(
|
||||
val textSize: TextUnit,
|
||||
val textColor: Color,
|
||||
val fontStyle: FontStyle? = null,
|
||||
val fontWeight: FontWeight? = null,
|
||||
val fontFamily: FontFamily? = null,
|
||||
val indicatorSize: Dp = 10.dp,
|
||||
val maxLine: Int = 1,
|
||||
val overflow: TextOverflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Represents the start angle for a circular chart.
|
||||
*/
|
||||
@Immutable
|
||||
sealed class StartAngle(open val angle: Float) {
|
||||
/**
|
||||
* Start angle of 0 degrees.
|
||||
*/
|
||||
object Zero : StartAngle(0F)
|
||||
|
||||
/**
|
||||
* Start angle of 180 degrees (straight angle).
|
||||
*/
|
||||
object StraightAngle : StartAngle(180F)
|
||||
|
||||
/**
|
||||
* Start angle of 270 degrees (reflex angle).
|
||||
*/
|
||||
object ReflexAngle : StartAngle(270F)
|
||||
|
||||
/**
|
||||
* Start angle of 90 degrees (right angle).
|
||||
*/
|
||||
object RightAngle : StartAngle(90F)
|
||||
|
||||
/**
|
||||
* Custom start angle specified by the given angle in degrees.
|
||||
*
|
||||
* @param angle The custom start angle in degrees.
|
||||
*/
|
||||
data class CustomAngle(override val angle: Float) : StartAngle(angle)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.math
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
|
||||
/**
|
||||
* Calculates the offset for a data point on a chart.
|
||||
*
|
||||
* @param index The index of the data point.
|
||||
* @param bound The width of each data point.
|
||||
* @param size The size of the chart.
|
||||
* @param data The value of the data point.
|
||||
* @param scaleFactor The scale factor for the data points.
|
||||
* @return The offset of the data point.
|
||||
*/
|
||||
internal fun chartDataToOffset(
|
||||
index: Int,
|
||||
bound: Float,
|
||||
size: Size,
|
||||
data: Float,
|
||||
scaleFactor: Float
|
||||
): Offset {
|
||||
val startX = index * bound * 1.2F
|
||||
val endX = (index + 1) * bound * 1.2F
|
||||
val y = size.height - data * scaleFactor
|
||||
return Offset((startX + endX) / 2F, y)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.ui
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
/**
|
||||
* Draws the X axis.
|
||||
*
|
||||
* @param color The color of the axis.
|
||||
* @param stroke The stroke width of the axis.
|
||||
*/
|
||||
fun DrawScope.drawXAxis(color: Color, stroke: Float) {
|
||||
drawLine(
|
||||
start = Offset(0f, 0F),
|
||||
end = Offset(0f, size.height),
|
||||
color = color,
|
||||
strokeWidth = stroke,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the Y axis.
|
||||
*
|
||||
* @param color The color of the axis.
|
||||
* @param stroke The stroke width of the axis.
|
||||
*/
|
||||
fun DrawScope.drawYAxis(color: Color, stroke: Float) {
|
||||
drawLine(
|
||||
start = Offset(0f, size.height),
|
||||
end = Offset(size.width, size.height),
|
||||
color = color,
|
||||
strokeWidth = stroke
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.ui
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
|
||||
/**
|
||||
* Draws X-axis labels for a single data point.
|
||||
*
|
||||
* @param data The data value for the label.
|
||||
* @param center The center position of the label.
|
||||
* @param count The total count of data points.
|
||||
* @param padding The padding value.
|
||||
* @param minLabelCount The minimum label count.
|
||||
* @param textColor The color of the text label.
|
||||
*/
|
||||
internal fun DrawScope.drawXAxisLabels(
|
||||
data: Any,
|
||||
center: Offset,
|
||||
count: Int,
|
||||
padding: Float,
|
||||
minLabelCount: Int,
|
||||
textColor: Color = Color.Black,
|
||||
) {
|
||||
val divisibleFactor = if (count > 10) count else 1
|
||||
val textSizeFactor = if (count > 10) 3 else 30
|
||||
|
||||
val textBounds = Rect()
|
||||
val textPaint = Paint().apply {
|
||||
color = textColor.toArgb()
|
||||
textSize = size.width / textSizeFactor / divisibleFactor
|
||||
textAlign = Paint.Align.CENTER
|
||||
getTextBounds(data.toString(), 0, data.toString().length, textBounds)
|
||||
}
|
||||
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
data.toString().take(minLabelCount),
|
||||
center.x,
|
||||
size.height + padding,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws X-axis labels for a list of data points.
|
||||
*
|
||||
* @param data The list of data points.
|
||||
* @param count The total count of data points.
|
||||
* @param padding The padding value.
|
||||
* @param minLabelCount The minimum label count.
|
||||
* @param textColor The color of the text labels.
|
||||
*/
|
||||
internal fun DrawScope.drawXAxisLabels(
|
||||
data: List<Any>,
|
||||
count: Int,
|
||||
padding: Float,
|
||||
minLabelCount: Int,
|
||||
textColor: Color = Color.Black,
|
||||
) {
|
||||
val textBounds = Rect()
|
||||
val textPaint = Paint().apply {
|
||||
color = textColor.toArgb()
|
||||
textSize = size.width / 30
|
||||
textAlign = Paint.Align.CENTER
|
||||
getTextBounds(data.toString(), 0, data.toString().length, textBounds)
|
||||
}
|
||||
|
||||
val xStart = padding / 2
|
||||
val xEnd = size.width - padding
|
||||
val nearestCenterIndex = if (count % 2 == 0) count / 2 else (count - 1) / 2
|
||||
val xMiddle = nearestCenterIndex * (size.width - padding) / (count - 1)
|
||||
|
||||
val y = size.height + padding
|
||||
val textCount = data.toString().length.coerceAtLeast(minLabelCount)
|
||||
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
data.first().toString().take(textCount),
|
||||
xStart,
|
||||
y,
|
||||
textPaint
|
||||
)
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
data.last().toString().take(textCount),
|
||||
xEnd,
|
||||
y,
|
||||
textPaint
|
||||
)
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
data[nearestCenterIndex].toString().take(textCount),
|
||||
xMiddle,
|
||||
y,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws Y-axis labels for a list of values.
|
||||
*
|
||||
* @param values The list of values.
|
||||
* @param spacing The spacing between labels.
|
||||
* @param textColor The color of the text labels.
|
||||
*/
|
||||
fun DrawScope.drawYAxisLabels(
|
||||
values: List<Float>,
|
||||
spacing: Float,
|
||||
textColor: Color = Color.Black,
|
||||
) {
|
||||
val maxLabelCount = 4
|
||||
val maxLabelValue = values.maxOrNull() ?: return
|
||||
val minLabelValue = values.minOrNull() ?: return
|
||||
val labelRange = maxLabelValue - minLabelValue
|
||||
|
||||
val textPaint = Paint().apply {
|
||||
color = textColor.toArgb()
|
||||
textSize = size.width / 30
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
val labelSpacing = size.height / (maxLabelCount - 1)
|
||||
|
||||
repeat(maxLabelCount) { i ->
|
||||
val y = size.height - (i * labelSpacing)
|
||||
val x = 0F.minus(spacing)
|
||||
val labelValue = minLabelValue + ((i * labelRange) / (maxLabelCount - 1))
|
||||
|
||||
val text = if (labelValue.toString().length > 4) {
|
||||
labelValue.toString().take(4)
|
||||
} else {
|
||||
labelValue.toString()
|
||||
}
|
||||
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.common.ui
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
fun DrawScope.drawGridLines(width: Float, height: Float, spacing: Float) {
|
||||
// Calculate the dimensions of the grid area by subtracting the spacing
|
||||
val newHeight = height.minus(spacing)
|
||||
val newWidth = width.minus(spacing)
|
||||
|
||||
// Calculate the spacing between horizontal and vertical grid lines
|
||||
val horizontalGridSpacing = newHeight / 5
|
||||
val verticalGridSpacing = newWidth / 4
|
||||
|
||||
// Draw horizontal grid lines
|
||||
repeat(4) { i ->
|
||||
val y = (i + 1) * horizontalGridSpacing
|
||||
|
||||
drawLine(
|
||||
start = Offset(0F, y + spacing),
|
||||
end = Offset(newWidth + spacing, y + spacing),
|
||||
color = Color.LightGray,
|
||||
strokeWidth = 1f
|
||||
)
|
||||
}
|
||||
|
||||
// Draw vertical grid lines
|
||||
repeat(3) { i ->
|
||||
val x = (i + 1) * verticalGridSpacing
|
||||
|
||||
drawLine(
|
||||
start = Offset(x + spacing, 0F),
|
||||
end = Offset(x + spacing, newHeight + spacing),
|
||||
color = Color.LightGray,
|
||||
strokeWidth = 1f
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.gauge
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.himanshoe.charty.gauge.config.GaugeChartConfig
|
||||
import com.himanshoe.charty.gauge.config.GaugeChartDefaults
|
||||
import com.himanshoe.charty.gauge.config.NeedleConfig
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
private const val START_ANGLE = 135F
|
||||
private const val INITIAL_START_ANGLE = 405F
|
||||
|
||||
/**
|
||||
* A composable function that displays a gauge chart.
|
||||
*
|
||||
* @param percentValue The value in percentage to be displayed on the gauge chart.
|
||||
* @param modifier The modifier for styling or positioning the gauge chart.
|
||||
* @param gaugeChartConfig The configuration for the gauge chart appearance.
|
||||
* @param needleConfig The configuration for the needle in the gauge chart.
|
||||
* @param animated Determines whether the gauge chart should be animated.
|
||||
* @param animationSpec The animation specification for the gauge chart animation.
|
||||
*/
|
||||
@Composable
|
||||
fun GaugeChart(
|
||||
percentValue: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
gaugeChartConfig: GaugeChartConfig = GaugeChartDefaults.gaugeConfigDefaults(),
|
||||
needleConfig: NeedleConfig = GaugeChartDefaults.needleConfigDefaults(),
|
||||
animated: Boolean = true,
|
||||
animationSpec: AnimationSpec<Float> = tween(),
|
||||
) {
|
||||
require(percentValue in 1..100) { "percentValue must be within the range of 1 to 100" }
|
||||
|
||||
val animatedPercent = rememberAnimatedPercent(animated, percentValue, animationSpec)
|
||||
Box(modifier = modifier.aspectRatio(1f)) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val centerX = size.width / 2
|
||||
val centerY = size.height / 2
|
||||
val radius = (size.width - gaugeChartConfig.strokeWidth) / 2
|
||||
val sweepAngle = INITIAL_START_ANGLE - START_ANGLE
|
||||
|
||||
// Draw background arc
|
||||
drawArc(
|
||||
color = gaugeChartConfig.placeHolderColor,
|
||||
startAngle = START_ANGLE,
|
||||
sweepAngle = sweepAngle,
|
||||
useCenter = false,
|
||||
topLeft = Offset(centerX - radius, centerY - radius),
|
||||
size = Size(radius * 2, radius * 2),
|
||||
style = Stroke(width = gaugeChartConfig.strokeWidth, cap = StrokeCap.Round)
|
||||
)
|
||||
if (gaugeChartConfig.showIndicator) {
|
||||
// Draw minute hour dividers
|
||||
val dividerCount = 100
|
||||
val dividerSweepAngle = sweepAngle / dividerCount
|
||||
val longerDividerLength = radius * 0.1f
|
||||
val shorterDividerLength = radius * 0.05f
|
||||
|
||||
for (i in 0..dividerCount) { // Updated loop range to include 0
|
||||
val dividerStartAngle = START_ANGLE + (i * dividerSweepAngle)
|
||||
val isLongerDivider = i % 10 == 0
|
||||
|
||||
val dividerLength =
|
||||
if (isLongerDivider) longerDividerLength else shorterDividerLength
|
||||
val startPoint = polarToCartesian(
|
||||
centerX,
|
||||
centerY,
|
||||
radius - gaugeChartConfig.strokeWidth,
|
||||
dividerStartAngle
|
||||
)
|
||||
val endPoint = polarToCartesian(
|
||||
centerX,
|
||||
centerY,
|
||||
radius - gaugeChartConfig.strokeWidth - dividerLength,
|
||||
dividerStartAngle
|
||||
)
|
||||
val color =
|
||||
if (isLongerDivider) gaugeChartConfig.indicatorColor else gaugeChartConfig.indicatorColor.copy(
|
||||
alpha = 0.8F
|
||||
)
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(startPoint.x, startPoint.y),
|
||||
end = Offset(endPoint.x, endPoint.y),
|
||||
strokeWidth = gaugeChartConfig.indicatorWidth,
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
}
|
||||
}
|
||||
val currentSweepAngle = animatedPercent * sweepAngle
|
||||
drawArc(
|
||||
color = gaugeChartConfig.primaryColor,
|
||||
startAngle = START_ANGLE,
|
||||
sweepAngle = currentSweepAngle,
|
||||
useCenter = false,
|
||||
topLeft = Offset(centerX - radius, centerY - radius),
|
||||
size = Size(radius * 2, radius * 2),
|
||||
style = Stroke(width = gaugeChartConfig.strokeWidth, cap = StrokeCap.Round)
|
||||
)
|
||||
|
||||
if (gaugeChartConfig.showNeedle) {
|
||||
// Calculate needle path
|
||||
val needleAngle = START_ANGLE + currentSweepAngle
|
||||
val needlePath = Path().apply {
|
||||
val startPoint =
|
||||
polarToCartesian(centerX, centerY, radius.div(1.2F), needleAngle)
|
||||
val endPoint = polarToCartesian(centerX, centerY, radius.div(1.2F), needleAngle)
|
||||
|
||||
moveTo(centerX, centerY)
|
||||
lineTo(startPoint.x, startPoint.y)
|
||||
lineTo(endPoint.x, endPoint.y)
|
||||
}
|
||||
|
||||
// Draw needle
|
||||
drawPath(
|
||||
path = needlePath,
|
||||
color = needleConfig.color,
|
||||
style = Stroke(width = needleConfig.strokeWidth, cap = StrokeCap.Round)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun polarToCartesian(
|
||||
centerX: Float,
|
||||
centerY: Float,
|
||||
radius: Float,
|
||||
angle: Float
|
||||
): Offset {
|
||||
val x = centerX + radius * cos(Math.toRadians(angle.toDouble())).toFloat()
|
||||
val y = centerY + radius * sin(Math.toRadians(angle.toDouble())).toFloat()
|
||||
return Offset(x, y)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberAnimatedPercent(
|
||||
animated: Boolean,
|
||||
percentValue: Int,
|
||||
animationSpec: AnimationSpec<Float>
|
||||
): Float {
|
||||
val animatedPercent = remember {
|
||||
Animatable(initialValue = 0f)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (animated) {
|
||||
animatedPercent.animateTo(
|
||||
targetValue = percentValue / 100f,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
} else {
|
||||
animatedPercent.snapTo(percentValue / 100f)
|
||||
}
|
||||
}
|
||||
|
||||
return animatedPercent.value
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun GaugeChartPreview() {
|
||||
val percentValue = 100
|
||||
GaugeChart(percentValue = percentValue)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.gauge.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Represents the configuration for a gauge chart.
|
||||
*
|
||||
* @param placeHolderColor The color of the placeholder arc in the gauge chart.
|
||||
* @param primaryColor The color of the primary arc in the gauge chart.
|
||||
* @param strokeWidth The width of the arcs in the gauge chart.
|
||||
* @param showNeedle Indicates whether to show the needle in the gauge chart.
|
||||
* @param showIndicator Indicates whether to show the indicator in the gauge chart.
|
||||
* @param indicatorColor The color of the indicator in the gauge chart.
|
||||
* @param indicatorWidth The width of the indicator in the gauge chart.
|
||||
*/
|
||||
@Immutable
|
||||
data class GaugeChartConfig(
|
||||
val placeHolderColor: Color,
|
||||
val primaryColor: Color,
|
||||
val strokeWidth: Float,
|
||||
val showNeedle: Boolean,
|
||||
val showIndicator: Boolean,
|
||||
val indicatorColor: Color,
|
||||
val indicatorWidth: Float,
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.gauge.config
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Provides default configurations for the gauge chart.
|
||||
*/
|
||||
object GaugeChartDefaults {
|
||||
|
||||
/**
|
||||
* Returns the default configuration for the gauge chart.
|
||||
*
|
||||
* @return The default [GaugeChartConfig] for the gauge chart.
|
||||
*/
|
||||
fun gaugeConfigDefaults() = GaugeChartConfig(
|
||||
primaryColor = Color(0xFF20A100),
|
||||
placeHolderColor = Color(0xFFABEDCD),
|
||||
strokeWidth = 48F,
|
||||
showNeedle = true,
|
||||
showIndicator = true,
|
||||
indicatorColor = Color(0xffed625d),
|
||||
indicatorWidth = 8F
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the default configuration for the needle in the gauge chart.
|
||||
*
|
||||
* @return The default [NeedleConfig] for the needle in the gauge chart.
|
||||
*/
|
||||
fun needleConfigDefaults() = NeedleConfig(
|
||||
color = Color(0xFF93A047),
|
||||
strokeWidth = 40f,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.gauge.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Configuration for the needle in the gauge chart.
|
||||
*
|
||||
* @property color The color of the needle.
|
||||
* @property strokeWidth The width of the needle stroke.
|
||||
*/
|
||||
@Immutable
|
||||
data class NeedleConfig(
|
||||
val color: Color,
|
||||
val strokeWidth: Float,
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.group
|
||||
|
||||
import android.graphics.Paint
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.ComposeList
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.config.ChartyLabelTextConfig
|
||||
import com.himanshoe.charty.common.toComposeList
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
import com.himanshoe.charty.group.model.GroupBarData
|
||||
|
||||
/**
|
||||
* Composable function that renders a grouped bar chart.
|
||||
*
|
||||
* @param groupBarDataCollection The collection of group bar chart data.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param padding The padding around the chart. Defaults to 16.dp.
|
||||
* @param barWidthRatio The ratio of the bar width to the available space. Defaults to 0.8f.
|
||||
* @param axisConfig The configuration for the chart's axis. Defaults to [ChartDefaults.axisConfigDefaults].
|
||||
* @param textLabelTextConfig The configuration for the text labels in the chart. Defaults to [ChartDefaults.defaultTextLabelConfig].
|
||||
*/
|
||||
@Composable
|
||||
fun GroupedBarChart(
|
||||
groupBarDataCollection: ComposeList<GroupBarData>,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: Dp = 16.dp,
|
||||
barWidthRatio: Float = 0.8f,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
textLabelTextConfig: ChartyLabelTextConfig = ChartDefaults.defaultTextLabelConfig(),
|
||||
) {
|
||||
require(barWidthRatio in 0.4f..0.9f) { "barWidthRatio must be within the range of 0.4F to 0.9F, but use 0.8F for best looking View" }
|
||||
|
||||
val allDataPoints = groupBarDataCollection.data.flatMap { it.dataPoints }
|
||||
val maxValue = allDataPoints.maxOrNull() ?: 0f
|
||||
val minValue = allDataPoints.minOrNull() ?: 0f
|
||||
val newItems = if (allDataPoints.min() > 0F) {
|
||||
listOf(0F) + allDataPoints
|
||||
} else {
|
||||
allDataPoints
|
||||
}
|
||||
var chartWidth by remember { mutableStateOf(0F) }
|
||||
var chartHeight by remember { mutableStateOf(0F) }
|
||||
|
||||
ChartSurface(
|
||||
modifier = modifier,
|
||||
padding = padding,
|
||||
axisConfig = axisConfig,
|
||||
chartData = newItems.toComposeList(),
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val groupCount = groupBarDataCollection.data.size
|
||||
val barCount = groupBarDataCollection.data.firstOrNull()?.dataPoints?.size ?: 0
|
||||
|
||||
val groupWidth = chartWidth / groupCount
|
||||
val totalBarWidth = groupWidth * barWidthRatio
|
||||
val barWidth = totalBarWidth / barCount.toFloat()
|
||||
|
||||
groupBarDataCollection.data.fastForEachIndexed { groupIndex, groupedBarData ->
|
||||
if (axisConfig.showGridLabel) {
|
||||
drawLabel(
|
||||
groupedBarData = groupedBarData,
|
||||
groupIndex = groupIndex,
|
||||
groupWidth = groupWidth,
|
||||
chartHeight = chartHeight,
|
||||
textLabelTextConfig = textLabelTextConfig
|
||||
)
|
||||
}
|
||||
|
||||
groupedBarData.dataPoints.fastForEachIndexed { barIndex, barValue ->
|
||||
val x = calculateBarX(groupIndex, groupWidth, barWidthRatio, barIndex, barWidth)
|
||||
val barHeight = calculateBarHeight(barValue, minValue, maxValue, chartHeight)
|
||||
|
||||
drawGroupedBar(
|
||||
x = x,
|
||||
barHeight = barHeight,
|
||||
width = barWidth,
|
||||
height = chartHeight,
|
||||
colors = groupedBarData.colors,
|
||||
barIndex = barIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawLabel(
|
||||
groupedBarData: GroupBarData,
|
||||
groupIndex: Int,
|
||||
groupWidth: Float,
|
||||
chartHeight: Float,
|
||||
textLabelTextConfig: ChartyLabelTextConfig
|
||||
) {
|
||||
val label = groupedBarData.label
|
||||
val labelX = (groupIndex * groupWidth) + (groupWidth / 2f)
|
||||
val labelY = chartHeight + size.width / 20
|
||||
|
||||
drawIntoCanvas {
|
||||
it.nativeCanvas.drawText(
|
||||
label,
|
||||
labelX,
|
||||
labelY,
|
||||
Paint().apply {
|
||||
color = textLabelTextConfig.textColor.toArgb()
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = size.width / 35
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateBarX(
|
||||
groupIndex: Int,
|
||||
groupWidth: Float,
|
||||
barWidthRatio: Float,
|
||||
barIndex: Int,
|
||||
barWidth: Float
|
||||
) = (groupIndex * groupWidth) +
|
||||
((1 - barWidthRatio) / 2) *
|
||||
groupWidth +
|
||||
(barIndex * barWidth) + (barWidth / 2f)
|
||||
|
||||
private fun calculateBarHeight(
|
||||
barValue: Float,
|
||||
minValue: Float,
|
||||
maxValue: Float,
|
||||
chartHeight: Float
|
||||
): Float {
|
||||
val newMinValue = if (minValue < 0) minValue else 0F
|
||||
val range = maxValue - newMinValue
|
||||
val normalizedValue = barValue - newMinValue
|
||||
val heightRatio = if (range != 0f) normalizedValue / range else 0f
|
||||
return heightRatio * chartHeight
|
||||
}
|
||||
|
||||
private fun DrawScope.drawGroupedBar(
|
||||
x: Float,
|
||||
barHeight: Float,
|
||||
width: Float,
|
||||
height: Float,
|
||||
colors: List<Color>,
|
||||
barIndex: Int
|
||||
) {
|
||||
val barRect = Rect(
|
||||
left = x - (width / 2f),
|
||||
top = height - barHeight,
|
||||
right = x + (width / 2f),
|
||||
bottom = height
|
||||
)
|
||||
|
||||
drawRoundRect(
|
||||
color = colors.getOrElse(barIndex % colors.size) { Color.Gray },
|
||||
topLeft = Offset(barRect.left, barRect.top),
|
||||
size = Size(barRect.width, barRect.height),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.group.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Immutable data class representing a group bar chart data.
|
||||
*
|
||||
* @param label The label for the group bar.
|
||||
* @param dataPoints The list of data points for each bar in the group.
|
||||
* @param colors The list of colors for each bar in the group. Defaults to [Color.Transparent]
|
||||
* if not provided explicitly.
|
||||
*/
|
||||
@Immutable
|
||||
data class GroupBarData(
|
||||
val label: String,
|
||||
val dataPoints: List<Float>,
|
||||
val colors: List<Color> = List(dataPoints.count()) { Color.Transparent }
|
||||
)
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.line
|
||||
|
||||
import android.graphics.PointF
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.common.ChartDataCollection
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.math.chartDataToOffset
|
||||
import com.himanshoe.charty.common.maxYValue
|
||||
import com.himanshoe.charty.common.minYValue
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawXAxisLabels
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
import com.himanshoe.charty.line.config.CurvedLineChartColors
|
||||
import com.himanshoe.charty.line.config.CurvedLineChartDefaults
|
||||
import com.himanshoe.charty.line.config.LineChartDefaults
|
||||
import com.himanshoe.charty.line.config.LineConfig
|
||||
|
||||
/**
|
||||
* A composable function that displays a curved line chart.
|
||||
*
|
||||
* @param dataCollection The collection of chart data points.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param padding The padding around the chart.
|
||||
* @param axisConfig The configuration for the chart's axes.
|
||||
* @param radiusScale The scale factor for the radius of the data points.
|
||||
* @param lineConfig The configuration for the line in the chart.
|
||||
* @param chartColors The colors used in the chart.
|
||||
*/
|
||||
@Composable
|
||||
fun CurveLineChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: Dp = 16.dp,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
radiusScale: Float = 0.02f,
|
||||
lineConfig: LineConfig = LineChartDefaults.defaultConfig(),
|
||||
chartColors: CurvedLineChartColors = CurvedLineChartDefaults.defaultColor(),
|
||||
) {
|
||||
val points = dataCollection.data
|
||||
|
||||
var chartWidth by remember { mutableStateOf(0F) }
|
||||
var chartHeight by remember { mutableStateOf(0F) }
|
||||
var pointBound by remember { mutableStateOf(0F) }
|
||||
|
||||
val horizontalScale = chartWidth.div(points.count())
|
||||
val verticalScale = chartHeight.div((dataCollection.maxYValue() - dataCollection.minYValue()))
|
||||
|
||||
ChartSurface(
|
||||
padding = padding,
|
||||
chartData = dataCollection,
|
||||
modifier = modifier,
|
||||
axisConfig = axisConfig
|
||||
) {
|
||||
val contentColor = Brush.linearGradient(chartColors.contentColor)
|
||||
val backgroundColor = Brush.linearGradient(chartColors.backgroundColors)
|
||||
val dotColor = Brush.linearGradient(chartColors.dotColor)
|
||||
|
||||
val minYValue = dataCollection.minYValue()
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
pointBound = size.width.div(
|
||||
points
|
||||
.count()
|
||||
.times(1.2F)
|
||||
)
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val graphPathPoints = mutableListOf<PointF>()
|
||||
val radius = size.width * radiusScale
|
||||
|
||||
Path().apply {
|
||||
moveTo(0f, size.height) // Start from the bottom-left corner of the canvas
|
||||
|
||||
val firstData = dataCollection.data.first()
|
||||
val initialX = 0f
|
||||
val initialY = size.height - ((firstData.yValue - minYValue) * verticalScale)
|
||||
lineTo(initialX, initialY) // Move to the initial point
|
||||
|
||||
dataCollection.data.fastForEachIndexed { index, data ->
|
||||
val centerOffset = chartDataToOffset(
|
||||
index,
|
||||
pointBound,
|
||||
size,
|
||||
data.yValue,
|
||||
horizontalScale,
|
||||
)
|
||||
|
||||
val x = centerOffset.x
|
||||
val y = size.height - ((data.yValue - minYValue) * verticalScale)
|
||||
val innerX =
|
||||
x.coerceIn(centerOffset.x - radius / 2, centerOffset.x + radius / 2)
|
||||
val innerY = y.coerceIn(radius, size.height - radius)
|
||||
|
||||
graphPathPoints.add(PointF(innerX, innerY))
|
||||
|
||||
if (index > 0) {
|
||||
val prevIndex = index.minus(1)
|
||||
val previousCenterOffset = chartDataToOffset(
|
||||
index.minus(1),
|
||||
pointBound,
|
||||
size,
|
||||
data.yValue,
|
||||
horizontalScale,
|
||||
)
|
||||
val prevX = previousCenterOffset.x
|
||||
val prevY =
|
||||
size.height - (
|
||||
dataCollection.data[prevIndex].yValue -
|
||||
dataCollection.minYValue()
|
||||
) * verticalScale
|
||||
|
||||
val prevInnerX = prevX.coerceIn(
|
||||
prevX - radiusScale * size.width,
|
||||
prevX + radiusScale * size.width
|
||||
)
|
||||
val prevInnerY = prevY.coerceIn(0f, size.height)
|
||||
|
||||
cubicTo(
|
||||
prevInnerX + (innerX - prevInnerX) / 2, prevInnerY,
|
||||
prevInnerX + (innerX - prevInnerX) / 2, innerY,
|
||||
innerX, innerY
|
||||
)
|
||||
}
|
||||
if (points.count() < 14) {
|
||||
drawXAxisLabels(
|
||||
data = data.xValue,
|
||||
center = centerOffset,
|
||||
count = points.count(),
|
||||
padding = padding.toPx(),
|
||||
minLabelCount = axisConfig.minLabelCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
// Close the path
|
||||
lineTo(size.width, size.height)
|
||||
lineTo(0f, size.height)
|
||||
close()
|
||||
|
||||
// Draw the background path
|
||||
drawPath(
|
||||
path = this,
|
||||
brush = contentColor
|
||||
)
|
||||
}
|
||||
if (lineConfig.hasDotMarker) {
|
||||
graphPathPoints.fastForEach { point ->
|
||||
drawCircle(
|
||||
brush = dotColor,
|
||||
radius = radiusScale * size.width,
|
||||
center = Offset(point.x, point.y)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.line
|
||||
|
||||
import android.graphics.PointF
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
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.Stroke
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.common.ChartDataCollection
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.math.chartDataToOffset
|
||||
import com.himanshoe.charty.common.maxYValue
|
||||
import com.himanshoe.charty.common.minYValue
|
||||
import com.himanshoe.charty.common.toChartDataCollection
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawXAxisLabels
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
import com.himanshoe.charty.line.config.LineChartColors
|
||||
import com.himanshoe.charty.line.config.LineChartDefaults
|
||||
import com.himanshoe.charty.line.config.LineConfig
|
||||
import com.himanshoe.charty.line.model.LineData
|
||||
|
||||
/**
|
||||
* A composable function that displays a line chart.
|
||||
*
|
||||
* @param dataCollection The collection of chart data points.
|
||||
* @param dotColor The color of the data points.
|
||||
* @param lineColor The color of the line in the chart.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param backgroundColor The background color of the chart.
|
||||
* @param padding The padding around the chart.
|
||||
* @param axisConfig The configuration for the chart's axes.
|
||||
* @param lineConfig The configuration for the line in the chart.
|
||||
* @param radiusScale The scale factor for the radius of the data points.
|
||||
*/
|
||||
@Composable
|
||||
fun LineChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
dotColor: Color,
|
||||
lineColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = Color.White,
|
||||
padding: Dp = 16.dp,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
lineConfig: LineConfig = LineChartDefaults.defaultConfig(),
|
||||
radiusScale: Float = 0.02f,
|
||||
) {
|
||||
LineChart(
|
||||
dataCollection = dataCollection,
|
||||
modifier = modifier,
|
||||
radiusScale = radiusScale,
|
||||
padding = padding,
|
||||
axisConfig = axisConfig,
|
||||
lineConfig = lineConfig,
|
||||
chartColors = LineChartColors(
|
||||
dotColor = listOf(dotColor, dotColor),
|
||||
lineColor = listOf(lineColor, lineColor),
|
||||
backgroundColors = listOf(backgroundColor, backgroundColor)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable function that displays a line chart.
|
||||
*
|
||||
* @param dataCollection The collection of chart data points.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param padding The padding around the chart.
|
||||
* @param axisConfig The configuration for the chart's axes.
|
||||
* @param radiusScale The scale factor for the radius of the data points.
|
||||
* @param lineConfig The configuration for the line in the chart.
|
||||
* @param chartColors The colors used in the chart.
|
||||
*/
|
||||
@Composable
|
||||
fun LineChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: Dp = 16.dp,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
radiusScale: Float = 0.02f,
|
||||
lineConfig: LineConfig = LineChartDefaults.defaultConfig(),
|
||||
chartColors: LineChartColors = LineChartDefaults.defaultColor(),
|
||||
) {
|
||||
val points = dataCollection.data
|
||||
|
||||
var chartWidth by remember { mutableStateOf(0F) }
|
||||
var chartHeight by remember { mutableStateOf(0F) }
|
||||
var pointBound by remember { mutableStateOf(0F) }
|
||||
|
||||
val horizontalScale = chartWidth.div(points.count())
|
||||
val verticalScale = chartHeight.div((dataCollection.maxYValue() - dataCollection.minYValue()))
|
||||
|
||||
ChartSurface(
|
||||
padding = padding,
|
||||
chartData = dataCollection,
|
||||
modifier = modifier,
|
||||
axisConfig = axisConfig
|
||||
) {
|
||||
val lineColor = Brush.linearGradient(chartColors.lineColor)
|
||||
val backgroundColor = Brush.linearGradient(chartColors.backgroundColors)
|
||||
val dotColor = Brush.linearGradient(chartColors.dotColor)
|
||||
|
||||
val minYValue = dataCollection.minYValue()
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
pointBound = size.width.div(
|
||||
points
|
||||
.count()
|
||||
.times(1.2F)
|
||||
)
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val graphPathPoints = mutableListOf<PointF>()
|
||||
val radius = size.width * radiusScale
|
||||
|
||||
Path().apply {
|
||||
val firstData = points.first()
|
||||
val initialX = 0f
|
||||
val initialY = size.height - ((firstData.yValue - minYValue) * verticalScale)
|
||||
moveTo(initialX, initialY) // Move to the initial point
|
||||
|
||||
points.fastForEachIndexed { index, data ->
|
||||
val centerOffset = chartDataToOffset(
|
||||
index,
|
||||
pointBound,
|
||||
size,
|
||||
data.yValue,
|
||||
horizontalScale,
|
||||
)
|
||||
|
||||
val x = centerOffset.x
|
||||
val y = size.height - ((data.yValue - minYValue) * verticalScale)
|
||||
val innerX =
|
||||
x.coerceIn(centerOffset.x - radius / 2, centerOffset.x + radius / 2)
|
||||
val innerY = y.coerceIn(radius, size.height - radius)
|
||||
|
||||
graphPathPoints.add(PointF(innerX, innerY))
|
||||
|
||||
if (points.size > 1) {
|
||||
when (index) {
|
||||
0 -> moveTo(x, y)
|
||||
else -> lineTo(innerX, innerY)
|
||||
}
|
||||
}
|
||||
|
||||
if (points.count() < 14) {
|
||||
drawXAxisLabels(
|
||||
data = data.xValue,
|
||||
center = centerOffset,
|
||||
count = points.count(),
|
||||
padding = padding.toPx(),
|
||||
minLabelCount = axisConfig.minLabelCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
// Close the path
|
||||
lineTo(size.width, size.height)
|
||||
close()
|
||||
|
||||
val pathEffect =
|
||||
if (lineConfig.hasSmoothCurve) PathEffect.cornerPathEffect(radius) else null
|
||||
// Draw the background path
|
||||
drawPath(
|
||||
path = this,
|
||||
brush = lineColor,
|
||||
style = Stroke(width = lineConfig.strokeSize, pathEffect = pathEffect),
|
||||
)
|
||||
}
|
||||
if (lineConfig.hasDotMarker) {
|
||||
graphPathPoints.fastForEach { point ->
|
||||
drawCircle(
|
||||
brush = dotColor,
|
||||
radius = radiusScale * size.width,
|
||||
center = Offset(point.x, point.y)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun LineChartPreview(modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
CurveLineChart(
|
||||
dataCollection = generateMockPointList().toChartDataCollection(),
|
||||
modifier = Modifier
|
||||
.size(450.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateMockPointList(): List<LineData> {
|
||||
return listOf(
|
||||
LineData(0F, "Jan"),
|
||||
LineData(10F, "Feb"),
|
||||
LineData(05F, "Mar"),
|
||||
LineData(50F, "Apr"),
|
||||
LineData(03F, "June"),
|
||||
LineData(9F, "July"),
|
||||
LineData(40F, "Aug"),
|
||||
LineData(60F, "Sept"),
|
||||
LineData(33F, "Oct"),
|
||||
LineData(11F, "Nov"),
|
||||
LineData(27F, "Dec"),
|
||||
LineData(10F, "Jan"),
|
||||
LineData(13F, "Oct"),
|
||||
LineData(-10F, "Nov"),
|
||||
LineData(0F, "Dec"),
|
||||
LineData(10F, "Jan"),
|
||||
)
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.line.config
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Default configuration values for a curved line chart.
|
||||
*/
|
||||
object CurvedLineChartDefaults {
|
||||
|
||||
/**
|
||||
* Returns the default colors for the curved line chart.
|
||||
*
|
||||
* @return The default colors for the curved line chart.
|
||||
*/
|
||||
fun defaultColor() = CurvedLineChartColors(
|
||||
contentColor = listOf(
|
||||
Color(0xffed625d),
|
||||
Color(0xfff79f88)
|
||||
),
|
||||
dotColor = listOf(
|
||||
Color(0xff50c0a8),
|
||||
Color(0xff7a57e3),
|
||||
),
|
||||
backgroundColors = listOf(
|
||||
Color.White,
|
||||
Color.White,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.line.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Represents the colors used in a line chart.
|
||||
*
|
||||
* @property lineColor The colors of the lines in the chart.
|
||||
* @property dotColor The colors of the dots in the chart.
|
||||
* @property backgroundColors The background colors of the chart.
|
||||
*/
|
||||
@Immutable
|
||||
data class LineChartColors(
|
||||
val lineColor: List<Color> = emptyList(),
|
||||
val dotColor: List<Color> = emptyList(),
|
||||
val backgroundColors: List<Color> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents the colors used in a curved line chart.
|
||||
*
|
||||
* @property dotColor The colors of the dots in the chart.
|
||||
* @property backgroundColors The background colors of the chart.
|
||||
* @property contentColor The colors of the content (lines) in the chart.
|
||||
*/
|
||||
@Immutable
|
||||
data class CurvedLineChartColors(
|
||||
val dotColor: List<Color> = emptyList(),
|
||||
val backgroundColors: List<Color> = emptyList(),
|
||||
val contentColor: List<Color> = emptyList(),
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.line.config
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Default configurations and colors for a line chart.
|
||||
*/
|
||||
object LineChartDefaults {
|
||||
|
||||
/**
|
||||
* Returns the default configuration for a line chart.
|
||||
*
|
||||
* @return The default line chart configuration.
|
||||
*/
|
||||
fun defaultConfig() = LineConfig(
|
||||
hasSmoothCurve = true,
|
||||
hasDotMarker = true,
|
||||
strokeSize = 5F
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the default colors for a line chart.
|
||||
*
|
||||
* @return The default line chart colors.
|
||||
*/
|
||||
fun defaultColor() = LineChartColors(
|
||||
lineColor = listOf(
|
||||
Color(0xffed625d),
|
||||
Color(0xfff79f88)
|
||||
),
|
||||
dotColor = listOf(
|
||||
Color(0xff50c0a8),
|
||||
Color(0xff7a57e3),
|
||||
),
|
||||
backgroundColors = listOf(
|
||||
Color.White,
|
||||
Color.White,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.line.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Configuration options for a line in a line chart.
|
||||
*
|
||||
* @property hasSmoothCurve Whether the line should have a smooth curve.
|
||||
* @property hasDotMarker Whether the line should have a dot marker at each data point.
|
||||
* @property strokeSize The size of the line stroke.
|
||||
*/
|
||||
@Immutable
|
||||
data class LineConfig(
|
||||
val hasSmoothCurve: Boolean,
|
||||
val hasDotMarker: Boolean,
|
||||
val strokeSize: Float
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.line.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.himanshoe.charty.common.ChartData
|
||||
|
||||
/**
|
||||
* Represents a data point for a line chart.
|
||||
*
|
||||
* @property yValue The y-axis value of the data point.
|
||||
* @property xValue The x-axis value of the data point.
|
||||
*/
|
||||
@Immutable
|
||||
data class LineData(override val yValue: Float, override val xValue: Any) : ChartData {
|
||||
override val chartString: String
|
||||
get() = "Line Chart"
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.pie
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import com.himanshoe.charty.common.ChartDataCollection
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.config.ChartyLabelTextConfig
|
||||
import com.himanshoe.charty.common.toChartDataCollection
|
||||
import com.himanshoe.charty.pie.config.PieChartConfig
|
||||
import com.himanshoe.charty.pie.config.PieChartDefaults
|
||||
import com.himanshoe.charty.pie.model.PieData
|
||||
|
||||
/**
|
||||
* Displays a pie chart based on the provided data collection.
|
||||
*
|
||||
* @param dataCollection The collection of data points for the pie chart.
|
||||
* @param modifier The modifier for styling the pie chart.
|
||||
* @param textLabelTextConfig The configuration for the text labels in the pie chart.
|
||||
* @param pieChartConfig The configuration for the pie chart.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun PieChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
textLabelTextConfig: ChartyLabelTextConfig = ChartDefaults.defaultTextLabelConfig(),
|
||||
pieChartConfig: PieChartConfig = PieChartDefaults.defaultConfig(),
|
||||
) {
|
||||
val pieChartData = dataCollection.data.fastMap { it.yValue }
|
||||
val total = pieChartData.sum()
|
||||
|
||||
var startAngle = pieChartConfig.startAngle.angle
|
||||
|
||||
Column(modifier) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.aspectRatio(1F)
|
||||
) {
|
||||
val radius = size.minDimension / 3
|
||||
val centerX = size.width / 2
|
||||
val centerY = size.height / 2
|
||||
val holeRadius =
|
||||
if (pieChartConfig.donut) radius * 0.4f else 0f // Adjust the hole radius based on donut config
|
||||
|
||||
dataCollection.data.fastForEach { value ->
|
||||
if (value is PieData) {
|
||||
val sweepAngle = (value.yValue / total) * 360
|
||||
if (pieChartConfig.donut.not()) {
|
||||
drawArc(
|
||||
color = value.color,
|
||||
startAngle = startAngle,
|
||||
sweepAngle = sweepAngle,
|
||||
useCenter = true,
|
||||
)
|
||||
} else {
|
||||
drawArc(
|
||||
color = value.color,
|
||||
startAngle = startAngle,
|
||||
sweepAngle = sweepAngle,
|
||||
useCenter = false,
|
||||
topLeft = Offset(centerX - radius, centerY - radius),
|
||||
size = Size(radius * 2, radius * 2),
|
||||
style = Stroke(width = radius - holeRadius)
|
||||
)
|
||||
}
|
||||
startAngle += sweepAngle
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pieChartConfig.showLabel) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
dataCollection.data.fastMap {
|
||||
if (it is PieData) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(textLabelTextConfig.indicatorSize)
|
||||
.clip(CircleShape)
|
||||
.align(Alignment.CenterVertically)
|
||||
.background(it.color)
|
||||
)
|
||||
Text(
|
||||
text = it.xValue.toString(),
|
||||
fontSize = textLabelTextConfig.textSize,
|
||||
fontStyle = textLabelTextConfig.fontStyle,
|
||||
fontWeight = textLabelTextConfig.fontWeight,
|
||||
fontFamily = textLabelTextConfig.fontFamily,
|
||||
maxLines = textLabelTextConfig.maxLine,
|
||||
overflow = textLabelTextConfig.overflow,
|
||||
modifier = Modifier.padding(
|
||||
end = 8.dp,
|
||||
start = 4.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun PieChartPreview() {
|
||||
val data = listOf(
|
||||
PieData(30f, "Category A", Color.Blue),
|
||||
PieData(20f, "Category B", Color.Red),
|
||||
PieData(10f, "Category C", Color.Green),
|
||||
PieData(10f, "Category C", Color.Black),
|
||||
)
|
||||
PieChart(
|
||||
dataCollection = data.toChartDataCollection(),
|
||||
modifier = Modifier.wrapContentSize()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.pie.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.himanshoe.charty.common.config.StartAngle
|
||||
|
||||
/**
|
||||
* Immutable data class representing the configuration options for a pie chart.
|
||||
*
|
||||
* @param donut Indicates whether the pie chart should be rendered as a donut.
|
||||
* @param showLabel Indicates whether labels should be shown for each pie slice.
|
||||
* @param startAngle The starting angle of the pie chart.
|
||||
*/
|
||||
@Immutable
|
||||
data class PieChartConfig(
|
||||
val donut: Boolean,
|
||||
val showLabel: Boolean,
|
||||
val startAngle: StartAngle = StartAngle.Zero
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.pie.config
|
||||
|
||||
/**
|
||||
* Object providing default configurations for a pie chart.
|
||||
*/
|
||||
object PieChartDefaults {
|
||||
|
||||
/**
|
||||
* Returns the default configuration for a pie chart.
|
||||
*
|
||||
* @return The default configuration.
|
||||
*/
|
||||
fun defaultConfig() = PieChartConfig(donut = true, showLabel = true)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.pie.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.himanshoe.charty.common.ChartData
|
||||
|
||||
/**
|
||||
* Represents a data point in a pie chart.
|
||||
*
|
||||
* @property yValue The value of the data point.
|
||||
* @property xValue The label or identifier of the data point.
|
||||
* @property color The color associated with the data point.
|
||||
*/
|
||||
@Immutable
|
||||
data class PieData(override val yValue: Float, override val xValue: Any, val color: Color) :
|
||||
ChartData {
|
||||
override val chartString: String
|
||||
get() = "Pie Chart"
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.point
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.common.ChartDataCollection
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartColors
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.math.chartDataToOffset
|
||||
import com.himanshoe.charty.common.maxYValue
|
||||
import com.himanshoe.charty.common.minYValue
|
||||
import com.himanshoe.charty.common.toChartDataCollection
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawXAxisLabels
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
import com.himanshoe.charty.point.config.PointType
|
||||
import com.himanshoe.charty.point.model.PointData
|
||||
|
||||
/**
|
||||
* Displays a point chart using the provided data.
|
||||
*
|
||||
* @param dataCollection The collection of chart data.
|
||||
* @param contentColor The color of the chart points.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param backgroundColor The background color of the chart.
|
||||
* @param padding The padding around the chart.
|
||||
* @param pointType The type of point to use in the chart.
|
||||
* @param axisConfig The configuration for the chart axes.
|
||||
* @param radiusScale The scale factor for the point radius.
|
||||
*/
|
||||
@Composable
|
||||
fun PointChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
contentColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = Color.White,
|
||||
padding: Dp = 16.dp,
|
||||
pointType: PointType = PointType.Stroke(),
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
radiusScale: Float = 0.02f,
|
||||
) {
|
||||
PointChart(
|
||||
dataCollection = dataCollection,
|
||||
modifier = modifier,
|
||||
padding = padding,
|
||||
pointType = pointType,
|
||||
axisConfig = axisConfig,
|
||||
radiusScale = radiusScale,
|
||||
chartColors = ChartColors(
|
||||
contentColor = listOf(contentColor, contentColor),
|
||||
backgroundColors = listOf(backgroundColor, backgroundColor)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a point chart using the provided data.
|
||||
*
|
||||
* @param dataCollection The collection of chart data.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param padding The padding around the chart.
|
||||
* @param pointType The type of point to use in the chart.
|
||||
* @param axisConfig The configuration for the chart axes.
|
||||
* @param radiusScale The scale factor for the point radius.
|
||||
* @param chartColors The colors used in the chart.
|
||||
*/
|
||||
@Composable
|
||||
fun PointChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: Dp = 16.dp,
|
||||
pointType: PointType = PointType.Stroke(),
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
radiusScale: Float = 0.02f,
|
||||
chartColors: ChartColors = ChartDefaults.colorDefaults(),
|
||||
) {
|
||||
val points = dataCollection.data
|
||||
|
||||
var chartWidth by remember { mutableStateOf(0F) }
|
||||
var chartHeight by remember { mutableStateOf(0F) }
|
||||
var pointBound by remember { mutableStateOf(0F) }
|
||||
|
||||
val horizontalScale = chartWidth.div(points.count())
|
||||
val verticalScale = chartHeight.div((dataCollection.maxYValue() - dataCollection.minYValue()))
|
||||
|
||||
ChartSurface(
|
||||
padding = padding,
|
||||
chartData = dataCollection,
|
||||
modifier = modifier,
|
||||
axisConfig = axisConfig
|
||||
) {
|
||||
val contentColor = Brush.linearGradient(chartColors.contentColor)
|
||||
val backgroundColor = Brush.linearGradient(chartColors.backgroundColors)
|
||||
|
||||
val minYValue = dataCollection.minYValue()
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
pointBound = size.width.div(
|
||||
points
|
||||
.count()
|
||||
.times(1.2F)
|
||||
)
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val radius = chartWidth * radiusScale
|
||||
|
||||
points.fastForEachIndexed { index, point ->
|
||||
val centerOffset = chartDataToOffset(
|
||||
index,
|
||||
pointBound,
|
||||
size,
|
||||
point.yValue,
|
||||
horizontalScale,
|
||||
)
|
||||
|
||||
val x = centerOffset.x
|
||||
val y = chartHeight - ((point.yValue - minYValue) * verticalScale)
|
||||
val clampedX = x.coerceIn(centerOffset.x - radius / 2, centerOffset.x + radius / 2)
|
||||
val clampedY = y.coerceIn(radius, chartHeight - radius)
|
||||
|
||||
val style = when (pointType) {
|
||||
is PointType.Stroke -> Stroke(width = pointType.strokeWidth)
|
||||
else -> Fill
|
||||
}
|
||||
drawCircle(
|
||||
center = Offset(clampedX, clampedY),
|
||||
style = style,
|
||||
radius = radius,
|
||||
brush = contentColor
|
||||
)
|
||||
|
||||
if (points.count() < 14) {
|
||||
drawXAxisLabels(
|
||||
data = point.xValue,
|
||||
center = centerOffset,
|
||||
count = points.count(),
|
||||
padding = padding.toPx(),
|
||||
minLabelCount = axisConfig.minLabelCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun PointChartPreview(modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
PointChart(
|
||||
dataCollection = generateMockPointList().toChartDataCollection(),
|
||||
modifier = Modifier
|
||||
.size(450.dp),
|
||||
contentColor = Color.Red,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateMockPointList(): List<PointData> {
|
||||
return listOf(
|
||||
PointData(-10F, "Jan"),
|
||||
PointData(10F, "Feb"),
|
||||
PointData(05F, "Mar"),
|
||||
PointData(50F, "Apr"),
|
||||
PointData(03F, "June"),
|
||||
PointData(9F, "July"),
|
||||
PointData(40F, "Aug"),
|
||||
PointData(60F, "Sept"),
|
||||
PointData(33F, "Oct"),
|
||||
PointData(11F, "Nov"),
|
||||
PointData(27F, "Dec"),
|
||||
PointData(10F, "Jan"),
|
||||
PointData(73F, "Oct"),
|
||||
PointData(-20F, "Nov"),
|
||||
PointData(0F, "Dec"),
|
||||
PointData(10F, "Jan"),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.point.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Represents the type of a point in a chart.
|
||||
*/
|
||||
sealed interface PointType {
|
||||
/**
|
||||
* A filled point type.
|
||||
*/
|
||||
object Fill : PointType
|
||||
|
||||
/**
|
||||
* A stroke point type with a specified stroke width.
|
||||
*
|
||||
* @param strokeWidth The stroke width for the point.
|
||||
*/
|
||||
@Immutable
|
||||
data class Stroke(val strokeWidth: Float = 4F) : PointType
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.point.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.himanshoe.charty.common.ChartData
|
||||
|
||||
/**
|
||||
* Represents data for a point in a chart.
|
||||
*
|
||||
* @param yValue The y-value of the point.
|
||||
* @param xValue The x-value of the point.
|
||||
*/
|
||||
@Immutable
|
||||
data class PointData(override val yValue: Float, override val xValue: Any) : ChartData {
|
||||
/**
|
||||
* Returns the string representation of the chart type.
|
||||
*/
|
||||
override val chartString: String
|
||||
get() = "Point Chart"
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package com.himanshoe.charty.radar
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
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.Stroke
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.common.ChartDataCollection
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.maxYValue
|
||||
import com.himanshoe.charty.common.minYValue
|
||||
import com.himanshoe.charty.radar.config.RadarChartColors
|
||||
import com.himanshoe.charty.radar.config.RadarChartDefaults
|
||||
import com.himanshoe.charty.radar.config.RadarConfig
|
||||
import com.himanshoe.charty.radar.model.RadarData
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* A composable function that displays a radar chart.
|
||||
*
|
||||
* @param dataCollection The collection of chart data points.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param axisConfig The configuration for the chart's axes.
|
||||
* @param radarConfig The configuration for the polygon in the chart.
|
||||
* @param chartColors The colors used in the chart.
|
||||
* @param radiusScale The scale factor for the radius of the data points.
|
||||
*/
|
||||
@Composable
|
||||
fun RadarChart(
|
||||
dataCollection: ChartDataCollection,
|
||||
modifier: Modifier = Modifier,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
radarConfig: RadarConfig = RadarChartDefaults.defaultConfig(),
|
||||
chartColors: RadarChartColors = RadarChartDefaults.defaultColor(),
|
||||
radiusScale: Float = 0.02f,
|
||||
) {
|
||||
val maxYValue by remember { mutableStateOf(dataCollection.maxYValue()) }
|
||||
val minYValue by remember { mutableStateOf(dataCollection.minYValue()) }
|
||||
val diff = abs(maxYValue * 0.1F)
|
||||
val dataRange = (minYValue - diff)..(maxYValue + diff)
|
||||
val partitionAngle = (2 * Math.PI / dataCollection.data.size).toFloat()
|
||||
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.drawBehind {
|
||||
drawRadarAxis(
|
||||
dataCollection = dataCollection,
|
||||
dataRange = dataRange,
|
||||
radius = size.minDimension / 3,
|
||||
center = PointF(size.width / 2, size.height / 2),
|
||||
axisConfig = axisConfig,
|
||||
)
|
||||
}
|
||||
) {
|
||||
val radius = size.minDimension / 3
|
||||
val centerX = size.width / 2
|
||||
val centerY = size.height / 2
|
||||
|
||||
val scaleFactor = radius / (dataRange.endInclusive - dataRange.start)
|
||||
|
||||
val lineColor = Brush.linearGradient(chartColors.lineColor)
|
||||
val dotColor = Brush.linearGradient(chartColors.dotColor)
|
||||
|
||||
val radarPolygonVertices = mutableListOf<PointF>()
|
||||
|
||||
Path().apply {
|
||||
dataCollection.data.fastForEachIndexed { index, chartData ->
|
||||
val angle = index * partitionAngle
|
||||
val x = centerX + scaleFactor * (chartData.yValue - dataRange.start) * cos(angle)
|
||||
val y = centerY + scaleFactor * (chartData.yValue - dataRange.start) * sin(angle)
|
||||
if (index == 0) {
|
||||
moveTo(x, y)
|
||||
} else {
|
||||
lineTo(x, y)
|
||||
}
|
||||
|
||||
radarPolygonVertices.add(PointF(x, y))
|
||||
}
|
||||
close()
|
||||
|
||||
drawPath(
|
||||
path = this,
|
||||
brush = lineColor,
|
||||
style = Stroke(width = radarConfig.strokeSize)
|
||||
)
|
||||
if (radarConfig.fillPolygon) {
|
||||
drawPath(
|
||||
path = this,
|
||||
brush = Brush.linearGradient(chartColors.fillColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (radarConfig.hasDotMarker) {
|
||||
radarPolygonVertices.fastForEach {
|
||||
drawCircle(
|
||||
brush = dotColor,
|
||||
radius = radiusScale * size.width,
|
||||
center = Offset(it.x, it.y)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawRadarAxis(
|
||||
dataCollection: ChartDataCollection,
|
||||
dataRange: ClosedFloatingPointRange<Float>,
|
||||
radius: Float,
|
||||
center: PointF,
|
||||
axisConfig: AxisConfig,
|
||||
) {
|
||||
val partitionAngle = (2 * Math.PI / dataCollection.data.size).toFloat()
|
||||
val axisPolygons = if (axisConfig.showGridLines) {
|
||||
listOf(Path(), Path(), Path(), Path())
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
for (spokeIndex in 0 until dataCollection.data.size) {
|
||||
val angle = spokeIndex * partitionAngle
|
||||
|
||||
if (axisConfig.showAxes) {
|
||||
drawLine(
|
||||
start = Offset(center.x, center.y),
|
||||
end = Offset(center.x + radius * cos(angle), center.y + radius * sin(angle)),
|
||||
color = axisConfig.axisColor,
|
||||
strokeWidth = axisConfig.axisStroke
|
||||
)
|
||||
}
|
||||
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
dataCollection.data[spokeIndex].xValue.toString(),
|
||||
center.x + (radius + 30F) * cos(angle),
|
||||
center.y + (radius + 30F) * sin(angle),
|
||||
Paint().apply {
|
||||
this.color = axisConfig.axisColor.toArgb()
|
||||
this.textSize = size.width / 30
|
||||
this.textAlign = if (angle > Math.PI / 2 && angle < 3 * Math.PI / 2) {
|
||||
Paint.Align.RIGHT
|
||||
} else {
|
||||
Paint.Align.LEFT
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
axisPolygons.fastForEachIndexed { i, path ->
|
||||
val scale = 1F - 0.25F * i
|
||||
if (spokeIndex == 0) {
|
||||
path.moveTo(center.x + scale * radius * cos(angle), center.y + scale * radius * sin(angle))
|
||||
} else {
|
||||
path.lineTo(center.x + scale * radius * cos(angle), center.y + scale * radius * sin(angle))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!axisConfig.showGridLines) return
|
||||
|
||||
axisPolygons.fastForEach {
|
||||
it.close()
|
||||
drawPath(
|
||||
path = it,
|
||||
color = axisConfig.gridColor,
|
||||
style = Stroke(width = axisConfig.axisStroke)
|
||||
)
|
||||
}
|
||||
|
||||
if (!axisConfig.showGridLabel) return
|
||||
|
||||
drawGridLabels(
|
||||
radius = radius,
|
||||
center = center,
|
||||
dataRange = dataRange,
|
||||
angle = (partitionAngle / 2),
|
||||
labelCount = axisPolygons.size,
|
||||
labelColor = axisConfig.gridColor
|
||||
)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawGridLabels(
|
||||
radius: Float,
|
||||
center: PointF,
|
||||
dataRange: ClosedFloatingPointRange<Float>,
|
||||
angle: Float,
|
||||
labelCount: Int,
|
||||
labelColor: Color
|
||||
) {
|
||||
val reduceBy = 1F / labelCount
|
||||
for (i in 0 until labelCount) {
|
||||
val scale = 1F - reduceBy * i
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
(dataRange.start + (dataRange.endInclusive - dataRange.start) * scale).toString(),
|
||||
center.x + scale * radius * cos(angle),
|
||||
center.y + scale * radius * sin(angle),
|
||||
Paint().apply {
|
||||
this.color = labelColor.toArgb()
|
||||
this.textSize = size.width / 30
|
||||
this.textAlign = Paint.Align.CENTER
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun RadarChartPreview() {
|
||||
RadarChart(
|
||||
dataCollection = ChartDataCollection(
|
||||
listOf(
|
||||
RadarData(30f, "AAAAAA"),
|
||||
RadarData(25f, "BBBBBB"),
|
||||
RadarData(20f, "CCCCCC"),
|
||||
RadarData(15f, "DDDDDD"),
|
||||
RadarData(10f, "EEEEEE"),
|
||||
)
|
||||
),
|
||||
modifier = Modifier.size(350.dp),
|
||||
axisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
radarConfig = RadarChartDefaults.defaultConfig().copy(
|
||||
fillPolygon = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.himanshoe.charty.radar.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Represents the colors used in a radar chart.
|
||||
*
|
||||
* @property lineColor The colors of the lines in the chart.
|
||||
* @property dotColor The colors of the dots in the chart.
|
||||
* @property fillColor The fill color of the polygon in the chart.
|
||||
*/
|
||||
@Immutable
|
||||
data class RadarChartColors(
|
||||
val lineColor: List<Color>,
|
||||
val dotColor: List<Color>,
|
||||
val fillColor: List<Color> = lineColor.map { it.copy(alpha = 0.5F) }
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.himanshoe.charty.radar.config
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Default configurations and colors for a radar chart.
|
||||
*/
|
||||
object RadarChartDefaults {
|
||||
|
||||
/**
|
||||
* Returns the default configuration for a radar chart.
|
||||
*
|
||||
* @return The default radar chart configuration.
|
||||
*/
|
||||
fun defaultConfig(): RadarConfig =
|
||||
RadarConfig(
|
||||
hasDotMarker = true,
|
||||
strokeSize = 5F,
|
||||
fillPolygon = true,
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the default colors for a radar chart.
|
||||
*
|
||||
* @return The default radar chart colors.
|
||||
*/
|
||||
fun defaultColor(): RadarChartColors =
|
||||
RadarChartColors(
|
||||
lineColor = listOf(
|
||||
Color(0xffed625d),
|
||||
Color(0xfff79f88)
|
||||
),
|
||||
dotColor = listOf(
|
||||
Color(0xff50c0a8),
|
||||
Color(0xff7a57e3),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.himanshoe.charty.radar.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Configuration options for a data polygon in a radar chart.
|
||||
*
|
||||
* @property hasDotMarker Whether the polygon should have a dot marker at each data point.
|
||||
* @property strokeSize The size of the line stroke.
|
||||
* @property fillPolygon Whether the polygon should be filled with a color.
|
||||
*/
|
||||
@Immutable
|
||||
data class RadarConfig(
|
||||
val hasDotMarker: Boolean,
|
||||
val strokeSize: Float,
|
||||
val fillPolygon: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.himanshoe.charty.radar.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.himanshoe.charty.common.ChartData
|
||||
|
||||
/**
|
||||
* Represents a data point for a radar chart.
|
||||
*
|
||||
* @property yValue The value of the data point.
|
||||
* @property xValue The label or identifier of the data point.
|
||||
*/
|
||||
@Immutable
|
||||
data class RadarData(
|
||||
override val yValue: Float,
|
||||
override val xValue: Any,
|
||||
) : ChartData {
|
||||
override val chartString: String
|
||||
get() = "Radar Chart"
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.stacked
|
||||
|
||||
import android.graphics.Paint
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.himanshoe.charty.common.ChartSurface
|
||||
import com.himanshoe.charty.common.ComposeList
|
||||
import com.himanshoe.charty.common.config.AxisConfig
|
||||
import com.himanshoe.charty.common.config.ChartDefaults
|
||||
import com.himanshoe.charty.common.config.ChartyLabelTextConfig
|
||||
import com.himanshoe.charty.common.toComposeList
|
||||
import com.himanshoe.charty.common.ui.drawGridLines
|
||||
import com.himanshoe.charty.common.ui.drawXAxis
|
||||
import com.himanshoe.charty.common.ui.drawYAxis
|
||||
import com.himanshoe.charty.stacked.config.StackBarData
|
||||
|
||||
/**
|
||||
* Composable function that renders a stacked bar chart.
|
||||
*
|
||||
* @param stackBarData The collection of stack bar data.
|
||||
* @param modifier The modifier for the chart.
|
||||
* @param axisConfig The configuration for the chart axes.
|
||||
* @param padding The padding around the chart.
|
||||
* @param spacing The spacing between stack bars.
|
||||
* @param textLabelTextConfig The configuration for the text labels in the chart.
|
||||
*/
|
||||
@Composable
|
||||
fun StackedBarChart(
|
||||
stackBarData: ComposeList<StackBarData>,
|
||||
modifier: Modifier = Modifier,
|
||||
axisConfig: AxisConfig = ChartDefaults.axisConfigDefaults(),
|
||||
padding: Dp = 16.dp,
|
||||
spacing: Dp = 4.dp,
|
||||
textLabelTextConfig: ChartyLabelTextConfig = ChartDefaults.defaultTextLabelConfig(),
|
||||
) {
|
||||
val allDataPoints = stackBarData.data.map {
|
||||
it.dataPoints.sum()
|
||||
}.toMutableList().apply {
|
||||
add(0F)
|
||||
}
|
||||
|
||||
val maxSum = stackBarData.data.maxOfOrNull {
|
||||
it.dataPoints.sum()
|
||||
} ?: 0f
|
||||
|
||||
var chartWidth by remember { mutableStateOf(0f) }
|
||||
var chartHeight by remember { mutableStateOf(0f) }
|
||||
|
||||
ChartSurface(
|
||||
modifier = modifier,
|
||||
padding = padding,
|
||||
axisConfig = axisConfig,
|
||||
chartData = allDataPoints.toComposeList()
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
chartWidth = size.width.toFloat()
|
||||
chartHeight = size.height.toFloat()
|
||||
}
|
||||
.drawBehind {
|
||||
if (axisConfig.showAxes) {
|
||||
drawYAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
drawXAxis(axisConfig.axisColor, axisConfig.axisStroke)
|
||||
}
|
||||
if (axisConfig.showGridLines) {
|
||||
drawGridLines(chartWidth, chartHeight, padding.toPx())
|
||||
}
|
||||
}
|
||||
) {
|
||||
val width = chartWidth - (padding).toPx()
|
||||
val height = chartHeight - (padding).toPx()
|
||||
val scaleFactor = chartHeight / maxSum
|
||||
|
||||
val barCount = stackBarData.data.size
|
||||
val totalSpacing = (barCount - 1) * spacing.toPx()
|
||||
val barWidth = (width - totalSpacing) / barCount.toFloat()
|
||||
|
||||
stackBarData.data.fastForEachIndexed { index, stackValues ->
|
||||
val x = (index * (barWidth + spacing.toPx()))
|
||||
var startY = height + padding.toPx()
|
||||
|
||||
if (axisConfig.showGridLabel) {
|
||||
when {
|
||||
barCount < 14 -> {
|
||||
drawLabel(
|
||||
stackValues = stackValues,
|
||||
index = index,
|
||||
barWidth = barWidth + spacing.toPx(),
|
||||
chartHeight = chartHeight,
|
||||
textLabelTextConfig = textLabelTextConfig
|
||||
)
|
||||
}
|
||||
|
||||
index == 0 || index == barCount / 2 || index == barCount - 1 -> {
|
||||
drawLabel(
|
||||
stackValues = stackValues,
|
||||
index = index,
|
||||
barWidth = barWidth + spacing.toPx(),
|
||||
chartHeight = chartHeight,
|
||||
textLabelTextConfig = textLabelTextConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stackValues.dataPoints.fastForEachIndexed { stackIndex, value ->
|
||||
val stackHeight = value * scaleFactor
|
||||
val endY = startY - stackHeight
|
||||
|
||||
val clampedEndY = startY.coerceAtMost(endY)
|
||||
val clampedStackHeight = startY - clampedEndY
|
||||
|
||||
drawRect(
|
||||
color = stackValues.colors.getOrElse(stackIndex) { Color.Gray },
|
||||
topLeft = Offset(x, clampedEndY),
|
||||
size = Size(barWidth, clampedStackHeight)
|
||||
)
|
||||
|
||||
startY = endY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawLabel(
|
||||
stackValues: StackBarData,
|
||||
index: Int,
|
||||
barWidth: Float,
|
||||
chartHeight: Float,
|
||||
textLabelTextConfig: ChartyLabelTextConfig
|
||||
) {
|
||||
val label = stackValues.label
|
||||
val labelY = chartHeight + size.width / 20
|
||||
val labelX = when (index) {
|
||||
0 -> 0.5f * barWidth
|
||||
else -> (index * barWidth) + (barWidth / 2f)
|
||||
}
|
||||
|
||||
drawIntoCanvas {
|
||||
it.nativeCanvas.drawText(
|
||||
label,
|
||||
labelX,
|
||||
labelY,
|
||||
Paint().apply {
|
||||
color = textLabelTextConfig.textColor.toArgb()
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = size.width / 35f
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.stacked.config
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Represents the data for a stack bar chart.
|
||||
*
|
||||
* @param label The label associated with the data.
|
||||
* @param dataPoints The list of data points for each stack.
|
||||
* @param colors The list of colors for each stack (default is transparent for each stack).
|
||||
*/
|
||||
@Immutable
|
||||
data class StackBarData(
|
||||
val label: String,
|
||||
val dataPoints: List<Float>,
|
||||
val colors: List<Color> = List(dataPoints.count()) { Color.Transparent }
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* **************
|
||||
* Charty Library : Android
|
||||
*
|
||||
* Copyright (c) 2023. Charty Contributor
|
||||
* **************
|
||||
*/
|
||||
|
||||
package com.himanshoe.charty.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Composable
|
||||
fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.toPx() }
|
||||
|
||||
@Composable
|
||||
fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() }
|
||||
Reference in New Issue
Block a user