的
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user