feat: Origin

This commit is contained in:
2023-01-10 12:45:36 +01:00
commit 4d787d9393
91 changed files with 3371 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest></manifest>

View File

@ -0,0 +1,66 @@
package com.dzeio.charts
import kotlin.math.abs
import kotlin.math.max
data class Animation(
/**
* Enable / Disable the Chart Animations
*/
var enabled: Boolean = true,
/**
* Number of milliseconds the animation is running before it ends
*/
var duration: Int = 1000,
/**
* Number of updates per seconds
*/
var refreshRate: Int = 50
) {
/**
* Update the value depending on the maximum obtainable value
*
* @param maxValue the maximum value the item can obtain
* @param targetValue the value you want to obtain at the end of the animation
* @param currentValue the current value
*
* @return the new updated value
*/
fun updateValue(
maxValue: Float,
targetValue: Float,
currentValue: Float,
minValue: Float,
minStep: Float
): Float {
if (!enabled) {
return targetValue
}
if (currentValue < minValue) {
return minValue
}
val moveValue = max(minStep, (maxValue - targetValue) / refreshRate)
var result = targetValue
if (currentValue < targetValue) {
result = currentValue + moveValue
} else if (currentValue > targetValue) {
result = currentValue - moveValue
}
if (
abs(targetValue - currentValue) <= moveValue ||
result < minValue ||
result > maxValue
) {
return targetValue
}
return result
}
fun getDelay() = this.duration / this.refreshRate
}

View File

@ -0,0 +1,150 @@
package com.dzeio.charts
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.dzeio.charts.axis.XAxis
import com.dzeio.charts.axis.YAxis
import com.dzeio.charts.components.ChartScroll
import com.dzeio.charts.series.SerieInterface
class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs), ChartViewInterface {
private companion object {
const val TAG = "Charts/ChartView"
}
override var debug: Boolean = false
override val xAxis = XAxis(this)
override val yAxis = YAxis(this)
override var series: ArrayList<SerieInterface> = arrayListOf()
override var padding: Float = 8f
private val scroller = ChartScroll(this).apply {
var lastMovement = 0.0
setOnChartMoved { movementX, _ ->
xAxis.x += (movementX - lastMovement) * xAxis.getDataWidth() / width
lastMovement = movementX.toDouble()
refresh()
}
// setOnZoomChanged {
// Log.d(TAG, "New Zoom: $it")
// zoom = (it * 1.2).toFloat()
// refresh()
// }
}
// val animator: Runnable = object : Runnable {
// override fun run() {
// var needNewFrame = false
// for (serie in series) {
// val result = serie.onUpdate()
// if (result) {
// needNewFrame = true
// }
// }
// if (needNewFrame) {
// postDelayed(this, animation.getDelay().toLong())
// }
// invalidate()
// }
// }
// rect used for calculations
private val rect = RectF()
// stroke used while in debug
private val debugStrokePaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 8f
color = Color.parseColor("#654321")
}
override fun refresh() {
// run Axis logics
xAxis.refresh()
yAxis.refresh()
// run series logic
for (serie in series) {
serie.refresh()
}
// invalidate the view
invalidate()
// removeCallbacks(animator)
// post(animator)
}
override fun onDraw(canvas: Canvas) {
// don't draw anything if everything is empty
if (series.isEmpty() || series.maxOf { it.entries.size } == 0) {
super.onDraw(canvas)
return
}
if (debug) {
// draw corners
canvas.drawRect(rect.apply {
set(
padding / 2,
padding / 2,
width.toFloat() - padding / 2,
height.toFloat() - padding / 2
)
}, debugStrokePaint)
}
val bottom = xAxis.onDraw(canvas, rect.apply {
set(padding, 0f, width.toFloat() - padding, height.toFloat() - padding)
})
// right distance from the yAxis
val rightDistance = yAxis.onDraw(canvas, rect.apply {
set(padding, padding, width.toFloat() - padding, height.toFloat() - bottom - padding)
})
// chart draw rectangle
rect.apply {
set(
padding,
padding,
width.toFloat() - padding - rightDistance,
height - bottom - padding
)
}
for (serie in series) {
serie.onDraw(canvas, rect)
}
super.onDraw(canvas)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
performClick()
return scroller.onTouchEvent(event)
}
override fun getDataset(): ArrayList<Entry> {
val data: ArrayList<Entry> = arrayListOf()
for (serie in series) {
data.addAll(serie.entries)
}
data.sortBy { it.x }
return data.filterIndexed { index, entry -> data.indexOf(entry) == index } as ArrayList<Entry>
}
}

