feat: Add basic Annotations (#38)

This commit is contained in:
Florian Bouillon 2023-01-16 00:30:51 +01:00 committed by GitHub
parent 73d2d21a7e
commit 919cbaa823
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 235 additions and 8 deletions

View File

@ -11,6 +11,7 @@ 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.Annotation
import com.dzeio.charts.components.ChartScroll
import com.dzeio.charts.series.SerieInterface
@ -23,6 +24,8 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
override val animator: Animation = Animation()
override val annotator: Annotation = Annotation(this)
override var type: ChartType = ChartType.BASIC
override var debug: Boolean = false
@ -55,6 +58,34 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
refresh()
}
setOnChartClick { x, y ->
// Log.d("Chart clicked at", "$x, $y")
val dataset = series.map { it.getDisplayedEntries() }.reduce { acc, entries ->
acc.addAll(entries)
return@reduce acc
}
val entrySize = xAxis.getEntryWidth(seriesRect)
val clickPos = x
var entryFound = false
for (entry in dataset) {
val posX = xAxis.getPositionOnRect(entry, seriesRect)
// Log.d("pouet", "$posX, $clickPos, ${posX + entrySize}")
if (posX <= clickPos && clickPos <= posX + entrySize) {
// Log.d("entry found!", "$entry")
if (annotator.entry == entry) {
annotator.entry = null
} else {
annotator.entry = entry
}
entryFound = true
break
}
}
if (!entryFound && annotator.entry != null) {
annotator.entry = null
}
refresh()
}
// setOnZoomChanged {
// Log.d(TAG, "New Zoom: $it")
// zoom = (it * 1.2).toFloat()
@ -64,6 +95,7 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
// rect used for calculations
private val rect = RectF()
private val seriesRect = RectF()
// stroke used while in debug
private val debugStrokePaint = Paint().apply {
@ -121,7 +153,7 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
})
// chart draw rectangle
rect.apply {
seriesRect.apply {
set(
padding,
padding,
@ -133,22 +165,24 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
var needRedraw = false
if (type == ChartType.STACKED) {
for (serie in series.reversed()) {
val tmp = serie.onDraw(canvas, rect)
val tmp = serie.onDraw(canvas, seriesRect)
if (tmp) {
needRedraw = true
}
}
} else {
for (serie in series) {
val tmp = serie.onDraw(canvas, rect)
val tmp = serie.onDraw(canvas, seriesRect)
if (tmp) {
needRedraw = true
}
}
}
if (needRedraw) {
postDelayed({ this.invalidate() }, animator.getDelay().toLong())
}
annotator.onDraw(canvas, seriesRect)
postDelayed({ this.invalidate() }, animator.getDelay().toLong())
super.onDraw(canvas)
}

View File

@ -2,6 +2,7 @@ package com.dzeio.charts
import com.dzeio.charts.axis.XAxisInterface
import com.dzeio.charts.axis.YAxisInterface
import com.dzeio.charts.components.Annotation
import com.dzeio.charts.series.SerieInterface
interface ChartViewInterface {
@ -40,6 +41,8 @@ interface ChartViewInterface {
*/
var series: ArrayList<SerieInterface>
val annotator: Annotation
/**
* refresh and run pre-display logic the chart
*

View File

@ -0,0 +1,165 @@
package com.dzeio.charts.components
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.view.View
import com.dzeio.charts.ChartView
import com.dzeio.charts.Entry
class Annotation(
private val view: ChartView
) {
val backgroundPaint: Paint = Paint().apply {
color = Color.WHITE
setShadowLayer(12.0f, 0.0f, 0.0f, Color.GRAY)
view.setLayerType(View.LAYER_TYPE_SOFTWARE, this)
}
var annotationSubTitleFormat: (entry: Entry) -> String = { it.y.toString() }
var annotationTitleFormat: (entry: Entry) -> String = { it.x.toString() }
var enabled = true
var entry: Entry? = null
var hideOnScroll = true
var orientation = Orientation.HORIZONTAL
var padding: Float = 32f
private val rect: Rect = Rect()
val subTitlePaint: Paint = Paint().apply {
textSize = 48.0f
color = Color.BLACK
textAlign = Paint.Align.CENTER
}
val titlePaint: Paint = Paint().apply {
textSize = 64.0f
color = Color.BLACK
textAlign = Paint.Align.CENTER
}
/* compiled from: Annotation.kt */
enum class Orientation {
VERTICAL,
HORIZONTAL
}
private companion object {
val TAG = Annotation::class.java.simpleName
}
fun onDraw(canvas: Canvas, space: RectF) {
if (entry == null || !enabled) {
return
}
val xAxis = view.xAxis
val yAxis = view.yAxis
val x = xAxis.getPositionOnRect(entry!!, space)
val y = yAxis.getPositionOnRect(entry!!, space)
.coerceIn(space.top, space.bottom)
val xCenter = view.xAxis.getEntryWidth(space) / 2.0 + x
val xText = annotationSubTitleFormat.invoke(entry!!)
val yText = annotationTitleFormat.invoke(entry!!)
titlePaint.getTextBounds(yText, 0, yText.length, rect)
val yTextWidth = rect.width()
val yTextHeight = rect.height()
subTitlePaint.getTextBounds(xText, 0, xText.length, rect)
val xTextWidth = rect.width()
val xTextHeight = rect.height()
var contentWitdh = Math.max(yTextWidth, xTextWidth)
if (orientation == Orientation.HORIZONTAL) {
contentWitdh = (yTextWidth.toFloat() + padding + xTextWidth.toFloat()).toInt()
}
var contentHeight = (yTextHeight.toFloat() + padding + xTextHeight.toFloat()).toInt()
if (orientation == Orientation.HORIZONTAL) {
contentHeight = Math.max(yTextHeight, xTextHeight)
}
val finalRect = RectF(
(xCenter - (contentWitdh / 2).toDouble()).toFloat() - padding,
space.top,
((contentWitdh / 2).toDouble() + xCenter).toFloat() + padding,
padding * 2f + contentHeight.toFloat()
)
var reverseArrow = false
if (y < finalRect.height() + padding * 2f) {
finalRect.top += padding * 3f + y
finalRect.bottom += padding * 3f + y
reverseArrow = true
}
if (finalRect.left < space.left) {
finalRect.right += space.left - finalRect.left
finalRect.left = space.left
} else if (finalRect.right > space.right) {
finalRect.left -= finalRect.right - space.right
finalRect.right = space.right
}
val twoPointsY = if (reverseArrow) finalRect.top + 1f else finalRect.bottom
val p1 = PointF((xCenter - padding.toDouble()).toFloat(), twoPointsY)
val p2 = PointF((padding.toDouble() + xCenter).toFloat(), twoPointsY)
val p3 = PointF(xCenter.toFloat(), if (reverseArrow) { twoPointsY - padding } else { padding + twoPointsY })
val path = Path()
path.fillType = Path.FillType.EVEN_ODD
path.moveTo(p1.x, p1.y)
path.lineTo(p2.x, p2.y)
path.lineTo(p3.x, p3.y)
path.close()
canvas.drawRoundRect(finalRect, 16.0f, 16.0f, backgroundPaint)
canvas.drawPath(path, Paint(backgroundPaint).apply { clearShadowLayer() })
if (orientation == Orientation.VERTICAL) {
canvas.drawText(
yText,
finalRect.left + padding + (contentWitdh / 2).toFloat(),
finalRect.top + padding + yTextHeight.toFloat(),
titlePaint
)
canvas.drawText(
xText,
finalRect.left + padding + (contentWitdh / 2).toFloat(),
finalRect.top + padding + yTextHeight.toFloat() + padding + xTextHeight.toFloat(),
subTitlePaint
)
} else {
val left = finalRect.left + padding
canvas.drawText(
yText,
left + ((contentWitdh - xTextWidth).toFloat() - padding) / 2f,
finalRect.top + padding + (contentHeight.toFloat() + padding) / 2f,
titlePaint
)
canvas.drawText(
xText,
left + yTextWidth.toFloat() + padding + ((contentWitdh - yTextWidth).toFloat() - padding) / 2f,
finalRect.top + padding + (contentHeight.toFloat() + padding) / 2f,
subTitlePaint
)
}
}
}

View File

@ -4,6 +4,7 @@ import android.view.MotionEvent
import android.view.MotionEvent.INVALID_POINTER_ID
import android.view.ScaleGestureDetector
import android.view.View
import kotlin.math.abs
/**
* Class handling the scroll/zoom for the library
@ -32,6 +33,11 @@ class ChartScroll(view: View) {
private var lastZoom: Float = 100f
private var currentZoom: Float = 0f
private var onChartClick: ((x: Float, y: Float) -> Unit)? = null
fun setOnChartClick(fn: (x: Float, y: Float) -> Unit) {
onChartClick = fn
}
private var onChartMoved: ((movementX: Float, movementY: Float) -> Unit)? = null
fun setOnChartMoved(fn: (movementX: Float, movementY: Float) -> Unit) {
onChartMoved = fn
@ -69,6 +75,8 @@ class ChartScroll(view: View) {
}
)
private var hasMoved = false
/**
* Code mostly stolen from https://developer.android.com/training/gestures/scale#drag
*/
@ -99,8 +107,16 @@ class ChartScroll(view: View) {
ev.getX(pointerIndex) to ev.getY(pointerIndex)
}
posX += x - lastTouchX
posY += y - lastTouchY
val moveX = x - lastTouchX
val moveY = y - lastTouchY
if (!hasMoved && (abs(moveY) > 1 || abs(moveX) > 1)) {
hasMoved = true
}
posX += moveX
posY += moveY
if (scrollEnabled) {
onChartMoved?.invoke(-posX, posY)
@ -111,6 +127,15 @@ class ChartScroll(view: View) {
lastTouchY = y
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (!hasMoved) {
val (x: Float, y: Float) =
ev.findPointerIndex(activePointerId).let { pointerIndex ->
// Calculate the distance moved
ev.getX(pointerIndex) to ev.getY(pointerIndex)
}
onChartClick?.invoke(x, y)
}
hasMoved = false
onToggleScroll?.invoke(true)
activePointerId = INVALID_POINTER_ID
}