This commit is contained in:
coco
2026-07-03 15:56:07 +08:00
commit caef23209c
5767 changed files with 1004268 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+76
View File
@@ -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")
}
View File
+24
View File
@@ -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
+21
View File
@@ -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
)
@@ -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
)
@@ -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
)
}
@@ -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"),
)
}
@@ -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() }