的
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,26 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
apply(from = rootProject.file("gradle/configure-android.gradle"))
|
||||
apply(from = rootProject.file("gradle/configure-compose.gradle"))
|
||||
apply(from = rootProject.file("gradle/jitpack-publish.gradle"))
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:common"))
|
||||
|
||||
implementation(Kotlin.stdLib)
|
||||
|
||||
implementation(Compose.animation)
|
||||
implementation(Compose.core)
|
||||
implementation(Compose.layout)
|
||||
implementation(Compose.foundation)
|
||||
implementation(Compose.material)
|
||||
implementation(Compose.runtime)
|
||||
debugImplementation(Compose.tooling)
|
||||
}
|
||||
android {
|
||||
namespace = "com.github.tehras.charts.bar"
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.github.tehras.charts.bar
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import com.github.tehras.charts.bar.BarChartUtils.axisAreas
|
||||
import com.github.tehras.charts.bar.BarChartUtils.barDrawableArea
|
||||
import com.github.tehras.charts.bar.BarChartUtils.forEachWithArea
|
||||
import com.github.tehras.charts.bar.renderer.bar.BarDrawer
|
||||
import com.github.tehras.charts.bar.renderer.bar.SimpleBarDrawer
|
||||
import com.github.tehras.charts.bar.renderer.label.LabelDrawer
|
||||
import com.github.tehras.charts.bar.renderer.label.SimpleValueDrawer
|
||||
import com.github.tehras.charts.bar.renderer.xaxis.SimpleXAxisDrawer
|
||||
import com.github.tehras.charts.bar.renderer.xaxis.XAxisDrawer
|
||||
import com.github.tehras.charts.bar.renderer.yaxis.SimpleYAxisDrawer
|
||||
import com.github.tehras.charts.bar.renderer.yaxis.YAxisDrawer
|
||||
import com.github.tehras.charts.piechart.animation.simpleChartAnimation
|
||||
|
||||
@Composable
|
||||
fun BarChart(
|
||||
barChartData: BarChartData,
|
||||
modifier: Modifier = Modifier,
|
||||
animation: AnimationSpec<Float> = simpleChartAnimation(),
|
||||
barDrawer: BarDrawer = SimpleBarDrawer(),
|
||||
xAxisDrawer: XAxisDrawer = SimpleXAxisDrawer(),
|
||||
yAxisDrawer: YAxisDrawer = SimpleYAxisDrawer(),
|
||||
labelDrawer: LabelDrawer = SimpleValueDrawer()
|
||||
) {
|
||||
val transitionAnimation = remember(barChartData.bars) { Animatable(initialValue = 0f) }
|
||||
|
||||
LaunchedEffect(barChartData.bars) {
|
||||
transitionAnimation.animateTo(1f, animationSpec = animation)
|
||||
}
|
||||
|
||||
val progress = transitionAnimation.value
|
||||
|
||||
Canvas(modifier = modifier
|
||||
.fillMaxSize()
|
||||
.drawBehind {
|
||||
drawIntoCanvas { canvas ->
|
||||
val (xAxisArea, yAxisArea) = axisAreas(
|
||||
drawScope = this,
|
||||
totalSize = size,
|
||||
xAxisDrawer = xAxisDrawer,
|
||||
labelDrawer = labelDrawer
|
||||
)
|
||||
val barDrawableArea = barDrawableArea(xAxisArea)
|
||||
|
||||
// Draw yAxis line.
|
||||
yAxisDrawer.drawAxisLine(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
drawableArea = yAxisArea
|
||||
)
|
||||
|
||||
// Draw xAxis line.
|
||||
xAxisDrawer.drawAxisLine(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
drawableArea = xAxisArea
|
||||
)
|
||||
// Draw each bar.
|
||||
barChartData.forEachWithArea(
|
||||
this,
|
||||
barDrawableArea,
|
||||
progress,
|
||||
labelDrawer
|
||||
) { barArea, bar ->
|
||||
barDrawer.drawBar(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
barArea = barArea,
|
||||
bar = bar
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
/**
|
||||
* Typically we could draw everything here, but because of the lack of canvas.drawText
|
||||
* APIs we have to use Android's `nativeCanvas` which seems to be drawn behind
|
||||
* Compose's canvas.
|
||||
*/
|
||||
drawIntoCanvas { canvas ->
|
||||
val (xAxisArea, yAxisArea) = axisAreas(
|
||||
drawScope = this,
|
||||
totalSize = size,
|
||||
xAxisDrawer = xAxisDrawer,
|
||||
labelDrawer = labelDrawer
|
||||
)
|
||||
val barDrawableArea = barDrawableArea(xAxisArea)
|
||||
|
||||
barChartData.forEachWithArea(
|
||||
this,
|
||||
barDrawableArea,
|
||||
progress,
|
||||
labelDrawer
|
||||
) { barArea, bar ->
|
||||
labelDrawer.drawLabel(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
label = bar.label,
|
||||
barArea = barArea,
|
||||
xAxisArea = xAxisArea
|
||||
)
|
||||
}
|
||||
|
||||
yAxisDrawer.drawAxisLabels(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
minValue = barChartData.minYValue,
|
||||
maxValue = barChartData.maxYValue,
|
||||
drawableArea = yAxisArea
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.github.tehras.charts.bar
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
data class BarChartData(
|
||||
val bars: List<Bar>,
|
||||
val padBy: Float = 10f,
|
||||
val startAtZero: Boolean = true
|
||||
) {
|
||||
init {
|
||||
require(padBy in 0f..100f)
|
||||
}
|
||||
|
||||
private val yMinMax: Pair<Float, Float>
|
||||
get() {
|
||||
val min = bars.minByOrNull { it.value }?.value ?: 0f
|
||||
val max = bars.maxByOrNull { it.value }?.value ?: 0f
|
||||
|
||||
return min to max
|
||||
}
|
||||
internal val maxYValue: Float =
|
||||
yMinMax.second + ((yMinMax.second - yMinMax.first) * padBy / 100f)
|
||||
internal val minYValue: Float
|
||||
get() {
|
||||
return if (startAtZero) {
|
||||
0f
|
||||
} else {
|
||||
yMinMax.first - ((yMinMax.second - yMinMax.first) * padBy / 100f)
|
||||
}
|
||||
}
|
||||
|
||||
val maxBarValue = bars.maxByOrNull { it.value }?.value ?: 0f
|
||||
|
||||
data class Bar(
|
||||
val value: Float,
|
||||
val color: Color,
|
||||
val label: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.github.tehras.charts.bar
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.tehras.charts.bar.renderer.label.LabelDrawer
|
||||
import com.github.tehras.charts.bar.renderer.xaxis.XAxisDrawer
|
||||
|
||||
internal object BarChartUtils {
|
||||
fun axisAreas(
|
||||
drawScope: DrawScope,
|
||||
totalSize: Size,
|
||||
xAxisDrawer: XAxisDrawer,
|
||||
labelDrawer: LabelDrawer
|
||||
): Pair<Rect, Rect> = with(drawScope) {
|
||||
// yAxis
|
||||
val yAxisTop = labelDrawer.requiredAboveBarHeight(drawScope)
|
||||
|
||||
// Either 50dp or 10% of the chart width.
|
||||
val yAxisRight = minOf(50.dp.toPx(), size.width * 10f / 100f)
|
||||
|
||||
// xAxis
|
||||
val xAxisRight = totalSize.width
|
||||
|
||||
// Measure the size of the text and line.
|
||||
val xAxisTop = totalSize.height - xAxisDrawer.requiredHeight(drawScope)
|
||||
|
||||
return Pair(
|
||||
Rect(yAxisRight, xAxisTop, xAxisRight, totalSize.height),
|
||||
Rect(0f, yAxisTop, yAxisRight, xAxisTop)
|
||||
)
|
||||
}
|
||||
|
||||
fun barDrawableArea(xAxisArea: Rect): Rect {
|
||||
return Rect(
|
||||
left = xAxisArea.left,
|
||||
top = 0f,
|
||||
right = xAxisArea.right,
|
||||
bottom = xAxisArea.top
|
||||
)
|
||||
}
|
||||
|
||||
fun BarChartData.forEachWithArea(
|
||||
drawScope: DrawScope,
|
||||
barDrawableArea: Rect,
|
||||
progress: Float,
|
||||
labelDrawer: LabelDrawer,
|
||||
block: (barArea: Rect, bar: BarChartData.Bar) -> Unit
|
||||
) {
|
||||
val totalBars = bars.size
|
||||
val widthOfBarArea = barDrawableArea.width / totalBars
|
||||
val offsetOfBar = widthOfBarArea * 0.2f
|
||||
|
||||
bars.forEachIndexed { index, bar ->
|
||||
val left = barDrawableArea.left + (index * widthOfBarArea)
|
||||
val height = barDrawableArea.height
|
||||
|
||||
val barHeight = (height - labelDrawer.requiredAboveBarHeight(drawScope)) * progress
|
||||
|
||||
val barArea = Rect(
|
||||
left = left + offsetOfBar,
|
||||
top = barDrawableArea.bottom - (bar.value / maxBarValue) * barHeight,
|
||||
right = left + widthOfBarArea - offsetOfBar,
|
||||
bottom = barDrawableArea.bottom
|
||||
)
|
||||
|
||||
block(barArea, bar)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.github.tehras.charts.bar.renderer.bar
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import com.github.tehras.charts.bar.BarChartData
|
||||
|
||||
interface BarDrawer {
|
||||
fun drawBar(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
barArea: Rect,
|
||||
bar: BarChartData.Bar
|
||||
)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.github.tehras.charts.bar.renderer.bar
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import com.github.tehras.charts.bar.BarChartData
|
||||
|
||||
class SimpleBarDrawer : BarDrawer {
|
||||
private val barPaint = Paint().apply {
|
||||
this.isAntiAlias = true
|
||||
}
|
||||
|
||||
override fun drawBar(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
barArea: Rect,
|
||||
bar: BarChartData.Bar
|
||||
) {
|
||||
canvas.drawRect(barArea, barPaint.apply {
|
||||
color = bar.color
|
||||
})
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.github.tehras.charts.bar.renderer.label
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
interface LabelDrawer {
|
||||
fun requiredXAxisHeight(drawScope: DrawScope): Float = 0f
|
||||
fun requiredAboveBarHeight(drawScope: DrawScope): Float = 0f
|
||||
|
||||
fun drawLabel(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
label: String,
|
||||
barArea: Rect,
|
||||
xAxisArea: Rect
|
||||
)
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.github.tehras.charts.bar.renderer.label
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.github.tehras.charts.bar.renderer.label.SimpleValueDrawer.DrawLocation.Inside
|
||||
import com.github.tehras.charts.bar.renderer.label.SimpleValueDrawer.DrawLocation.Outside
|
||||
import com.github.tehras.charts.bar.renderer.label.SimpleValueDrawer.DrawLocation.XAxis
|
||||
import com.github.tehras.charts.piechart.utils.toLegacyInt
|
||||
|
||||
class SimpleValueDrawer(
|
||||
private val drawLocation: DrawLocation = Inside,
|
||||
private val labelTextSize: TextUnit = 12.sp,
|
||||
private val labelTextColor: Color = Color.Black
|
||||
) : LabelDrawer {
|
||||
private val _labelTextArea: Float? = null
|
||||
private val paint = android.graphics.Paint().apply {
|
||||
this.textAlign = android.graphics.Paint.Align.CENTER
|
||||
this.color = labelTextColor.toLegacyInt()
|
||||
}
|
||||
|
||||
override fun requiredAboveBarHeight(drawScope: DrawScope): Float = when (drawLocation) {
|
||||
Outside -> (3f / 2f) * labelTextHeight(drawScope)
|
||||
Inside,
|
||||
XAxis -> 0f
|
||||
}
|
||||
|
||||
override fun requiredXAxisHeight(drawScope: DrawScope): Float = when (drawLocation) {
|
||||
XAxis -> labelTextHeight(drawScope)
|
||||
Inside,
|
||||
Outside -> 0f
|
||||
}
|
||||
|
||||
override fun drawLabel(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
label: String,
|
||||
barArea: Rect,
|
||||
xAxisArea: Rect
|
||||
) = with(drawScope) {
|
||||
val xCenter = barArea.left + (barArea.width / 2)
|
||||
|
||||
val yCenter = when (drawLocation) {
|
||||
Inside -> (barArea.top + barArea.bottom) / 2
|
||||
Outside -> (barArea.top) - labelTextSize.toPx() / 2
|
||||
XAxis -> barArea.bottom + labelTextHeight(drawScope)
|
||||
}
|
||||
|
||||
canvas.nativeCanvas.drawText(label, xCenter, yCenter, paint(drawScope))
|
||||
}
|
||||
|
||||
private fun labelTextHeight(drawScope: DrawScope) = with(drawScope) {
|
||||
_labelTextArea ?: ((3f / 2f) * labelTextSize.toPx())
|
||||
}
|
||||
|
||||
private fun paint(drawScope: DrawScope) = with(drawScope) {
|
||||
paint.apply {
|
||||
this.textSize = labelTextSize.toPx()
|
||||
}
|
||||
}
|
||||
|
||||
enum class DrawLocation {
|
||||
Inside,
|
||||
Outside,
|
||||
XAxis
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.github.tehras.charts.bar.renderer.xaxis
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
class SimpleXAxisDrawer(
|
||||
private val axisLineThickness: Dp = 1.dp,
|
||||
private val axisLineColor: Color = Color.Black
|
||||
) : XAxisDrawer {
|
||||
private val axisLinePaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = axisLineColor
|
||||
style = PaintingStyle.Stroke
|
||||
}
|
||||
|
||||
override fun requiredHeight(drawScope: DrawScope): Float = with(drawScope) {
|
||||
(3f / 2f) * axisLineThickness.toPx()
|
||||
}
|
||||
|
||||
override fun drawAxisLine(drawScope: DrawScope, canvas: Canvas, drawableArea: Rect) =
|
||||
with(drawScope) {
|
||||
val lineThickness = axisLineThickness.toPx()
|
||||
val y = drawableArea.top + (lineThickness / 2f)
|
||||
|
||||
canvas.drawLine(
|
||||
p1 = Offset(
|
||||
x = drawableArea.left,
|
||||
y = y
|
||||
),
|
||||
p2 = Offset(
|
||||
x = drawableArea.right,
|
||||
y = y
|
||||
),
|
||||
paint = axisLinePaint.apply {
|
||||
strokeWidth = lineThickness
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.github.tehras.charts.bar.renderer.xaxis
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
interface XAxisDrawer {
|
||||
fun requiredHeight(drawScope: DrawScope): Float
|
||||
|
||||
fun drawAxisLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect
|
||||
)
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.github.tehras.charts.bar.renderer.yaxis
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.github.tehras.charts.piechart.utils.toLegacyInt
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
typealias LabelFormatter = (value: Float) -> String
|
||||
|
||||
class SimpleYAxisDrawer(
|
||||
private val labelTextSize: TextUnit = 12.sp,
|
||||
private val labelTextColor: Color = Color.Black,
|
||||
private val labelRatio: Int = 3,
|
||||
private val labelValueFormatter: LabelFormatter = { value -> "%.1f".format(value) },
|
||||
private val axisLineThickness: Dp = 1.dp,
|
||||
private val axisLineColor: Color = Color.Black
|
||||
) : YAxisDrawer {
|
||||
private val axisLinePaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = axisLineColor
|
||||
style = PaintingStyle.Stroke
|
||||
}
|
||||
private val textPaint = android.graphics.Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = labelTextColor.toLegacyInt()
|
||||
}
|
||||
private val textBounds = android.graphics.Rect()
|
||||
|
||||
override fun drawAxisLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect
|
||||
) = with(drawScope) {
|
||||
val lineThickness = axisLineThickness.toPx()
|
||||
val x = drawableArea.right - (lineThickness / 2f)
|
||||
|
||||
canvas.drawLine(
|
||||
p1 = Offset(
|
||||
x = x,
|
||||
y = drawableArea.top
|
||||
),
|
||||
p2 = Offset(
|
||||
x = x,
|
||||
y = drawableArea.bottom
|
||||
),
|
||||
paint = axisLinePaint.apply {
|
||||
strokeWidth = lineThickness
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun drawAxisLabels(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect,
|
||||
minValue: Float,
|
||||
maxValue: Float
|
||||
) = with(drawScope) {
|
||||
val labelPaint = textPaint.apply {
|
||||
textSize = labelTextSize.toPx()
|
||||
textAlign = android.graphics.Paint.Align.RIGHT
|
||||
}
|
||||
val minLabelHeight = (labelTextSize.toPx() * labelRatio.toFloat())
|
||||
val totalHeight = drawableArea.height
|
||||
val labelCount = max((drawableArea.height / minLabelHeight).roundToInt(), 2)
|
||||
|
||||
for (i in 0..labelCount) {
|
||||
val value = minValue + (i * ((maxValue - minValue) / labelCount))
|
||||
|
||||
val label = labelValueFormatter(value)
|
||||
val x = drawableArea.right - axisLineThickness.toPx() - (labelTextSize.toPx() / 2f)
|
||||
|
||||
labelPaint.getTextBounds(label, 0, label.length, textBounds)
|
||||
|
||||
val y =
|
||||
drawableArea.bottom - (i * (totalHeight / labelCount)) + (textBounds.height() / 2f)
|
||||
|
||||
canvas.nativeCanvas.drawText(label, x, y, labelPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.github.tehras.charts.bar.renderer.yaxis
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
interface YAxisDrawer {
|
||||
fun drawAxisLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect
|
||||
)
|
||||
|
||||
fun drawAxisLabels(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect,
|
||||
minValue: Float,
|
||||
maxValue: Float
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
apply(from = rootProject.file("gradle/configure-android.gradle"))
|
||||
apply(from = rootProject.file("gradle/configure-compose.gradle"))
|
||||
apply(from = rootProject.file("gradle/jitpack-publish.gradle"))
|
||||
|
||||
dependencies {
|
||||
implementation(Kotlin.stdLib)
|
||||
|
||||
implementation(Compose.animation)
|
||||
implementation(Compose.core)
|
||||
implementation(Compose.layout)
|
||||
implementation(Compose.foundation)
|
||||
implementation(Compose.runtime)
|
||||
debugImplementation(Compose.tooling)
|
||||
}
|
||||
android {
|
||||
namespace = "com.github.tehras.charts.common"
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.github.tehras.charts.piechart
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class AxisLine(
|
||||
val thickness: Dp = 1.5.dp,
|
||||
val color: Color = Color.Gray
|
||||
) {
|
||||
private val paint = Paint().apply {
|
||||
this.color = this@AxisLine.color
|
||||
this.style = PaintingStyle.Stroke
|
||||
}
|
||||
|
||||
fun paint(density: Density) = paint.apply {
|
||||
this.strokeWidth = thickness.value * density.density
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.github.tehras.charts.piechart.animation
|
||||
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
|
||||
fun simpleChartAnimation() = TweenSpec<Float>(durationMillis = 500)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.github.tehras.charts.piechart.utils
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
fun Color.toLegacyInt(): Int {
|
||||
return android.graphics.Color.argb(
|
||||
(alpha * 255.0f + 0.5f).toInt(),
|
||||
(red * 255.0f + 0.5f).toInt(),
|
||||
(green * 255.0f + 0.5f).toInt(),
|
||||
(blue * 255.0f + 0.5f).toInt()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,25 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
apply(from = rootProject.file("gradle/configure-android.gradle"))
|
||||
apply(from = rootProject.file("gradle/configure-compose.gradle"))
|
||||
apply(from = rootProject.file("gradle/jitpack-publish.gradle"))
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:common"))
|
||||
|
||||
implementation(Kotlin.stdLib)
|
||||
|
||||
implementation(Compose.animation)
|
||||
implementation(Compose.core)
|
||||
implementation(Compose.layout)
|
||||
implementation(Compose.foundation)
|
||||
implementation(Compose.runtime)
|
||||
debugImplementation(Compose.tooling)
|
||||
}
|
||||
android {
|
||||
namespace = "com.github.tehras.charts.line"
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.github.tehras.charts.line
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import com.github.tehras.charts.line.LineChartUtils.calculateDrawableArea
|
||||
import com.github.tehras.charts.line.LineChartUtils.calculateFillPath
|
||||
import com.github.tehras.charts.line.LineChartUtils.calculateLinePath
|
||||
import com.github.tehras.charts.line.LineChartUtils.calculatePointLocation
|
||||
import com.github.tehras.charts.line.LineChartUtils.calculateXAxisDrawableArea
|
||||
import com.github.tehras.charts.line.LineChartUtils.calculateXAxisLabelsDrawableArea
|
||||
import com.github.tehras.charts.line.LineChartUtils.calculateYAxisDrawableArea
|
||||
import com.github.tehras.charts.line.LineChartUtils.withProgress
|
||||
import com.github.tehras.charts.line.renderer.line.LineDrawer
|
||||
import com.github.tehras.charts.line.renderer.line.LineShader
|
||||
import com.github.tehras.charts.line.renderer.line.NoLineShader
|
||||
import com.github.tehras.charts.line.renderer.line.SolidLineDrawer
|
||||
import com.github.tehras.charts.line.renderer.point.FilledCircularPointDrawer
|
||||
import com.github.tehras.charts.line.renderer.point.PointDrawer
|
||||
import com.github.tehras.charts.line.renderer.xaxis.SimpleXAxisDrawer
|
||||
import com.github.tehras.charts.line.renderer.xaxis.XAxisDrawer
|
||||
import com.github.tehras.charts.line.renderer.yaxis.SimpleYAxisDrawer
|
||||
import com.github.tehras.charts.line.renderer.yaxis.YAxisDrawer
|
||||
import com.github.tehras.charts.piechart.animation.simpleChartAnimation
|
||||
|
||||
@Composable
|
||||
fun LineChart(
|
||||
modifier: Modifier = Modifier,
|
||||
linesChartData: List<LineChartData>,
|
||||
labels: List<String> = linesChartData.maxByOrNull { it.points.size }?.points?.map { it.label }
|
||||
?: emptyList(),
|
||||
animation: AnimationSpec<Float> = simpleChartAnimation(),
|
||||
pointDrawer: PointDrawer = FilledCircularPointDrawer(),
|
||||
lineShader: LineShader = NoLineShader,
|
||||
xAxisDrawer: XAxisDrawer = SimpleXAxisDrawer(),
|
||||
yAxisDrawer: YAxisDrawer = SimpleYAxisDrawer(),
|
||||
horizontalOffset: Float = 5f
|
||||
) {
|
||||
check(horizontalOffset in 0f..25f) {
|
||||
"Horizontal offset is the % offset from sides, " +
|
||||
"and should be between 0%-25%"
|
||||
}
|
||||
|
||||
val transitionAnimation = remember(linesChartData.forEach { it.points }) {
|
||||
mutableStateListOf(
|
||||
*linesChartData.map { Animatable(0f) }.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(linesChartData.forEach { it.points }) {
|
||||
repeat(linesChartData.size) { index ->
|
||||
transitionAnimation[index].snapTo(0f)
|
||||
transitionAnimation[index].animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = animation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Canvas(modifier = modifier.fillMaxSize()) {
|
||||
drawIntoCanvas { canvas ->
|
||||
|
||||
val yAxisDrawableArea = calculateYAxisDrawableArea(
|
||||
xAxisLabelSize = xAxisDrawer.requiredHeight(this),
|
||||
size = size
|
||||
)
|
||||
val xAxisDrawableArea = calculateXAxisDrawableArea(
|
||||
yAxisWidth = yAxisDrawableArea.width,
|
||||
labelHeight = xAxisDrawer.requiredHeight(this),
|
||||
size = size
|
||||
)
|
||||
val xAxisLabelsDrawableArea = calculateXAxisLabelsDrawableArea(
|
||||
xAxisDrawableArea = xAxisDrawableArea,
|
||||
offset = horizontalOffset
|
||||
)
|
||||
val chartDrawableArea = calculateDrawableArea(
|
||||
xAxisDrawableArea = xAxisDrawableArea,
|
||||
yAxisDrawableArea = yAxisDrawableArea,
|
||||
size = size,
|
||||
offset = horizontalOffset
|
||||
)
|
||||
|
||||
// Draw the X Axis line.
|
||||
xAxisDrawer.drawAxisLine(
|
||||
drawScope = this,
|
||||
drawableArea = xAxisDrawableArea,
|
||||
canvas = canvas
|
||||
)
|
||||
|
||||
xAxisDrawer.drawAxisLabels(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
drawableArea = xAxisLabelsDrawableArea,
|
||||
labels = labels
|
||||
)
|
||||
|
||||
// Draw the Y Axis line.
|
||||
yAxisDrawer.drawAxisLine(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
drawableArea = yAxisDrawableArea
|
||||
)
|
||||
|
||||
yAxisDrawer.drawAxisLabels(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
drawableArea = yAxisDrawableArea,
|
||||
minValue = linesChartData.minOf { it.minYValue },
|
||||
maxValue = linesChartData.maxOf { it.maxYValue }
|
||||
)
|
||||
|
||||
linesChartData.forEachIndexed { index, lineChartData ->
|
||||
drawLine(
|
||||
canvas = canvas,
|
||||
lineChartData = lineChartData,
|
||||
transitionAnimation = transitionAnimation[index],
|
||||
pointDrawer = pointDrawer,
|
||||
lineDrawer = lineChartData.lineDrawer,
|
||||
lineShader = lineShader,
|
||||
chartDrawableArea = chartDrawableArea
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawLine(
|
||||
pointDrawer: PointDrawer = FilledCircularPointDrawer(),
|
||||
lineDrawer: LineDrawer = SolidLineDrawer(),
|
||||
lineShader: LineShader = NoLineShader,
|
||||
canvas: Canvas,
|
||||
transitionAnimation: Animatable<Float, AnimationVector1D>,
|
||||
lineChartData: LineChartData,
|
||||
chartDrawableArea: Rect
|
||||
) {
|
||||
|
||||
// Draw the chart line.
|
||||
lineDrawer.drawLine(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
linePath = calculateLinePath(
|
||||
drawableArea = chartDrawableArea,
|
||||
lineChartData = lineChartData,
|
||||
transitionProgress = transitionAnimation.value
|
||||
)
|
||||
)
|
||||
|
||||
lineShader.fillLine(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
fillPath = calculateFillPath(
|
||||
drawableArea = chartDrawableArea,
|
||||
lineChartData = lineChartData,
|
||||
transitionProgress = transitionAnimation.value
|
||||
)
|
||||
)
|
||||
|
||||
lineChartData.points.forEachIndexed { index, point ->
|
||||
withProgress(
|
||||
index = index,
|
||||
lineChartData = lineChartData,
|
||||
transitionProgress = transitionAnimation.value
|
||||
) {
|
||||
pointDrawer.drawPoint(
|
||||
drawScope = this,
|
||||
canvas = canvas,
|
||||
center = calculatePointLocation(
|
||||
drawableArea = chartDrawableArea,
|
||||
lineChartData = lineChartData,
|
||||
point = point,
|
||||
index = index
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.github.tehras.charts.line
|
||||
|
||||
import com.github.tehras.charts.line.renderer.line.LineDrawer
|
||||
|
||||
data class LineChartData(
|
||||
val points: List<Point>,
|
||||
/** This is percentage we pad yValue by.**/
|
||||
val padBy: Float = 20f,
|
||||
val startAtZero: Boolean = false,
|
||||
val lineDrawer: LineDrawer,
|
||||
) {
|
||||
init {
|
||||
require(padBy in 0f..100f)
|
||||
}
|
||||
|
||||
private val yMinMax: Pair<Float, Float>
|
||||
get() {
|
||||
val min = points.minByOrNull { it.value }?.value ?: 0f
|
||||
val max = points.maxByOrNull { it.value }?.value ?: 0f
|
||||
|
||||
return min to max
|
||||
}
|
||||
|
||||
internal val maxYValue: Float =
|
||||
yMinMax.second + ((yMinMax.second - yMinMax.first) * padBy / 100f)
|
||||
internal val minYValue: Float
|
||||
get() {
|
||||
return if (startAtZero) {
|
||||
0f
|
||||
} else {
|
||||
yMinMax.first - ((yMinMax.second - yMinMax.first) * padBy / 100f)
|
||||
}
|
||||
}
|
||||
internal val yRange = maxYValue - minYValue
|
||||
|
||||
data class Point(val value: Float, val label: String)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.github.tehras.charts.line
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.geometry.isSpecified
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object LineChartUtils {
|
||||
fun calculateDrawableArea(
|
||||
xAxisDrawableArea: Rect,
|
||||
yAxisDrawableArea: Rect,
|
||||
size: Size,
|
||||
offset: Float
|
||||
): Rect {
|
||||
val horizontalOffset = xAxisDrawableArea.width * offset / 100f
|
||||
|
||||
return Rect(
|
||||
left = yAxisDrawableArea.right + horizontalOffset,
|
||||
top = 0f,
|
||||
bottom = xAxisDrawableArea.top,
|
||||
right = size.width - horizontalOffset
|
||||
)
|
||||
}
|
||||
|
||||
fun calculateXAxisDrawableArea(
|
||||
yAxisWidth: Float,
|
||||
labelHeight: Float,
|
||||
size: Size
|
||||
): Rect {
|
||||
val top = size.height - labelHeight
|
||||
|
||||
return Rect(
|
||||
left = yAxisWidth,
|
||||
top = top,
|
||||
bottom = size.height,
|
||||
right = size.width
|
||||
)
|
||||
}
|
||||
|
||||
fun calculateXAxisLabelsDrawableArea(
|
||||
xAxisDrawableArea: Rect,
|
||||
offset: Float
|
||||
): Rect {
|
||||
val horizontalOffset = xAxisDrawableArea.width * offset / 100f
|
||||
|
||||
return Rect(
|
||||
left = xAxisDrawableArea.left + horizontalOffset,
|
||||
top = xAxisDrawableArea.top,
|
||||
bottom = xAxisDrawableArea.bottom,
|
||||
right = xAxisDrawableArea.right - horizontalOffset
|
||||
)
|
||||
}
|
||||
|
||||
fun Density.calculateYAxisDrawableArea(
|
||||
xAxisLabelSize: Float,
|
||||
size: Size
|
||||
): Rect {
|
||||
// Either 50dp or 10% of the chart width.
|
||||
val right = minOf(50.dp.toPx(), size.width * 10f / 100f)
|
||||
|
||||
return Rect(
|
||||
left = 0f,
|
||||
top = 0f,
|
||||
bottom = size.height - xAxisLabelSize,
|
||||
right = right
|
||||
)
|
||||
}
|
||||
|
||||
fun calculatePointLocation(
|
||||
drawableArea: Rect,
|
||||
lineChartData: LineChartData,
|
||||
point: LineChartData.Point,
|
||||
index: Int
|
||||
): Offset {
|
||||
val x = (index.toFloat() / (lineChartData.points.size - 1))
|
||||
val y = ((point.value - lineChartData.minYValue) / lineChartData.yRange)
|
||||
|
||||
return Offset(
|
||||
x = (x * drawableArea.width) + drawableArea.left,
|
||||
y = drawableArea.height - (y * drawableArea.height)
|
||||
)
|
||||
}
|
||||
|
||||
fun withProgress(
|
||||
index: Int,
|
||||
lineChartData: LineChartData,
|
||||
transitionProgress: Float,
|
||||
showWithProgress: (progress: Float) -> Unit
|
||||
) {
|
||||
val size = lineChartData.points.size
|
||||
val toIndex = (size * transitionProgress).toInt() + 1
|
||||
|
||||
if (index == toIndex) {
|
||||
// Get the left over.
|
||||
val sizeF = lineChartData.points.size.toFloat()
|
||||
val perIndex = (1f / sizeF)
|
||||
val down = (index - 1) * perIndex
|
||||
|
||||
showWithProgress((transitionProgress - down) / perIndex)
|
||||
} else if (index < toIndex) {
|
||||
showWithProgress(1f)
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateLinePath(
|
||||
drawableArea: Rect,
|
||||
lineChartData: LineChartData,
|
||||
transitionProgress: Float
|
||||
): Path = Path().apply {
|
||||
var prevPointLocation: Offset? = null
|
||||
lineChartData.points.forEachIndexed { index, point ->
|
||||
withProgress(
|
||||
index = index,
|
||||
transitionProgress = transitionProgress,
|
||||
lineChartData = lineChartData
|
||||
) { progress ->
|
||||
val pointLocation = calculatePointLocation(
|
||||
drawableArea = drawableArea,
|
||||
lineChartData = lineChartData,
|
||||
point = point,
|
||||
index = index
|
||||
)
|
||||
|
||||
if (pointLocation.isSpecified) {
|
||||
if (index == 0) {
|
||||
moveTo(pointLocation.x, pointLocation.y)
|
||||
} else {
|
||||
if (progress <= 1f) {
|
||||
// We have to change the `dy` based on the progress
|
||||
val prevX = prevPointLocation!!.x
|
||||
val prevY = prevPointLocation!!.y
|
||||
|
||||
val x = (pointLocation.x - prevX) * progress + prevX
|
||||
val y = (pointLocation.y - prevY) * progress + prevY
|
||||
|
||||
lineTo(x, y)
|
||||
} else {
|
||||
lineTo(pointLocation.x, pointLocation.y)
|
||||
}
|
||||
}
|
||||
|
||||
prevPointLocation = pointLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateFillPath(drawableArea: Rect,
|
||||
lineChartData: LineChartData,
|
||||
transitionProgress: Float
|
||||
): Path = Path().apply {
|
||||
|
||||
// we start from the bottom left
|
||||
moveTo(drawableArea.left, drawableArea.bottom)
|
||||
var prevPointX : Float? = null
|
||||
var prevPointLocation: Offset? = null
|
||||
lineChartData.points.forEachIndexed { index, point ->
|
||||
withProgress(
|
||||
index = index,
|
||||
transitionProgress = transitionProgress,
|
||||
lineChartData = lineChartData
|
||||
) { progress ->
|
||||
val pointLocation = calculatePointLocation(
|
||||
drawableArea = drawableArea,
|
||||
lineChartData = lineChartData,
|
||||
point = point,
|
||||
index = index
|
||||
)
|
||||
|
||||
if (pointLocation.isSpecified) {
|
||||
if (index == 0) {
|
||||
lineTo(drawableArea.left, pointLocation.y)
|
||||
lineTo(pointLocation.x, pointLocation.y)
|
||||
} else {
|
||||
if (progress <= 1f) {
|
||||
// We have to change the `dy` based on the progress
|
||||
val prevX = prevPointLocation!!.x
|
||||
val prevY = prevPointLocation!!.y
|
||||
|
||||
val x = (pointLocation.x - prevX) * progress + prevX
|
||||
val y = (pointLocation.y - prevY) * progress + prevY
|
||||
|
||||
lineTo(x, y)
|
||||
|
||||
prevPointX = x
|
||||
} else {
|
||||
lineTo(pointLocation.x, pointLocation.y)
|
||||
prevPointX = pointLocation.x
|
||||
}
|
||||
}
|
||||
|
||||
prevPointLocation = pointLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
// We need to connect the line to the end of the drawable area
|
||||
prevPointX?.let { x->
|
||||
lineTo(x, drawableArea.bottom)
|
||||
lineTo(drawableArea.right, drawableArea.bottom)
|
||||
} ?: lineTo(drawableArea.left, drawableArea.bottom)
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.github.tehras.charts.line.renderer.line
|
||||
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
class GradientLineShader(colors: List<Color> = listOf(Color.Blue, Color.Transparent)) : LineShader {
|
||||
private val brush = Brush.verticalGradient(colors)
|
||||
override fun fillLine(drawScope: DrawScope, canvas: Canvas, fillPath: Path) {
|
||||
drawScope.drawPath(
|
||||
path = fillPath,
|
||||
brush = brush
|
||||
)
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.github.tehras.charts.line.renderer.line
|
||||
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
interface LineDrawer {
|
||||
fun drawLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
linePath: Path
|
||||
)
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.github.tehras.charts.line.renderer.line
|
||||
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
interface LineShader {
|
||||
fun fillLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
fillPath: Path
|
||||
)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.github.tehras.charts.line.renderer.line
|
||||
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
object NoLineShader : LineShader {
|
||||
override fun fillLine(drawScope: DrawScope, canvas: Canvas, fillPath: Path) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.github.tehras.charts.line.renderer.line
|
||||
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class SolidLineDrawer(
|
||||
val thickness: Dp = 3.dp,
|
||||
val color: Color = Color.Cyan
|
||||
) : LineDrawer {
|
||||
private val paint = Paint().apply {
|
||||
this.color = this@SolidLineDrawer.color
|
||||
this.style = PaintingStyle.Stroke
|
||||
this.isAntiAlias = true
|
||||
}
|
||||
|
||||
override fun drawLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
linePath: Path
|
||||
) {
|
||||
val lineThickness = with(drawScope) {
|
||||
thickness.toPx()
|
||||
}
|
||||
|
||||
canvas.drawPath(
|
||||
path = linePath,
|
||||
paint = paint.apply {
|
||||
strokeWidth = lineThickness
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.github.tehras.charts.line.renderer.line
|
||||
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
class SolidLineShader(val color: Color = Color.Blue) : LineShader {
|
||||
override fun fillLine(drawScope: DrawScope, canvas: Canvas, fillPath: Path) {
|
||||
drawScope.drawPath(
|
||||
path = fillPath,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.github.tehras.charts.line.renderer.point
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class FilledCircularPointDrawer(
|
||||
val diameter: Dp = 8.dp,
|
||||
val color: Color = Color.Blue
|
||||
) : PointDrawer {
|
||||
|
||||
private val paint = Paint().apply {
|
||||
color = this@FilledCircularPointDrawer.color
|
||||
style = PaintingStyle.Fill
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
override fun drawPoint(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
center: Offset
|
||||
) {
|
||||
with(drawScope as Density) {
|
||||
canvas.drawCircle(center, diameter.toPx() / 2f, paint)
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.github.tehras.charts.line.renderer.point
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class HollowCircularPointDrawer(
|
||||
val diameter: Dp = 8.dp,
|
||||
val lineThickness: Dp = 2.dp,
|
||||
val color: Color = Color.Blue
|
||||
) : PointDrawer {
|
||||
|
||||
private val paint = Paint().apply {
|
||||
color = this@HollowCircularPointDrawer.color
|
||||
style = PaintingStyle.Stroke
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
override fun drawPoint(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
center: Offset
|
||||
) {
|
||||
with(drawScope as Density) {
|
||||
canvas.drawCircle(
|
||||
center = center,
|
||||
radius = diameter.toPx() / 2f,
|
||||
paint = paint.apply {
|
||||
strokeWidth = lineThickness.toPx()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.github.tehras.charts.line.renderer.point
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
object NoPointDrawer : PointDrawer {
|
||||
override fun drawPoint(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
center: Offset
|
||||
) {
|
||||
// Leave empty on purpose, we do not want to draw anything.
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.github.tehras.charts.line.renderer.point
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
interface PointDrawer {
|
||||
fun drawPoint(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
center: Offset
|
||||
)
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.github.tehras.charts.line.renderer.xaxis
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.github.tehras.charts.piechart.utils.toLegacyInt
|
||||
|
||||
class SimpleXAxisDrawer(
|
||||
private val labelTextSize: TextUnit = 12.sp,
|
||||
private val labelTextColor: Color = Color.Black,
|
||||
/** 1 means we draw everything. 2 means we draw every other, and so on. */
|
||||
private val labelRatio: Int = 1,
|
||||
private val axisLineThickness: Dp = 1.dp,
|
||||
private val axisLineColor: Color = Color.Black
|
||||
) : XAxisDrawer {
|
||||
private val axisLinePaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = axisLineColor
|
||||
style = PaintingStyle.Stroke
|
||||
}
|
||||
|
||||
private val textPaint = android.graphics.Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = labelTextColor.toLegacyInt()
|
||||
}
|
||||
|
||||
override fun requiredHeight(drawScope: DrawScope): Float {
|
||||
return with(drawScope) {
|
||||
(3f / 2f) * (labelTextSize.toPx() + axisLineThickness.toPx())
|
||||
}
|
||||
}
|
||||
|
||||
override fun drawAxisLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect
|
||||
) {
|
||||
with(drawScope) {
|
||||
val lineThickness = axisLineThickness.toPx()
|
||||
val y = drawableArea.top + (lineThickness / 2f)
|
||||
|
||||
canvas.drawLine(
|
||||
p1 = Offset(
|
||||
x = drawableArea.left,
|
||||
y = y
|
||||
),
|
||||
p2 = Offset(
|
||||
x = drawableArea.right,
|
||||
y = y
|
||||
),
|
||||
paint = axisLinePaint.apply {
|
||||
strokeWidth = lineThickness
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun drawAxisLabels(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect,
|
||||
labels: List<String>
|
||||
) {
|
||||
with(drawScope) {
|
||||
val labelPaint = textPaint.apply {
|
||||
textSize = labelTextSize.toPx()
|
||||
textAlign = android.graphics.Paint.Align.CENTER
|
||||
}
|
||||
|
||||
val labelIncrements = drawableArea.width / (labels.size - 1)
|
||||
labels.forEachIndexed { index, label ->
|
||||
if (index.rem(labelRatio) == 0) {
|
||||
val x = drawableArea.left + (labelIncrements * (index))
|
||||
val y = drawableArea.bottom
|
||||
|
||||
canvas.nativeCanvas.drawText(label, x, y, labelPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.github.tehras.charts.line.renderer.xaxis
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
interface XAxisDrawer {
|
||||
fun requiredHeight(drawScope: DrawScope): Float
|
||||
|
||||
fun drawAxisLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect
|
||||
)
|
||||
|
||||
fun drawAxisLabels(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect,
|
||||
labels: List<String>
|
||||
)
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.github.tehras.charts.line.renderer.yaxis
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.github.tehras.charts.piechart.utils.toLegacyInt
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
typealias LabelFormatter = (value: Float) -> String
|
||||
|
||||
class SimpleYAxisDrawer(
|
||||
private val labelTextSize: TextUnit = 12.sp,
|
||||
private val labelTextColor: Color = Color.Black,
|
||||
private val labelRatio: Int = 3,
|
||||
private val labelValueFormatter: LabelFormatter = { value -> "%.1f".format(value) },
|
||||
private val axisLineThickness: Dp = 1.dp,
|
||||
private val axisLineColor: Color = Color.Black
|
||||
) : YAxisDrawer {
|
||||
private val axisLinePaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = axisLineColor
|
||||
style = PaintingStyle.Stroke
|
||||
}
|
||||
private val textPaint = android.graphics.Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = labelTextColor.toLegacyInt()
|
||||
}
|
||||
private val textBounds = android.graphics.Rect()
|
||||
|
||||
override fun drawAxisLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect
|
||||
) = with(drawScope) {
|
||||
val lineThickness = axisLineThickness.toPx()
|
||||
val x = drawableArea.right - (lineThickness / 2f)
|
||||
|
||||
canvas.drawLine(
|
||||
p1 = Offset(
|
||||
x = x,
|
||||
y = drawableArea.top
|
||||
),
|
||||
p2 = Offset(
|
||||
x = x,
|
||||
y = drawableArea.bottom
|
||||
),
|
||||
paint = axisLinePaint.apply {
|
||||
strokeWidth = lineThickness
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun drawAxisLabels(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect,
|
||||
minValue: Float,
|
||||
maxValue: Float
|
||||
) = with(drawScope) {
|
||||
val labelPaint = textPaint.apply {
|
||||
textSize = labelTextSize.toPx()
|
||||
textAlign = android.graphics.Paint.Align.RIGHT
|
||||
}
|
||||
val minLabelHeight = (labelTextSize.toPx() * labelRatio.toFloat())
|
||||
val totalHeight = drawableArea.height
|
||||
val labelCount = max((drawableArea.height / minLabelHeight).roundToInt(), 2)
|
||||
|
||||
for (i in 0..labelCount) {
|
||||
val value = minValue + (i * ((maxValue - minValue) / labelCount))
|
||||
|
||||
val label = labelValueFormatter(value)
|
||||
val x =
|
||||
drawableArea.right - axisLineThickness.toPx() - (labelTextSize.toPx() / 2f)
|
||||
|
||||
labelPaint.getTextBounds(label, 0, label.length, textBounds)
|
||||
|
||||
val y =
|
||||
drawableArea.bottom - (i * (totalHeight / labelCount)) + (textBounds.height() / 2f)
|
||||
|
||||
canvas.nativeCanvas.drawText(label, x, y, labelPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.github.tehras.charts.line.renderer.yaxis
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
|
||||
interface YAxisDrawer {
|
||||
fun drawAxisLine(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect
|
||||
)
|
||||
|
||||
fun drawAxisLabels(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
drawableArea: Rect,
|
||||
minValue: Float,
|
||||
maxValue: Float
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,27 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
apply(from = rootProject.file("gradle/configure-android.gradle"))
|
||||
apply(from = rootProject.file("gradle/configure-compose.gradle"))
|
||||
apply(from = rootProject.file("gradle/jitpack-publish.gradle"))
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:common"))
|
||||
|
||||
implementation(Kotlin.stdLib)
|
||||
|
||||
implementation(Compose.animation)
|
||||
implementation(Compose.core)
|
||||
implementation(Compose.layout)
|
||||
implementation(Compose.foundation)
|
||||
implementation(Compose.runtime)
|
||||
|
||||
// Previews weren't working when using debugImplementation
|
||||
implementation(Compose.tooling)
|
||||
}
|
||||
android {
|
||||
namespace = "com.github.tehras.charts.piechart"
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.github.tehras.charts.piechart
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.github.tehras.charts.piechart.PieChartUtils.calculateAngle
|
||||
import com.github.tehras.charts.piechart.animation.simpleChartAnimation
|
||||
import com.github.tehras.charts.piechart.renderer.SimpleSliceDrawer
|
||||
import com.github.tehras.charts.piechart.renderer.SliceDrawer
|
||||
|
||||
@Composable
|
||||
fun PieChart(
|
||||
pieChartData: PieChartData,
|
||||
modifier: Modifier = Modifier,
|
||||
animation: AnimationSpec<Float> = simpleChartAnimation(),
|
||||
sliceDrawer: SliceDrawer = SimpleSliceDrawer()
|
||||
) {
|
||||
val transitionProgress = remember(pieChartData.slices) { Animatable(initialValue = 0f) }
|
||||
|
||||
// When slices value changes we want to re-animated the chart.
|
||||
LaunchedEffect(pieChartData.slices) {
|
||||
transitionProgress.animateTo(1f, animationSpec = animation)
|
||||
}
|
||||
|
||||
DrawChart(
|
||||
pieChartData = pieChartData,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
progress = transitionProgress.value,
|
||||
sliceDrawer = sliceDrawer
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawChart(
|
||||
pieChartData: PieChartData,
|
||||
modifier: Modifier,
|
||||
progress: Float,
|
||||
sliceDrawer: SliceDrawer
|
||||
) {
|
||||
val slices = pieChartData.slices
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
drawIntoCanvas {
|
||||
var startArc = 0f
|
||||
|
||||
slices.forEach { slice ->
|
||||
val arc = calculateAngle(
|
||||
sliceLength = slice.value,
|
||||
totalLength = pieChartData.totalSize,
|
||||
progress = progress
|
||||
)
|
||||
|
||||
sliceDrawer.drawSlice(
|
||||
drawScope = this,
|
||||
canvas = drawContext.canvas,
|
||||
area = size,
|
||||
startAngle = startArc,
|
||||
sweepAngle = arc,
|
||||
slice = slice
|
||||
)
|
||||
|
||||
startArc += arc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PieChartPreview() = PieChart(
|
||||
pieChartData = PieChartData(
|
||||
slices = listOf(
|
||||
PieChartData.Slice(25f, Color.Red),
|
||||
PieChartData.Slice(42f, Color.Blue),
|
||||
PieChartData.Slice(23f, Color.Green)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.github.tehras.charts.piechart
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
data class PieChartData(
|
||||
val slices: List<Slice>
|
||||
) {
|
||||
internal val totalSize: Float
|
||||
get() {
|
||||
var total = 0f
|
||||
slices.forEach { total += it.value }
|
||||
return total
|
||||
}
|
||||
|
||||
data class Slice(
|
||||
val value: Float,
|
||||
val color: Color
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.github.tehras.charts.piechart
|
||||
|
||||
internal object PieChartUtils {
|
||||
fun calculateAngle(
|
||||
sliceLength: Float,
|
||||
totalLength: Float,
|
||||
progress: Float
|
||||
): Float {
|
||||
return 360.0f * (sliceLength * progress) / totalLength
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.github.tehras.charts.piechart.renderer
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import com.github.tehras.charts.piechart.PieChartData.Slice
|
||||
|
||||
class SimpleSliceDrawer(private val sliceThickness: Float = 25f) : SliceDrawer {
|
||||
init {
|
||||
require(sliceThickness in 10f..100f) {
|
||||
"Thickness of $sliceThickness must be between 10-100"
|
||||
}
|
||||
}
|
||||
|
||||
private val sectionPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
style = PaintingStyle.Stroke
|
||||
}
|
||||
|
||||
override fun drawSlice(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
area: Size,
|
||||
startAngle: Float,
|
||||
sweepAngle: Float,
|
||||
slice: Slice
|
||||
) {
|
||||
val sliceThickness = calculateSectorThickness(area = area)
|
||||
val drawableArea = calculateDrawableArea(area = area)
|
||||
|
||||
canvas.drawArc(
|
||||
rect = drawableArea,
|
||||
paint = sectionPaint.apply {
|
||||
color = slice.color
|
||||
strokeWidth = sliceThickness
|
||||
},
|
||||
startAngle = startAngle,
|
||||
sweepAngle = sweepAngle,
|
||||
useCenter = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun calculateSectorThickness(area: Size): Float {
|
||||
val minSize = minOf(area.width, area.height)
|
||||
|
||||
return minSize * (sliceThickness / 200f)
|
||||
}
|
||||
|
||||
private fun calculateDrawableArea(area: Size): Rect {
|
||||
val sliceThicknessOffset =
|
||||
calculateSectorThickness(area = area) / 2f
|
||||
val offsetHorizontally = (area.width - area.height) / 2f
|
||||
|
||||
return Rect(
|
||||
left = sliceThicknessOffset + offsetHorizontally,
|
||||
top = sliceThicknessOffset,
|
||||
right = area.width - sliceThicknessOffset - offsetHorizontally,
|
||||
bottom = area.height - sliceThicknessOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.github.tehras.charts.piechart.renderer
|
||||
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import com.github.tehras.charts.piechart.PieChartData.Slice
|
||||
|
||||
interface SliceDrawer {
|
||||
fun drawSlice(
|
||||
drawScope: DrawScope,
|
||||
canvas: Canvas,
|
||||
area: Size,
|
||||
startAngle: Float,
|
||||
sweepAngle: Float,
|
||||
slice: Slice
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user