mirror of
https://github.com/dzeiocom/charts.git
synced 2025-04-22 18:52:08 +00:00
feat: Add basic Annotations (#38)
This commit is contained in:
parent
73d2d21a7e
commit
919cbaa823
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
*
|
||||
|
165
library/src/main/java/com/dzeio/charts/components/Annotation.kt
Normal file
165
library/src/main/java/com/dzeio/charts/components/Annotation.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user