From 919cbaa823fc582dd995964730c06819f24bbc48 Mon Sep 17 00:00:00 2001 From: Avior Date: Mon, 16 Jan 2023 00:30:51 +0100 Subject: [PATCH] feat: Add basic Annotations (#38) --- .../main/java/com/dzeio/charts/ChartView.kt | 46 ++++- .../com/dzeio/charts/ChartViewInterface.kt | 3 + .../com/dzeio/charts/components/Annotation.kt | 165 ++++++++++++++++++ .../dzeio/charts/components/ChartScroll.kt | 29 ++- 4 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 library/src/main/java/com/dzeio/charts/components/Annotation.kt diff --git a/library/src/main/java/com/dzeio/charts/ChartView.kt b/library/src/main/java/com/dzeio/charts/ChartView.kt index 5f5261d..ca295c1 100644 --- a/library/src/main/java/com/dzeio/charts/ChartView.kt +++ b/library/src/main/java/com/dzeio/charts/ChartView.kt @@ -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) } diff --git a/library/src/main/java/com/dzeio/charts/ChartViewInterface.kt b/library/src/main/java/com/dzeio/charts/ChartViewInterface.kt index a9d0e8f..208c64d 100644 --- a/library/src/main/java/com/dzeio/charts/ChartViewInterface.kt +++ b/library/src/main/java/com/dzeio/charts/ChartViewInterface.kt @@ -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 + val annotator: Annotation + /** * refresh and run pre-display logic the chart * diff --git a/library/src/main/java/com/dzeio/charts/components/Annotation.kt b/library/src/main/java/com/dzeio/charts/components/Annotation.kt new file mode 100644 index 0000000..84b55e7 --- /dev/null +++ b/library/src/main/java/com/dzeio/charts/components/Annotation.kt @@ -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 + ) + } + } +} diff --git a/library/src/main/java/com/dzeio/charts/components/ChartScroll.kt b/library/src/main/java/com/dzeio/charts/components/ChartScroll.kt index 38e1b9f..d86d7f6 100644 --- a/library/src/main/java/com/dzeio/charts/components/ChartScroll.kt +++ b/library/src/main/java/com/dzeio/charts/components/ChartScroll.kt @@ -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 }