View File

@ -0,0 +1,47 @@
package com.dzeio.charts
import com.dzeio.charts.axis.XAxisInterface
import com.dzeio.charts.axis.YAxisInterface
import com.dzeio.charts.series.SerieInterface
interface ChartViewInterface {
/**
* Make the whole view in debug mode
*
* add debug texts, logs, and more
*/
var debug: Boolean
/**
* the padding inside the view
*/
var padding: Float
/**
* Hold metadata about the X axis
*/
val xAxis: XAxisInterface
/**
* Hold informations about the Y axis
*/
val yAxis: YAxisInterface
/**
* handle the series
*/
var series: ArrayList<SerieInterface>
/**
* refresh and run pre-display logic the chart
*
* this function should be run if you change parameters in the view
*/
fun refresh()
/**
* @return the whole dataset (sorted and cleaned up of dupps)
*/
fun getDataset(): ArrayList<Entry>
}

View File

@ -0,0 +1,10 @@
package com.dzeio.charts
/**
* A Base entry for any charts
*/
data class Entry(
var x: Double,
var y: Float,
var color: Int? = null
)

View File

@ -0,0 +1,140 @@
package com.dzeio.charts.axis
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.util.Log
import com.dzeio.charts.ChartViewInterface
import com.dzeio.charts.Entry
import kotlin.math.roundToInt
class XAxis(
private val view: ChartViewInterface
) : XAxisInterface {
private companion object {
const val TAG = "Charts/XAxis"
}
override var x: Double = 0.0
set(value) {
val max = getXMax() - getDataWidth()
val min = getXMin()
if (value > max && min <= max) {
field = max
return
}
if (value < min) {
field = min
return
}
field = value
}
override var enabled = true
override var dataWidth: Double? = null
get() = field ?: getXMax()
override var labelCount: Int = 2
var spacing = 16.0
override val textPaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#FC496D")
textSize = 30f
textAlign = Paint.Align.LEFT
}
private val rect = Rect()
override fun getPositionOnRect(entry: Entry, drawableSpace: RectF): Double {
return translatePositionToRect(entry.x, drawableSpace)
}
fun translatePositionToRect(value: Double, drawableSpace: RectF): Double {
return drawableSpace.width() * (value - x) / getDataWidth()
}
override fun getXMax(): Double {
return view.series.maxOf { serie ->
if (serie.entries.isEmpty()) {
return 0.0
}
serie.entries.maxOf { entry -> entry.x }
}
}
override fun getXMin(): Double {
return view.series.minOf { serie ->
if (serie.entries.isEmpty()) {
return 0.0
}
serie.entries.minOf { entry -> entry.x }
}
}
var onValueFormat: (value: Double) -> String = { it -> it.roundToInt().toString() }
override fun onDraw(canvas: Canvas, space: RectF): Float {
if (!enabled) {
return 0f
}
var maxHeight = 0f
val graphIncrement = space.width() / (labelCount - 1)
val valueIncrement = (getDataWidth() / (labelCount - 1)).toDouble()
for (index in 0 until labelCount) {
val text = onValueFormat(x + valueIncrement * index)
textPaint.getTextBounds(text, 0, text.length, rect)
maxHeight = maxHeight.coerceAtLeast(rect.height().toFloat() + 1)
var xPos = space.left + graphIncrement * index
if (xPos + rect.width() > space.right) {
xPos = space.right - rect.width()
}
canvas.drawText(
text,
xPos,
space.bottom,
textPaint
)
}
return maxHeight + 32f
}
override fun refresh() {
// TODO("Not yet implemented")
}
override fun getEntryWidth(drawableSpace: RectF): Double {
var smallest = Double.MAX_VALUE
val dataset = view.getDataset()
for (idx in 0 until dataset.size - 1) {
val distance = dataset[idx + 1].x - dataset[idx].x
if (smallest > distance && distance > 0.0) {
smallest = distance
}
}
return clamp(drawableSpace.width() * smallest / getDataWidth() - spacing, 1.0, drawableSpace.width().toDouble())
}
override fun getDataWidth(): Double {
// TODO: handle the auto dataWidth better
return dataWidth ?: getXMax()
}
private fun clamp(value: Double, min: Double, max: Double): Double {
return if (value < min) min else if (value > max) max else value
}
}

