mirror of
https://github.com/dzeiocom/charts.git
synced 2025-06-07 16:49:56 +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 android.view.View
|
||||||
import com.dzeio.charts.axis.XAxis
|
import com.dzeio.charts.axis.XAxis
|
||||||
import com.dzeio.charts.axis.YAxis
|
import com.dzeio.charts.axis.YAxis
|
||||||
|
import com.dzeio.charts.components.Annotation
|
||||||
import com.dzeio.charts.components.ChartScroll
|
import com.dzeio.charts.components.ChartScroll
|
||||||
import com.dzeio.charts.series.SerieInterface
|
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 animator: Animation = Animation()
|
||||||
|
|
||||||
|
override val annotator: Annotation = Annotation(this)
|
||||||
|
|
||||||
override var type: ChartType = ChartType.BASIC
|
override var type: ChartType = ChartType.BASIC
|
||||||
|
|
||||||
override var debug: Boolean = false
|
override var debug: Boolean = false
|
||||||
@ -55,6 +58,34 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
|
|
||||||
refresh()
|
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 {
|
// setOnZoomChanged {
|
||||||
// Log.d(TAG, "New Zoom: $it")
|
// Log.d(TAG, "New Zoom: $it")
|
||||||
// zoom = (it * 1.2).toFloat()
|
// zoom = (it * 1.2).toFloat()
|
||||||
@ -64,6 +95,7 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
|
|
||||||
// rect used for calculations
|
// rect used for calculations
|
||||||
private val rect = RectF()
|
private val rect = RectF()
|
||||||
|
private val seriesRect = RectF()
|
||||||
|
|
||||||
// stroke used while in debug
|
// stroke used while in debug
|
||||||
private val debugStrokePaint = Paint().apply {
|
private val debugStrokePaint = Paint().apply {
|
||||||
@ -121,7 +153,7 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
})
|
})
|
||||||
|
|
||||||
// chart draw rectangle
|
// chart draw rectangle
|
||||||
rect.apply {
|
seriesRect.apply {
|
||||||
set(
|
set(
|
||||||
padding,
|
padding,
|
||||||
padding,
|
padding,
|
||||||
@ -133,22 +165,24 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
var needRedraw = false
|
var needRedraw = false
|
||||||
if (type == ChartType.STACKED) {
|
if (type == ChartType.STACKED) {
|
||||||
for (serie in series.reversed()) {
|
for (serie in series.reversed()) {
|
||||||
val tmp = serie.onDraw(canvas, rect)
|
val tmp = serie.onDraw(canvas, seriesRect)
|
||||||
if (tmp) {
|
if (tmp) {
|
||||||
needRedraw = true
|
needRedraw = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (serie in series) {
|
for (serie in series) {
|
||||||
val tmp = serie.onDraw(canvas, rect)
|
val tmp = serie.onDraw(canvas, seriesRect)
|
||||||
if (tmp) {
|
if (tmp) {
|
||||||
needRedraw = true
|
needRedraw = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (needRedraw) {
|
|
||||||
postDelayed({ this.invalidate() }, animator.getDelay().toLong())
|
annotator.onDraw(canvas, seriesRect)
|
||||||
}
|
|
||||||
|
postDelayed({ this.invalidate() }, animator.getDelay().toLong())
|
||||||
|
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package com.dzeio.charts
|
|||||||
|
|
||||||
import com.dzeio.charts.axis.XAxisInterface
|
import com.dzeio.charts.axis.XAxisInterface
|
||||||
import com.dzeio.charts.axis.YAxisInterface
|
import com.dzeio.charts.axis.YAxisInterface
|
||||||
|
import com.dzeio.charts.components.Annotation
|
||||||
import com.dzeio.charts.series.SerieInterface
|
import com.dzeio.charts.series.SerieInterface
|
||||||
|
|
||||||
interface ChartViewInterface {
|
interface ChartViewInterface {
|
||||||
@ -40,6 +41,8 @@ interface ChartViewInterface {
|
|||||||
*/
|
*/
|
||||||
var series: ArrayList<SerieInterface>
|
var series: ArrayList<SerieInterface>
|
||||||
|
|
||||||
|
val annotator: Annotation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* refresh and run pre-display logic the chart
|
* 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.MotionEvent.INVALID_POINTER_ID
|
||||||
import android.view.ScaleGestureDetector
|
import android.view.ScaleGestureDetector
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class handling the scroll/zoom for the library
|
* Class handling the scroll/zoom for the library
|
||||||
@ -32,6 +33,11 @@ class ChartScroll(view: View) {
|
|||||||
private var lastZoom: Float = 100f
|
private var lastZoom: Float = 100f
|
||||||
private var currentZoom: Float = 0f
|
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
|
private var onChartMoved: ((movementX: Float, movementY: Float) -> Unit)? = null
|
||||||
fun setOnChartMoved(fn: (movementX: Float, movementY: Float) -> Unit) {
|
fun setOnChartMoved(fn: (movementX: Float, movementY: Float) -> Unit) {
|
||||||
onChartMoved = fn
|
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
|
* 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)
|
ev.getX(pointerIndex) to ev.getY(pointerIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
posX += x - lastTouchX
|
val moveX = x - lastTouchX
|
||||||
posY += y - lastTouchY
|
val moveY = y - lastTouchY
|
||||||
|
|
||||||
|
if (!hasMoved && (abs(moveY) > 1 || abs(moveX) > 1)) {
|
||||||
|
hasMoved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
posX += moveX
|
||||||
|
posY += moveY
|
||||||
|
|
||||||
|
|
||||||
if (scrollEnabled) {
|
if (scrollEnabled) {
|
||||||
onChartMoved?.invoke(-posX, posY)
|
onChartMoved?.invoke(-posX, posY)
|
||||||
@ -111,6 +127,15 @@ class ChartScroll(view: View) {
|
|||||||
lastTouchY = y
|
lastTouchY = y
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
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)
|
onToggleScroll?.invoke(true)
|
||||||
activePointerId = INVALID_POINTER_ID
|
activePointerId = INVALID_POINTER_ID
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user