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
+25
View File
@@ -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
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.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)
}
}
@@ -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
)
}
}
@@ -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
)
}
@@ -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
)
}
@@ -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
}
}
@@ -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
}
)
}
}
@@ -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
)
}
}
@@ -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)
}
}
}
@@ -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()
}
)
}
}
}
@@ -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.
}
}
@@ -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
)
}
@@ -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)
}
}
}
}
}
@@ -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>
)
}
@@ -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)
}
}
}
@@ -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
)
}