View File

@ -0,0 +1,84 @@
package com.dzeio.charts.axis
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import com.dzeio.charts.Entry
sealed interface XAxisInterface {
/**
* enable/disable the display of the xAxis
*/
var enabled: Boolean
/**
* set X position
*/
var x: Double
/**
* the "width" of the graph
*
* if not set it will be `XMax - XMin`
*
* ex: to display a 7 days graph history with x values being timestamp in secs, use 7*24*60*60
*/
var dataWidth: Double?
/**
* text Paint
*/
val textPaint: Paint
/**
* indicate the number of labels displayed
*/
var labelCount: Int
/**
* run when manually refreshing the system
*
* this is where the pre-logic is handled to make [onDraw] quicker
*/
fun refresh()
/**
* get the entry position on the rect
*
* @return the left side of the position of the entry
*/
fun getPositionOnRect(entry: Entry, drawableSpace: RectF): Double
/**
* get the maximum the X can get to
*/
fun getXMax(): Double
/**
* get the minimum the X can get to
*/
fun getXMin(): Double
/**
* get the size of an entry in the graph
*
* @return the size in [drawableSpace] px
*/
fun getEntryWidth(drawableSpace: RectF): Double
/**
* return the currently used dataWidth
*/
fun getDataWidth(): Double
/**
* onDraw event that will draw the XAxis
*
* @param canvas the canvas to draw on
* @param space the space where it is allowed to draw
*
* @return the final height of the XAxis
*/
fun onDraw(canvas: Canvas, space: RectF): Float
}

View File

@ -0,0 +1,146 @@
package com.dzeio.charts.axis
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import com.dzeio.charts.ChartViewInterface
import com.dzeio.charts.utils.drawDottedLine
import kotlin.math.roundToInt
class YAxis(
private val view: ChartViewInterface
) : YAxisInterface {
override var enabled = true
override val textLabel = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#FC496D")
textSize = 30f
textAlign = Paint.Align.LEFT
}
override val linePaint = Paint().apply {
isAntiAlias = true
color = Color.BLUE
}
override val goalLinePaint = Paint().apply {
isAntiAlias = true
color = Color.RED
strokeWidth = 4f
}
var onValueFormat: (value: Float) -> String = { it -> it.roundToInt().toString() }
override var labelCount = 5
private var min: Float? = 0f
private var max: Float? = null
private val rect = Rect()
override fun setYMin(yMin: Float?): YAxisInterface {
min = yMin
return this
}
override fun setYMax(yMax: Float?): YAxisInterface {
max = yMax
return this
}
override fun getYMax(): Float {
if (max != null) {
return max!!
}
if (view.series.isEmpty()) {
return (this.goalLine ?: 90f) + 10f
}
val seriesMax = view.series
.maxOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) {
return@maxOf 0f
}
return@maxOf serie.getDisplayedEntries().maxOf { entry -> entry.y }
}
if (this.goalLine != null) {
return if (seriesMax > this.goalLine!!) seriesMax else this.goalLine!! + 1000f
}
return seriesMax
}
override fun getYMin(): Float {
if (min != null) {
return min!!
}
if (view.series.isEmpty()) {
return this.goalLine ?: 0f
}
return view.series
.minOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) {
return@minOf 0f
}
return@minOf serie.getDisplayedEntries().minOf { entry -> entry.y }
}
}
override fun onDraw(canvas: Canvas, space: RectF): Float {
if (!enabled) {
return 0f
}
val min = getYMin()
val max = getYMax() - min
val top = space.top
val bottom = space.bottom
var maxWidth = 0f
val increment = (bottom - top) / labelCount
val valueIncrement = (max - min) / labelCount
for (index in 0 until labelCount) {
val text = onValueFormat((valueIncrement * (index + 1)))
textLabel.getTextBounds(text, 0, text.length, rect)
maxWidth = maxWidth.coerceAtLeast(rect.width().toFloat())
val posY = bottom - (index + 1) * increment
canvas.drawText(
text,
space.width() - rect.width().toFloat(),
(posY + rect.height() / 2).coerceAtLeast(rect.height().toFloat()),
textLabel
)
// canvas.drawDottedLine(0f, posY, canvas.width.toFloat(), posY, 40f, linePaint)
canvas.drawLine(space.left, posY, space.right - maxWidth - 32f, posY, linePaint)
}
if (this.goalLine != null) {
val pos = (1 - this.goalLine!! / max) * space.height() + space.top
canvas.drawDottedLine(
0f,
pos,
space.right - maxWidth - 32f,
pos,
space.right / 20,
goalLinePaint
)
}
return maxWidth + 32f
}
override fun refresh() {
// TODO("Not yet implemented")
}
private var goalLine: Float? = null
override fun setGoalLine(height: Float?) {
goalLine = height
}
}

View File

@ -0,0 +1,85 @@
package com.dzeio.charts.axis
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
sealed interface YAxisInterface {
/**
* whether or not this axis is displayed
*/
var enabled: Boolean
/**
* get/set the number of label of this Y axis
*
* the first/last labels are at the bottom/top of the chart
*/
var labelCount: Int
/**
* text label paint
*/
val textLabel: Paint
/**
* paint for the lines
*/
val linePaint: Paint
/**
* Goal line paint
*/
val goalLinePaint: Paint
/**
* run when manually refreshing the system
*
* this is where the pre-logic is handled to make [onDraw] quicker
*/
fun refresh()
/**
* override Y minimum
*
* @param yMin is set the min will ba at the value, if null it is calculated
*/
fun setYMin(yMin: Float?): YAxisInterface
/**
* override Y maximum
*
* @param yMax is set the max will ba at the value, if null it is calculated
*/
fun setYMax(yMax: Float?): YAxisInterface
/**
* get Y maximum
*
* @return the maximum value Y can get (for displayed values)
*/
fun getYMax(): Float
/**
* get Y minimum
*
* @return the minimum value Y can get (for displayed values)
*/
fun getYMin(): Float
/**
* function that draw our legend
*
* @param canvas the canvas to draw on
* @param space the space where it is allowed to draw on
*
* @return the width of the sidebar
*/
fun onDraw(canvas: Canvas, space: RectF): Float
/**
* Add a Goal line
*/
fun setGoalLine(height: Float?)
}

View File

@ -0,0 +1,11 @@
package com.dzeio.charts.axis
/**
* CURRENTLY UNUSED
*
* declare where the YAxis for the graph will be
*/
enum class YAxisPosition {
LEFT,
RIGHT
}

View File

@ -0,0 +1,144 @@
package com.dzeio.charts.components
import android.view.MotionEvent
import android.view.MotionEvent.INVALID_POINTER_ID
import android.view.ScaleGestureDetector
import android.view.View
/**
* Class handling the scroll/zoom for the library
*/
class ChartScroll(view: View) {
/**
* Enabled the zoom/unzoom of datas
*/
var zoomEnabled = true
/**
* Enable the horizontal scroll feature
*/
var scrollEnabled = true
// The active pointer is the one currently moving our object.
private var activePointerId = INVALID_POINTER_ID
private var lastTouchX: Float = 0f
private var lastTouchY: Float = 0f
private var posX: Float = 0f
private var posY: Float = 0f
private var lastZoom: Float = 100f
private var currentZoom: Float = 0f
private var onChartMoved: ((movementX: Float, movementY: Float) -> Unit)? = null
fun setOnChartMoved(fn: (movementX: Float, movementY: Float) -> Unit) {
onChartMoved = fn
}
private var onZoomChanged: ((scale: Float) -> Unit)? = null
/**
* @param fn.scale Float starting from 100%
*
* 99-% zoom out,
* 101+% zoom in
*/
fun setOnZoomChanged(fn: (scale: Float) -> Unit) {
onZoomChanged = fn
}
private val scaleGestureDetector = ScaleGestureDetector(
view.context,
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (currentZoom != detector.scaleFactor) {
currentZoom = detector.scaleFactor
onZoomChanged?.invoke(lastZoom + -currentZoom + 1)
}
return super.onScale(detector)
}
override fun onScaleEnd(detector: ScaleGestureDetector) {
super.onScaleEnd(detector)
lastZoom += -currentZoom + 1
}
}
)
/**
* Code mostly stolen from https://developer.android.com/training/gestures/scale#drag
*/
fun onTouchEvent(ev: MotionEvent): Boolean {
if (zoomEnabled) {
scaleGestureDetector.onTouchEvent(ev)
}
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
onToggleScroll?.invoke(false)
ev.actionIndex.also { pointerIndex ->
// Remember where we started (for dragging)
lastTouchX = ev.getX(pointerIndex)
lastTouchY = ev.getY(pointerIndex)
}
// Save the ID of this pointer (for dragging)
activePointerId = ev.getPointerId(0)
}
MotionEvent.ACTION_MOVE -> {
// Find the index of the active pointer and fetch its position
val (x: Float, y: Float) =
ev.findPointerIndex(activePointerId).let { pointerIndex ->
// Calculate the distance moved
ev.getX(pointerIndex) to ev.getY(pointerIndex)
}
posX += x - lastTouchX
posY += y - lastTouchY
if (scrollEnabled) {
onChartMoved?.invoke(-posX, posY)
}
// Remember this touch position for the next move event
lastTouchX = x
lastTouchY = y
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
onToggleScroll?.invoke(true)
activePointerId = INVALID_POINTER_ID
}
MotionEvent.ACTION_POINTER_UP -> {
onToggleScroll?.invoke(true)
ev.actionIndex.also { pointerIndex ->
ev.getPointerId(pointerIndex)
.takeIf { it == activePointerId }
?.run {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
val newPointerIndex = if (pointerIndex == 0) 1 else 0
lastTouchX = ev.getX(newPointerIndex)
lastTouchY = ev.getY(newPointerIndex)
activePointerId = ev.getPointerId(newPointerIndex)
}
}
}
}
return true
}
private var onToggleScroll: ((Boolean) -> Unit)? = null
/**
* @param ev if input is false disable scroll
*/
fun setOnToggleScroll(ev: (Boolean) -> Unit) {
onToggleScroll = ev
}
}

View File

@ -0,0 +1,125 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.util.Log
import com.dzeio.charts.ChartView
import com.dzeio.charts.utils.drawRoundRect
class BarSerie(
private val view: ChartView
) : BaseSerie(view) {
private companion object {
const val TAG = "Charts/BarSerie"
}
init {
view.series.add(this)
}
val barPaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#123456")
}
val textPaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#FC496D")
textSize = 30f
textAlign = Paint.Align.CENTER
}
private val rect = Rect()
override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
val displayedEntries = getDisplayedEntries()
val barWidth = view.xAxis.getEntryWidth(drawableSpace).toFloat()
val max = view.yAxis.getYMax()
val min = view.yAxis.getYMin()
// Log.d(TAG, "${space.left}, ${space.right}")
for (entry in displayedEntries) {
// calculated height in percent from 0 to 100
val top = (1 - entry.y / max) * drawableSpace.height() + drawableSpace.top
var posX = drawableSpace.left + view.xAxis.getPositionOnRect(
entry,
drawableSpace
).toFloat()
val right = (posX + barWidth).coerceAtMost(drawableSpace.right)
if (posX > right) {
continue
} else if (posX < drawableSpace.left) {
posX = drawableSpace.left
}
if (right < drawableSpace.left) {
continue
}
// handle color recoloration
val paint = Paint(barPaint)
if (entry.color != null) {
paint.color = entry.color!!
}
canvas.drawRoundRect(
posX,
top,
right,
drawableSpace.bottom,
// 8f, 8f,
32f,
32f,
0f,
0f,
paint
)
// handle text display
val text = view.yAxis.onValueFormat(entry.y)
textPaint.getTextBounds(text, 0, text.length, rect)
val textLeft = (posX + barWidth / 2)
if (
// handle right side
textLeft + rect.width() / 2 > right ||
// handle left sie
textLeft - rect.width() / 2 < drawableSpace.left
) {
continue
}
val doDisplayIn =
rect.height() < drawableSpace.bottom - top &&
rect.width() < barWidth
var textY = if (doDisplayIn) top + rect.height() + 16f else top - 16f
if (textY < 0) {
textY = drawableSpace.top + rect.height()
}
canvas.drawText(
text,
textLeft,
textY,
textPaint
)
}
}
override fun refresh() {
// TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,160 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.util.Log
import kotlin.math.max
class BarSerie : SerieAbstract() {
companion object {
const val TAG = "DzeioCharts/BarSerie"
}
var spacing: Float = 8f
/**
* Values displayed on the grapd
*/
var displayedDatas = arrayListOf<Float>()
/**
* Target values
*/
var targetDatas = arrayListOf<Float>()
var targetPercentList = arrayListOf<Float>()
var percentList = arrayListOf<Float>()
var previousRefresh = 0
private var fgPaint: Paint = Paint().apply {
isAntiAlias = true
}
private val r = Rect()
override fun onUpdate(): Boolean {
var needNewFrame = false
for (i in targetPercentList.indices) {
val value = view.animation.updateValue(
1f,
targetPercentList[i],
percentList[i],
0f,
0.00f
)
if (value != percentList[i]) {
needNewFrame = true
percentList[i] = value
}
}
return needNewFrame
}
override fun prepareData() {
val max: Float = if (view.yAxis.max != null) view.yAxis.max!! else {
getYMax(true)
}
targetPercentList = arrayListOf()
// Log.d(TAG, "offset: ${view.getXOffset()}, displayed: ${view.getDisplayedEntries()}")
for (item in getDisplayedEntries()) {
// // // Process bottom texts
// val text = view.xAxis.onValueFormat(item.x)
// bottomTexts.add(text)
//
// // get Text boundaries
// view.xAxis.labels.build().getTextBounds(text, 0, text.length, r)
//
// // get height of text
// if (bottomTextHeight < r.height()) {
// bottomTextHeight = r.height()
// }
//
// // get text descent
// val descent = abs(r.bottom)
// if (bottomTextDescent < descent) {
// bottomTextDescent = descent
// }
// process values
// Log.d(TAG, item.y.toString())
// add to animations the values
targetPercentList.add(1 - item.y / max)
}
// post list
val offset = view.getXOffset()
val movement = offset - previousRefresh
Log.d(TAG, "$offset - $previousRefresh = $movement")
if (movement != 0) {
previousRefresh = offset
}
// if (movement != 0) {
// Log.d(TAG, movement.toString())
// }
if (movement >= 1) {
percentList = percentList.subList(1, percentList.size).toCollection(ArrayList())
percentList.add(1f)
} else if (movement <= -1) {
percentList = percentList.subList(0, percentList.size - 1).toCollection(ArrayList())
percentList.add(0, 1f)
}
if (percentList.isEmpty() || percentList.size < targetPercentList.size) {
val temp = targetPercentList.size - percentList.size
for (i in 0 until temp) {
percentList.add(1f)
}
} else if (percentList.size > targetPercentList.size) {
val temp = percentList.size - targetPercentList.size
for (i in 0 until temp) {
percentList.removeAt(percentList.size - 1)
}
}
fgPaint.color = view.yAxis.color
}
override fun displayData(canvas: Canvas, rect: RectF) {
val barWidth = (rect.width() - view.padding * 2) / view.getDisplayedEntries() - spacing
if (percentList.isNotEmpty()) {
// draw each rectangles
for (i in 1..percentList.size) {
// Log.d(TAG, percentList[i - 1].toString())
val left = rect.left + spacing * i + barWidth * (i - 1).toFloat() + view.padding
// Log.d(TAG, "$spacing, $i, $barWidth = $left")
val right = rect.left + (spacing + barWidth) * i.toFloat()
val bottom = rect.top + rect.height() - view.padding
val top = (bottom - rect.top) * percentList[i - 1] + view.padding
// create rounded rect
canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, fgPaint)
// remove the bottom corners DUH
canvas.drawRect(left, max(top, bottom - 8f), right, bottom, fgPaint)
val targetTop = (bottom - rect.top) * targetPercentList[i - 1]
val text = view.yAxis.onValueFormat(getYMax(true) - getYMax(true) * targetPercentList[i - 1], true)
view.xAxis.labels.build().getTextBounds(text, 0, text.length, r)
val doDisplayIn =
r.width() + 10f < barWidth && bottom - targetTop > r.height() + 40f
if (view.debug || !doDisplayIn || (doDisplayIn && bottom - top > r.height() + 40f)) {
val y = if (doDisplayIn) top + r.height() + 20f else top - r.height()
canvas.drawText(
text,
left + (right - left) / 2,
y,
view.xAxis.labels.build()
)
}
}
}
}
}

View File

@ -0,0 +1,50 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.RectF
import com.dzeio.charts.ChartViewInterface
import com.dzeio.charts.Entry
import com.dzeio.charts.axis.YAxisPosition
import kotlin.math.roundToInt
sealed class BaseSerie(
private val view: ChartViewInterface
) : SerieInterface {
private companion object {
const val TAG = "Charts/BaseSerie"
}
override var formatValue: (entry: Entry) -> String = { entry -> entry.y.roundToInt().toString()}
override var yAxisPosition: YAxisPosition = YAxisPosition.RIGHT
override var entries: ArrayList<Entry> = arrayListOf()
override fun getDisplayedEntries(): ArrayList<Entry> {
val minX = view.xAxis.x
val maxX = minX + view.xAxis.getDataWidth()
val result: ArrayList<Entry> = arrayListOf()
var lastIndex = -1
for (i in 0 until entries.size) {
val it = entries[i]
if (it.x in minX..maxX) {
if (result.size === 0 && i > 0) {
result.add((entries[i - 1]))
}
lastIndex = i
result.add(it)
}
}
if (lastIndex < entries.size - 1) {
result.add(entries [lastIndex + 1])
}
return result
}
abstract override fun onDraw(canvas: Canvas, drawableSpace: RectF)
}

View File

@ -0,0 +1,74 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import com.dzeio.charts.ChartView
class LineSerie(
private val view: ChartView
) : BaseSerie(view) {
private companion object {
const val TAG = "Charts/LineSerie"
}
init {
view.series.add(this)
}
val linePaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#123456")
strokeWidth = 5f
}
val textPaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#FC496D")
textSize = 30f
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
val displayedEntries = getDisplayedEntries()
displayedEntries.sortBy { it.x }
val max = view.yAxis.getYMax()
var previousPosX: Float? = null
var previousPosY: Float? = null
for (entry in displayedEntries) {
// calculated height in percent from 0 to 100
val top = (1 - entry.y / max) * drawableSpace.height() + drawableSpace.top
val posX = (drawableSpace.left +
view.xAxis.getPositionOnRect(entry, drawableSpace) +
view.xAxis.getEntryWidth(drawableSpace) / 2f).toFloat()
// handle color recoloration
val paint = Paint(linePaint)
if (entry.color != null) {
paint.color = entry.color!!
}
// draw smol point
if (posX < drawableSpace.right) {
canvas.drawCircle(posX, top, paint.strokeWidth, paint)
}
// draw line
if (previousPosX != null && previousPosY != null) {
canvas.drawLine(previousPosX, previousPosY, posX, top, paint)
}
previousPosX = posX
previousPosY = top
}
}
override fun refresh() {
// TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,47 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.RectF
import com.dzeio.charts.Entry
import com.dzeio.charts.axis.YAxisPosition
sealed interface SerieInterface {
/**
* location of the Y axis
*/
var yAxisPosition: YAxisPosition
/**
* filter out out of display entries
*
* @return the list of entries displayed
*/
fun getDisplayedEntries(): ArrayList<Entry>
/**
* set the entries for the list
*/
var entries: ArrayList<Entry>
/**
* Change how the value is displayed for each elements
*/
var formatValue: (entry: Entry) -> String
/**
* function that display the graph
*
* @param canvas the canvas to draw on
* @param drawableSpace the space you are allowed to draw on
*/
fun onDraw(canvas: Canvas, drawableSpace: RectF)
/**
* run when manually refreshing the system
*
* this is where the pre-logic is handled to make [onDraw] quicker
*/
fun refresh()
}

View File

@ -0,0 +1,144 @@
package com.dzeio.charts.utils
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import kotlin.math.sqrt
/**
* draw a dotted line
*/
fun Canvas.drawDottedLine(
startX: Float,
startY: Float,
endX: Float,
endY: Float,
spacing: Float,
paint: Paint
) {
//calculate line length
val length = if (endX - startX == 0f) {
// just length of Y
endY - startY
} else if (endY - startY == 0f) {
// just length of X
endX - startX
} else {
// calculate using the Pythagorean theorem
sqrt((startX + endX) * (startX + endX) + (startY + endY) * (startY + endY))
}
val lineCount = (length / spacing).toInt()
val lenX = endX - startX
val lenY = endY - startY
// Log.d("DrawDottedLine", "----------- Start -----------")
// Log.d("DrawDottedLine", "lenX: $lenX, lenY: $lenY")
for (line in 0 until lineCount) {
if (line % 2 == 0) {
continue
}
val sx = lenX / lineCount * line + startX
val sy = lenY / lineCount * line + startY
val ex = lenX / lineCount * (line + 1) + startX
val ey = lenY / lineCount * (line + 1) + startY
// Log.d("DrawDottedLine", "$sx, $sy, $ex, $ey")
this.drawLine(sx, sy, ex, ey, paint)
// line
// total line startX, endX, startY, endY
// total line length
}
}
/**
* A more customizable drawRoundRect function
*/
fun Canvas.drawRoundRect(
left: Float,
top: Float,
right: Float,
bottom: Float,
topLeft: Float,
topRight: Float,
bottomLeft: Float,
bottomRight: Float,
paint: Paint
) {
val maxRound = arrayOf(topLeft, topRight, bottomLeft, bottomRight).maxOf { it }
val width = right - left
val height = bottom - top
// draw first/global rect
drawRoundRect(left, top, right, bottom, maxRound, maxRound, paint)
// top left border
if (topLeft == 0f) {
drawRect(left, top, left + width / 2, top + height / 2, paint)
} else {
drawRoundRect(left, top, left + width / 2, top + height / 2, topLeft, topLeft, paint)
}
// top right border
if (topRight == 0f) {
drawRect(right - width / 2, top, right, top + height / 2, paint)
} else {
drawRoundRect(right - width / 2, top, right, top + height / 2, topRight, topRight, paint)
}
// bottom left border
if (bottomLeft == 0f) {
drawRect(left, bottom - height / 2, left + width / 2, bottom, paint)
} else {
drawRoundRect(
left,
bottom - height / 2,
left + width / 2,
bottom,
bottomLeft,
bottomLeft,
paint
)
}
// bottom right border
if (bottomRight == 0f) {
drawRect(right - width / 2, bottom - height / 2, right, bottom, paint)
} else {
drawRoundRect(
right - width / 2,
bottom - height / 2,
right,
bottom,
bottomRight,
bottomRight,
paint
)
}
}
/**
* A more customizable drawRoundRect function
*/
fun Canvas.drawRoundRect(
rect: RectF,
topLeft: Float,
topRight: Float,
bottomLeft: Float,
bottomRight: Float,
paint: Paint
) {
drawRoundRect(
rect.left,
rect.top,
rect.right,
rect.bottom,
topLeft,
topRight,
bottomLeft,
bottomRight,
paint
)
}