From 83fc6a049007321926a3292119669095a6c8e2a0 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sun, 24 Sep 2023 20:39:33 +0200 Subject: [PATCH] GraphSeries -> kt --- .../core/graph/data/AreaGraphSeries.java | 453 ------------------ .../core/graph/data/AreaGraphSeries.kt | 412 ++++++++++++++++ .../core/graph/data/FixedLineGraphSeries.java | 392 --------------- .../core/graph/data/FixedLineGraphSeries.kt | 356 ++++++++++++++ .../data/PointsWithLabelGraphSeries.java | 382 --------------- .../graph/data/PointsWithLabelGraphSeries.kt | 346 +++++++++++++ 6 files changed, 1114 insertions(+), 1227 deletions(-) delete mode 100644 core/main/src/main/java/info/nightscout/core/graph/data/AreaGraphSeries.java create mode 100644 core/main/src/main/java/info/nightscout/core/graph/data/AreaGraphSeries.kt delete mode 100644 core/main/src/main/java/info/nightscout/core/graph/data/FixedLineGraphSeries.java create mode 100644 core/main/src/main/java/info/nightscout/core/graph/data/FixedLineGraphSeries.kt delete mode 100644 core/main/src/main/java/info/nightscout/core/graph/data/PointsWithLabelGraphSeries.java create mode 100644 core/main/src/main/java/info/nightscout/core/graph/data/PointsWithLabelGraphSeries.kt diff --git a/core/main/src/main/java/info/nightscout/core/graph/data/AreaGraphSeries.java b/core/main/src/main/java/info/nightscout/core/graph/data/AreaGraphSeries.java deleted file mode 100644 index b96cccdb13..0000000000 --- a/core/main/src/main/java/info/nightscout/core/graph/data/AreaGraphSeries.java +++ /dev/null @@ -1,453 +0,0 @@ -/** - * GraphView - * Copyright (C) 2014 Jonas Gehring - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, - * with the "Linking Exception", which can be found at the license.txt - * file in this program. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * with the "Linking Exception" along with this program; if not, - * write to the author Jonas Gehring . - */ -package info.nightscout.core.graph.data; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; - -import com.jjoe64.graphview.GraphView; -import com.jjoe64.graphview.series.BaseSeries; - -import java.util.Iterator; - -/** - * Series to plot the data as line. - * The line can be styled with many options. - * - * @author jjoe64 - */ -public class AreaGraphSeries extends BaseSeries { - /** - * wrapped styles regarding the line - */ - private static final class Styles { - /** - * the thickness of the line. - * This option will be ignored if you are - * using a custom paint via {@link #setCustomPaint(Paint)} - */ - private int thickness = 5; - - /** - * flag whether the area under the line to the bottom - * of the viewport will be filled with a - * specific background color. - * - * @see #backgroundColor - */ - private boolean drawBackground = false; - - /** - * flag whether the data points are highlighted as - * a visible point. - * - * @see #dataPointsRadius - */ - private boolean drawDataPoints = false; - - /** - * the radius for the data points. - * - * @see #drawDataPoints - */ - private float dataPointsRadius = 10f; - - /** - * the background color for the filling under - * the line. - * - * @see #drawBackground - */ - private int backgroundColor = Color.argb(100, 172, 218, 255); - } - - /** - * wrapped styles - */ - private Styles mStyles; - - /** - * internal paint object - */ - private Paint mPaint; - - /** - * paint for the background - */ - private Paint mPaintBackground; - - /** - * path for the background filling - */ - private Path mPathBackground; - - /** - * path to the line - */ - private Path mPath; - private Path mSecondPath; - - /** - * custom paint that can be used. - * this will ignore the thickness and color styles. - */ - private Paint mCustomPaint; - - /** - * creates a series without data - */ - @SuppressWarnings("unused") public AreaGraphSeries() { - init(); - } - - /** - * creates a series with data - * - * @param data data points - */ - public AreaGraphSeries(E[] data) { - super(data); - init(); - } - - /** - * do the initialization - * creates internal objects - */ - protected void init() { - mStyles = new Styles(); - mPaint = new Paint(); - mPaint.setStrokeCap(Paint.Cap.ROUND); - mPaint.setStyle(Paint.Style.STROKE); - mPaintBackground = new Paint(); - - mPathBackground = new Path(); - mPath = new Path(); - mSecondPath = new Path(); - } - - /** - * plots the series - * draws the line and the background - * - * @param graphView graphview - * @param canvas canvas - * @param isSecondScale flag if it is the second scale - */ - @Override - public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { - resetDataPoints(); - - // get data - double maxX = graphView.getViewport().getMaxX(false); - double minX = graphView.getViewport().getMinX(false); - - double maxY; - double minY; - if (isSecondScale) { - maxY = graphView.getSecondScale().getMaxY(); - minY = graphView.getSecondScale().getMinY(); - } else { - maxY = graphView.getViewport().getMaxY(false); - minY = graphView.getViewport().getMinY(false); - } - - Iterator values = getValues(minX, maxX); - - // draw background - double lastEndY1; - double lastEndY2; - double lastEndX; - - // draw data - mPaint.setStrokeWidth(mStyles.thickness); - mPaint.setColor(getColor()); - mPaintBackground.setColor(mStyles.backgroundColor); - - Paint paint; - if (mCustomPaint != null) { - paint = mCustomPaint; - } else { - paint = mPaint; - } - - if (mStyles.drawBackground) { - mPathBackground.reset(); - } - - double diffY = maxY - minY; - double diffX = maxX - minX; - - float graphHeight = graphView.getGraphContentHeight(); - float graphWidth = graphView.getGraphContentWidth(); - float graphLeft = graphView.getGraphContentLeft(); - float graphTop = graphView.getGraphContentTop(); - - lastEndY1 = 0; - lastEndY2 = 0; - lastEndX = 0; - int i=0; - while (values.hasNext()) { - E value = values.next(); - - double valY1 = value.getY() - minY; - double ratY1 = valY1 / diffY; - double y1 = graphHeight * ratY1; - - double valY2 = value.getY2() - minY; - double ratY2 = valY2 / diffY; - double y2 = graphHeight * ratY2; - - double valX = value.getX() - minX; - double ratX = valX / diffX; - double x = graphWidth * ratX; - - double orgX = x; - double orgY1 = y1; - double orgY2 = y2; - - if (i > 0) { - // overdraw - if (x > graphWidth) { // end right - double b = ((graphWidth - lastEndX) * (y1 - lastEndY1)/(x - lastEndX)); - y1 = lastEndY1+b; - x = graphWidth; - } - if (x > graphWidth) { // end right - double b = ((graphWidth - lastEndX) * (y2 - lastEndY2)/(x - lastEndX)); - y2 = lastEndY2+b; - x = graphWidth; - } - if (y1 < 0) { // end bottom - double b = ((0 - lastEndY1) * (x - lastEndX)/(y1 - lastEndY1)); - x = lastEndX+b; - y1 = 0; - } - if (y2 < 0) { // end bottom - double b = ((0 - lastEndY2) * (x - lastEndX)/(y2 - lastEndY2)); - x = lastEndX+b; - y2 = 0; - } - if (y1 > graphHeight) { // end top - double b = ((graphHeight - lastEndY1) * (x - lastEndX)/(y1 - lastEndY1)); - x = lastEndX+b; - y1 = graphHeight; - } - if (y2 > graphHeight) { // end top - double b = ((graphHeight - lastEndY2) * (x - lastEndX)/(y2 - lastEndY2)); - x = lastEndX+b; - y2 = graphHeight; - } - if (lastEndY1 < 0) { // start bottom - double b = ((0 - y1) * (x - lastEndX)/(lastEndY1 - y1)); - lastEndX = x-b; - lastEndY1 = 0; - } - if (lastEndY2 < 0) { // start bottom - double b = ((0 - y2) * (x - lastEndX)/(lastEndY2 - y2)); - lastEndX = x-b; - lastEndY2 = 0; - } - if (lastEndX < 0) { // start left - double b = ((0 - x) * (y1 - lastEndY1)/(lastEndX - x)); - lastEndY1 = y1-b; - lastEndX = 0; - } - if (lastEndX < 0) { // start left - double b = ((0 - x) * (y2 - lastEndY2)/(lastEndX - x)); - lastEndY2 = y2-b; - lastEndX = 0; - } - if (lastEndY1 > graphHeight) { // start top - double b = ((graphHeight - y1) * (x - lastEndX)/(lastEndY1 - y1)); - lastEndX = x-b; - lastEndY1 = graphHeight; - } - if (lastEndY2 > graphHeight) { // start top - double b = ((graphHeight - y2) * (x - lastEndX)/(lastEndY2 - y2)); - lastEndX = x-b; - lastEndY2 = graphHeight; - } - - float startX = (float) lastEndX + (graphLeft + 1); - float startY1 = (float) (graphTop - lastEndY1) + graphHeight; - float startY2 = (float) (graphTop - lastEndY2) + graphHeight; - float endX = (float) x + (graphLeft + 1); - float endY1 = (float) (graphTop - y1) + graphHeight; - float endY2 = (float) (graphTop - y2) + graphHeight; - - // draw data point - if (mStyles.drawDataPoints) { - //fix: last value was not drawn. Draw here now the end values - canvas.drawCircle(endX, endY1, mStyles.dataPointsRadius, mPaint); - canvas.drawCircle(endX, endY2, mStyles.dataPointsRadius, mPaint); - } - registerDataPoint(endX, endY1, value); - registerDataPoint(endX, endY2, value); - - mPath.reset(); - mSecondPath.reset(); - mPath.moveTo(startX, startY1); - mSecondPath.moveTo(startX, startY2); - mPath.lineTo(endX, endY1); - mSecondPath.lineTo(endX, endY2); - canvas.drawPath(mPath, paint); - canvas.drawPath(mSecondPath, paint); - if (mStyles.drawBackground) { - canvas.drawRect(startX, startY2, endX, endY1, mPaintBackground); - } - } else if (mStyles.drawDataPoints) { - //fix: last value not drawn as datapoint. Draw first point here, and then on every step the end values (above) - //float first_X = (float) x + (graphLeft + 1); - //float first_Y = (float) (graphTop - y) + graphHeight; - // canvas.drawCircle(first_X, first_Y, dataPointsRadius, mPaint); - } - lastEndY1 = orgY1; - lastEndY2 = orgY2; - lastEndX = orgX; - i++; - } - -/* - if (mStyles.drawBackground) { - // end / close path - mPathBackground.lineTo((float) lastUsedEndX, graphHeight + graphTop); - mPathBackground.lineTo(firstX, graphHeight + graphTop); - mPathBackground.close(); - canvas.drawPath(mPathBackground, mPaintBackground); - } -*/ - - } - - /** - * the thickness of the line. - * This option will be ignored if you are - * using a custom paint via {@link #setCustomPaint(Paint)} - * - * @return the thickness of the line - */ - public int getThickness() { - return mStyles.thickness; - } - - /** - * the thickness of the line. - * This option will be ignored if you are - * using a custom paint via {@link #setCustomPaint(Paint)} - * - * @param thickness thickness of the line - */ - public void setThickness(int thickness) { - mStyles.thickness = thickness; - } - - /** - * flag whether the area under the line to the bottom - * of the viewport will be filled with a - * specific background color. - * - * @return whether the background will be drawn - * @see #getBackgroundColor() - */ - public boolean isDrawBackground() { - return mStyles.drawBackground; - } - - /** - * flag whether the area under the line to the bottom - * of the viewport will be filled with a - * specific background color. - * - * @param drawBackground whether the background will be drawn - * @see #setBackgroundColor(int) - */ - public void setDrawBackground(boolean drawBackground) { - mStyles.drawBackground = drawBackground; - } - - /** - * flag whether the data points are highlighted as - * a visible point. - * - * @return flag whether the data points are highlighted - * @see #setDataPointsRadius(float) - */ - public boolean isDrawDataPoints() { - return mStyles.drawDataPoints; - } - - /** - * flag whether the data points are highlighted as - * a visible point. - * - * @param drawDataPoints flag whether the data points are highlighted - * @see #setDataPointsRadius(float) - */ - public void setDrawDataPoints(boolean drawDataPoints) { - mStyles.drawDataPoints = drawDataPoints; - } - - /** - * @return the radius for the data points. - * @see #setDrawDataPoints(boolean) - */ - public float getDataPointsRadius() { - return mStyles.dataPointsRadius; - } - - /** - * @param dataPointsRadius the radius for the data points. - * @see #setDrawDataPoints(boolean) - */ - public void setDataPointsRadius(float dataPointsRadius) { - mStyles.dataPointsRadius = dataPointsRadius; - } - - /** - * @return the background color for the filling under - * the line. - * @see #setDrawBackground(boolean) - */ - public int getBackgroundColor() { - return mStyles.backgroundColor; - } - - /** - * @param backgroundColor the background color for the filling under - * the line. - * @see #setDrawBackground(boolean) - */ - public void setBackgroundColor(int backgroundColor) { - mStyles.backgroundColor = backgroundColor; - } - - /** - * custom paint that can be used. - * this will ignore the thickness and color styles. - * - * @param customPaint the custom paint to be used for rendering the line - */ - public void setCustomPaint(Paint customPaint) { - this.mCustomPaint = customPaint; - } -} diff --git a/core/main/src/main/java/info/nightscout/core/graph/data/AreaGraphSeries.kt b/core/main/src/main/java/info/nightscout/core/graph/data/AreaGraphSeries.kt new file mode 100644 index 0000000000..381289f1c2 --- /dev/null +++ b/core/main/src/main/java/info/nightscout/core/graph/data/AreaGraphSeries.kt @@ -0,0 +1,412 @@ +/** + * GraphView + * Copyright (C) 2014 Jonas Gehring + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, + * with the "Linking Exception", which can be found at the license.txt + * file in this program. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * with the "Linking Exception" along with this program; if not, + * write to the author Jonas Gehring @gmail.com>. + */ +package info.nightscout.core.graph.data + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import com.jjoe64.graphview.GraphView +import com.jjoe64.graphview.series.BaseSeries + +/** + * Series to plot the data as line. + * The line can be styled with many options. + * + * @author jjoe64 + */ +@Suppress("unused") class AreaGraphSeries : BaseSeries { + + /** + * wrapped styles regarding the line + */ + private class Styles { + + /** + * the thickness of the line. + * This option will be ignored if you are + * using a custom paint via [.setCustomPaint] + */ + var thickness = 5 + + /** + * flag whether the area under the line to the bottom + * of the viewport will be filled with a + * specific background color. + * + * @see .backgroundColor + */ + var drawBackground = false + + /** + * flag whether the data points are highlighted as + * a visible point. + * + * @see .dataPointsRadius + */ + var drawDataPoints = false + + /** + * the radius for the data points. + * + * @see .drawDataPoints + */ + var dataPointsRadius = 10f + + /** + * the background color for the filling under + * the line. + * + * @see .drawBackground + */ + var backgroundColor = Color.argb(100, 172, 218, 255) + } + + /** + * wrapped styles + */ + private lateinit var mStyles: Styles + + /** + * internal paint object + */ + private lateinit var mPaint: Paint + + /** + * paint for the background + */ + private lateinit var mPaintBackground: Paint + + /** + * path for the background filling + */ + private lateinit var mPathBackground: Path + + /** + * path to the line + */ + private lateinit var mPath: Path + private lateinit var mSecondPath: Path + + /** + * custom paint that can be used. + * this will ignore the thickness and color styles. + */ + private var mCustomPaint: Paint? = null + + /** + * creates a series without data + */ + @Suppress("unused") + constructor() { + init() + } + + /** + * creates a series with data + * + * @param data data points + */ + constructor(data: Array?) : super(data) { + init() + } + + /** + * do the initialization + * creates internal objects + */ + private fun init() { + mStyles = Styles() + mPaint = Paint() + mPaint.strokeCap = Paint.Cap.ROUND + mPaint.style = Paint.Style.STROKE + mPaintBackground = Paint() + mPathBackground = Path() + mPath = Path() + mSecondPath = Path() + } + + /** + * plots the series + * draws the line and the background + * + * @param graphView graphview + * @param canvas canvas + * @param isSecondScale flag if it is the second scale + */ + override fun draw(graphView: GraphView, canvas: Canvas, isSecondScale: Boolean) { + resetDataPoints() + + // get data + val maxX = graphView.viewport.getMaxX(false) + val minX = graphView.viewport.getMinX(false) + val maxY: Double + val minY: Double + if (isSecondScale) { + maxY = graphView.secondScale.maxY + minY = graphView.secondScale.minY + } else { + maxY = graphView.viewport.getMaxY(false) + minY = graphView.viewport.getMinY(false) + } + val values = getValues(minX, maxX) + + // draw background + var lastEndY1: Double + var lastEndY2: Double + var lastEndX: Double + + // draw data + mPaint.strokeWidth = mStyles.thickness.toFloat() + mPaint.color = color + mPaintBackground.color = mStyles.backgroundColor + val paint = mCustomPaint ?: mPaint + if (mStyles.drawBackground) { + mPathBackground.reset() + } + val diffY = maxY - minY + val diffX = maxX - minX + val graphHeight = graphView.graphContentHeight.toFloat() + val graphWidth = graphView.graphContentWidth.toFloat() + val graphLeft = graphView.graphContentLeft.toFloat() + val graphTop = graphView.graphContentTop.toFloat() + lastEndY1 = 0.0 + lastEndY2 = 0.0 + lastEndX = 0.0 + var i = 0 + while (values.hasNext()) { + val value = values.next() ?: break + val valY1 = value.y - minY + val ratY1 = valY1 / diffY + var y1 = graphHeight * ratY1 + val valY2 = value.y2 - minY + val ratY2 = valY2 / diffY + var y2 = graphHeight * ratY2 + val valX = value.x - minX + val ratX = valX / diffX + var x = graphWidth * ratX + val orgX = x + val orgY1 = y1 + val orgY2 = y2 + @Suppress("ControlFlowWithEmptyBody") + if (i > 0) { + // overdraw + if (x > graphWidth) { // end right + val b = (graphWidth - lastEndX) * (y1 - lastEndY1) / (x - lastEndX) + y1 = lastEndY1 + b + x = graphWidth.toDouble() + } + if (x > graphWidth) { // end right + val b = (graphWidth - lastEndX) * (y2 - lastEndY2) / (x - lastEndX) + y2 = lastEndY2 + b + x = graphWidth.toDouble() + } + if (y1 < 0) { // end bottom + val b = (0 - lastEndY1) * (x - lastEndX) / (y1 - lastEndY1) + x = lastEndX + b + y1 = 0.0 + } + if (y2 < 0) { // end bottom + val b = (0 - lastEndY2) * (x - lastEndX) / (y2 - lastEndY2) + x = lastEndX + b + y2 = 0.0 + } + if (y1 > graphHeight) { // end top + val b = (graphHeight - lastEndY1) * (x - lastEndX) / (y1 - lastEndY1) + x = lastEndX + b + y1 = graphHeight.toDouble() + } + if (y2 > graphHeight) { // end top + val b = (graphHeight - lastEndY2) * (x - lastEndX) / (y2 - lastEndY2) + x = lastEndX + b + y2 = graphHeight.toDouble() + } + if (lastEndY1 < 0) { // start bottom + val b = (0 - y1) * (x - lastEndX) / (lastEndY1 - y1) + lastEndX = x - b + lastEndY1 = 0.0 + } + if (lastEndY2 < 0) { // start bottom + val b = (0 - y2) * (x - lastEndX) / (lastEndY2 - y2) + lastEndX = x - b + lastEndY2 = 0.0 + } + if (lastEndX < 0) { // start left + val b = (0 - x) * (y1 - lastEndY1) / (lastEndX - x) + lastEndY1 = y1 - b + lastEndX = 0.0 + } + if (lastEndY1 > graphHeight) { // start top + val b = (graphHeight - y1) * (x - lastEndX) / (lastEndY1 - y1) + lastEndX = x - b + lastEndY1 = graphHeight.toDouble() + } + if (lastEndY2 > graphHeight) { // start top + val b = (graphHeight - y2) * (x - lastEndX) / (lastEndY2 - y2) + lastEndX = x - b + lastEndY2 = graphHeight.toDouble() + } + val startX = lastEndX.toFloat() + (graphLeft + 1) + val startY1 = (graphTop - lastEndY1).toFloat() + graphHeight + val startY2 = (graphTop - lastEndY2).toFloat() + graphHeight + val endX = x.toFloat() + (graphLeft + 1) + val endY1 = (graphTop - y1).toFloat() + graphHeight + val endY2 = (graphTop - y2).toFloat() + graphHeight + + // draw data point + if (mStyles.drawDataPoints) { + //fix: last value was not drawn. Draw here now the end values + canvas.drawCircle(endX, endY1, mStyles.dataPointsRadius, mPaint) + canvas.drawCircle(endX, endY2, mStyles.dataPointsRadius, mPaint) + } + registerDataPoint(endX, endY1, value) + registerDataPoint(endX, endY2, value) + mPath.reset() + mSecondPath.reset() + mPath.moveTo(startX, startY1) + mSecondPath.moveTo(startX, startY2) + mPath.lineTo(endX, endY1) + mSecondPath.lineTo(endX, endY2) + canvas.drawPath(mPath, paint) + canvas.drawPath(mSecondPath, paint) + if (mStyles.drawBackground) { + canvas.drawRect(startX, startY2, endX, endY1, mPaintBackground) + } + } else if (mStyles.drawDataPoints) { + //fix: last value not drawn as datapoint. Draw first point here, and then on every step the end values (above) + //float first_X = (float) x + (graphLeft + 1); + //float first_Y = (float) (graphTop - y) + graphHeight; + // canvas.drawCircle(first_X, first_Y, dataPointsRadius, mPaint); + } + lastEndY1 = orgY1 + lastEndY2 = orgY2 + lastEndX = orgX + i++ + } + + /* + if (mStyles.drawBackground) { + // end / close path + mPathBackground.lineTo((float) lastUsedEndX, graphHeight + graphTop); + mPathBackground.lineTo(firstX, graphHeight + graphTop); + mPathBackground.close(); + canvas.drawPath(mPathBackground, mPaintBackground); + } +*/ + } + + var thickness: Int + /** + * the thickness of the line. + * This option will be ignored if you are + * using a custom paint via [.setCustomPaint] + * + * @return the thickness of the line + */ + get() = mStyles.thickness + /** + * the thickness of the line. + * This option will be ignored if you are + * using a custom paint via [.setCustomPaint] + * + * @param thickness thickness of the line + */ + set(thickness) { + mStyles.thickness = thickness + } + var isDrawBackground: Boolean + /** + * flag whether the area under the line to the bottom + * of the viewport will be filled with a + * specific background color. + * + * @return whether the background will be drawn + * @see .getBackgroundColor + */ + get() = mStyles.drawBackground + /** + * flag whether the area under the line to the bottom + * of the viewport will be filled with a + * specific background color. + * + * @param drawBackground whether the background will be drawn + * @see .setBackgroundColor + */ + set(drawBackground) { + mStyles.drawBackground = drawBackground + } + var isDrawDataPoints: Boolean + /** + * flag whether the data points are highlighted as + * a visible point. + * + * @return flag whether the data points are highlighted + * @see .setDataPointsRadius + */ + get() = mStyles.drawDataPoints + /** + * flag whether the data points are highlighted as + * a visible point. + * + * @param drawDataPoints flag whether the data points are highlighted + * @see .setDataPointsRadius + */ + set(drawDataPoints) { + mStyles.drawDataPoints = drawDataPoints + } + var dataPointsRadius: Float + /** + * @return the radius for the data points. + * @see .setDrawDataPoints + */ + get() = mStyles.dataPointsRadius + /** + * @param dataPointsRadius the radius for the data points. + * @see .setDrawDataPoints + */ + set(dataPointsRadius) { + mStyles.dataPointsRadius = dataPointsRadius + } + var backgroundColor: Int + /** + * @return the background color for the filling under + * the line. + * @see .setDrawBackground + */ + get() = mStyles.backgroundColor + /** + * @param backgroundColor the background color for the filling under + * the line. + * @see .setDrawBackground + */ + set(backgroundColor) { + mStyles.backgroundColor = backgroundColor + } + + /** + * custom paint that can be used. + * this will ignore the thickness and color styles. + * + * @param customPaint the custom paint to be used for rendering the line + */ + fun setCustomPaint(customPaint: Paint?) { + mCustomPaint = customPaint + } +} diff --git a/core/main/src/main/java/info/nightscout/core/graph/data/FixedLineGraphSeries.java b/core/main/src/main/java/info/nightscout/core/graph/data/FixedLineGraphSeries.java deleted file mode 100644 index 60b530c57b..0000000000 --- a/core/main/src/main/java/info/nightscout/core/graph/data/FixedLineGraphSeries.java +++ /dev/null @@ -1,392 +0,0 @@ -package info.nightscout.core.graph.data; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; - -import com.jjoe64.graphview.GraphView; -import com.jjoe64.graphview.series.BaseSeries; -import com.jjoe64.graphview.series.DataPointInterface; - -import java.util.Iterator; - -/** - * Series to plot the data as line. - * The line can be styled with many options. - * - * @author jjoe64 - */ -public class FixedLineGraphSeries extends BaseSeries { - /** - * wrapped styles regarding the line - */ - private static final class Styles { - /** - * the thickness of the line. - * This option will be ignored if you are - * using a custom paint via {@link #setCustomPaint(android.graphics.Paint)} - */ - private int thickness = 5; - - /** - * flag whether the area under the line to the bottom - * of the viewport will be filled with a - * specific background color. - * - * @see #backgroundColor - */ - private boolean drawBackground = false; - - /** - * flag whether the data points are highlighted as - * a visible point. - * - * @see #dataPointsRadius - */ - private boolean drawDataPoints = false; - - /** - * the radius for the data points. - * - * @see #drawDataPoints - */ - private float dataPointsRadius = 10f; - - /** - * the background color for the filling under - * the line. - * - * @see #drawBackground - */ - private int backgroundColor = Color.argb(100, 172, 218, 255); - } - - /** - * wrapped styles - */ - private Styles mStyles; - - /** - * internal paint object - */ - private Paint mPaint; - - /** - * paint for the background - */ - private Paint mPaintBackground; - - /** - * path for the background filling - */ - private Path mPathBackground; - - /** - * path to the line - */ - private Path mPath; - - /** - * custom paint that can be used. - * this will ignore the thickness and color styles. - */ - private Paint mCustomPaint; - - /** - * creates a series without data - */ - @SuppressWarnings("unused") public FixedLineGraphSeries() { - init(); - } - - /** - * creates a series with data - * - * @param data data points - */ - public FixedLineGraphSeries(E[] data) { - super(data); - init(); - } - - /** - * do the initialization - * creates internal objects - */ - protected void init() { - mStyles = new Styles(); - mPaint = new Paint(); - mPaint.setStrokeCap(Paint.Cap.ROUND); - mPaint.setStyle(Paint.Style.STROKE); - mPaintBackground = new Paint(); - - mPathBackground = new Path(); - mPath = new Path(); - } - - /** - * plots the series - * draws the line and the background - * - * @param graphView graphview - * @param canvas canvas - * @param isSecondScale flag if it is the second scale - */ - @Override - public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { - resetDataPoints(); - - // get data - double maxX = graphView.getViewport().getMaxX(false); - double minX = graphView.getViewport().getMinX(false); - - double maxY; - double minY; - if (isSecondScale) { - maxY = graphView.getSecondScale().getMaxY(); - minY = graphView.getSecondScale().getMinY(); - } else { - maxY = graphView.getViewport().getMaxY(false); - minY = graphView.getViewport().getMinY(false); - } - - Iterator values = getValues(minX, maxX); - - // draw background - double lastEndY; - double lastEndX; - - // draw data - mPaint.setStrokeWidth(mStyles.thickness); - mPaint.setColor(getColor()); - mPaintBackground.setColor(mStyles.backgroundColor); - - Paint paint; - if (mCustomPaint != null) { - paint = mCustomPaint; - } else { - paint = mPaint; - } - - if (mStyles.drawBackground) { - mPathBackground.reset(); - } - - double diffY = maxY - minY; - double diffX = maxX - minX; - - float graphHeight = graphView.getGraphContentHeight(); - float graphWidth = graphView.getGraphContentWidth(); - float graphLeft = graphView.getGraphContentLeft(); - float graphTop = graphView.getGraphContentTop(); - - lastEndY = 0; - lastEndX = 0; - double lastUsedEndX = 0; - float firstX = 0; - int i=0; - while (values.hasNext()) { - E value = values.next(); - - double valY = value.getY() - minY; - double ratY = valY / diffY; - double y = graphHeight * ratY; - - double valX = value.getX() - minX; - double ratX = valX / diffX; - double x = graphWidth * ratX; - - double orgX = x; - double orgY = y; - - if (i > 0) { - // overdraw - if (x > graphWidth) { // end right - double b = ((graphWidth - lastEndX) * (y - lastEndY)/(x - lastEndX)); - y = lastEndY+b; - x = graphWidth; - } - if (y < 0) { // end bottom - double b = ((0 - lastEndY) * (x - lastEndX)/(y - lastEndY)); - x = lastEndX+b; - y = 0; - } - if (y > graphHeight) { // end top - double b = ((graphHeight - lastEndY) * (x - lastEndX)/(y - lastEndY)); - x = lastEndX+b; - y = graphHeight; - } - if (lastEndY < 0) { // start bottom - double b = ((0 - y) * (x - lastEndX)/(lastEndY - y)); - lastEndX = x-b; - lastEndY = 0; - } - if (lastEndX < 0) { // start left - double b = ((0 - x) * (y - lastEndY)/(lastEndX - x)); - lastEndY = y-b; - lastEndX = 0; - } - if (lastEndY > graphHeight) { // start top - double b = ((graphHeight - y) * (x - lastEndX)/(lastEndY - y)); - lastEndX = x-b; - lastEndY = graphHeight; - } - - float startX = (float) lastEndX + (graphLeft + 1); - float startY = (float) (graphTop - lastEndY) + graphHeight; - float endX = (float) x + (graphLeft + 1); - float endY = (float) (graphTop - y) + graphHeight; - - // draw data point - if (mStyles.drawDataPoints) { - //fix: last value was not drawn. Draw here now the end values - canvas.drawCircle(endX, endY, mStyles.dataPointsRadius, mPaint); - } - registerDataPoint(endX, endY, value); - - mPath.reset(); - mPath.moveTo(startX, startY); - mPath.lineTo(endX, endY); - canvas.drawPath(mPath, paint); - if (mStyles.drawBackground) { - if (i==1) { - firstX = startX; - mPathBackground.moveTo(startX, startY); - } - mPathBackground.lineTo(endX, endY); - } - lastUsedEndX = endX; - } else if (mStyles.drawDataPoints) { - //fix: last value not drawn as datapoint. Draw first point here, and then on every step the end values (above) - // float first_X = (float) x + (graphLeft + 1); - // float first_Y = (float) (graphTop - y) + graphHeight; - // canvas.drawCircle(first_X, first_Y, dataPointsRadius, mPaint); - } - lastEndY = orgY; - lastEndX = orgX; - i++; - } - - if (mStyles.drawBackground) { - // end / close path - mPathBackground.lineTo((float) lastUsedEndX, (float) (graphTop - (-minY / diffY * graphHeight)) + graphHeight); - mPathBackground.lineTo(firstX, (float) (graphTop - (-minY / diffY * graphHeight)) + graphHeight); - mPathBackground.close(); - canvas.drawPath(mPathBackground, mPaintBackground); - } - - } - - /** - * the thickness of the line. - * This option will be ignored if you are - * using a custom paint via {@link #setCustomPaint(android.graphics.Paint)} - * - * @return the thickness of the line - */ - @SuppressWarnings("unused") public int getThickness() { - return mStyles.thickness; - } - - /** - * the thickness of the line. - * This option will be ignored if you are - * using a custom paint via {@link #setCustomPaint(android.graphics.Paint)} - * - * @param thickness thickness of the line - */ - public void setThickness(int thickness) { - mStyles.thickness = thickness; - } - - /** - * flag whether the area under the line to the bottom - * of the viewport will be filled with a - * specific background color. - * - * @return whether the background will be drawn - * @see #getBackgroundColor() - */ - @SuppressWarnings("unused") public boolean isDrawBackground() { - return mStyles.drawBackground; - } - - /** - * flag whether the area under the line to the bottom - * of the viewport will be filled with a - * specific background color. - * - * @param drawBackground whether the background will be drawn - * @see #setBackgroundColor(int) - */ - public void setDrawBackground(boolean drawBackground) { - mStyles.drawBackground = drawBackground; - } - - /** - * flag whether the data points are highlighted as - * a visible point. - * - * @return flag whether the data points are highlighted - * @see #setDataPointsRadius(float) - */ - @SuppressWarnings("unused") public boolean isDrawDataPoints() { - return mStyles.drawDataPoints; - } - - /** - * flag whether the data points are highlighted as - * a visible point. - * - * @param drawDataPoints flag whether the data points are highlighted - * @see #setDataPointsRadius(float) - */ - @SuppressWarnings("unused") public void setDrawDataPoints(boolean drawDataPoints) { - mStyles.drawDataPoints = drawDataPoints; - } - - /** - * @return the radius for the data points. - * @see #setDrawDataPoints(boolean) - */ - @SuppressWarnings("unused") public float getDataPointsRadius() { - return mStyles.dataPointsRadius; - } - - /** - * @param dataPointsRadius the radius for the data points. - * @see #setDrawDataPoints(boolean) - */ - @SuppressWarnings("unused") public void setDataPointsRadius(float dataPointsRadius) { - mStyles.dataPointsRadius = dataPointsRadius; - } - - /** - * @return the background color for the filling under - * the line. - * @see #setDrawBackground(boolean) - */ - @SuppressWarnings("unused") public int getBackgroundColor() { - return mStyles.backgroundColor; - } - - /** - * @param backgroundColor the background color for the filling under - * the line. - * @see #setDrawBackground(boolean) - */ - public void setBackgroundColor(int backgroundColor) { - mStyles.backgroundColor = backgroundColor; - } - - /** - * custom paint that can be used. - * this will ignore the thickness and color styles. - * - * @param customPaint the custom paint to be used for rendering the line - */ - public void setCustomPaint(Paint customPaint) { - this.mCustomPaint = customPaint; - } -} diff --git a/core/main/src/main/java/info/nightscout/core/graph/data/FixedLineGraphSeries.kt b/core/main/src/main/java/info/nightscout/core/graph/data/FixedLineGraphSeries.kt new file mode 100644 index 0000000000..5b978b32d2 --- /dev/null +++ b/core/main/src/main/java/info/nightscout/core/graph/data/FixedLineGraphSeries.kt @@ -0,0 +1,356 @@ +package info.nightscout.core.graph.data + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import com.jjoe64.graphview.GraphView +import com.jjoe64.graphview.series.BaseSeries +import com.jjoe64.graphview.series.DataPointInterface + +/** + * Series to plot the data as line. + * The line can be styled with many options. + * + * @author jjoe64 + */ +@Suppress("unused") class FixedLineGraphSeries : BaseSeries { + + /** + * wrapped styles regarding the line + */ + private class Styles { + + /** + * the thickness of the line. + * This option will be ignored if you are + * using a custom paint via [.setCustomPaint] + */ + var thickness = 5 + + /** + * flag whether the area under the line to the bottom + * of the viewport will be filled with a + * specific background color. + * + * @see .backgroundColor + */ + var drawBackground = false + + /** + * flag whether the data points are highlighted as + * a visible point. + * + * @see .dataPointsRadius + */ + var drawDataPoints = false + + /** + * the radius for the data points. + * + * @see .drawDataPoints + */ + var dataPointsRadius = 10f + + /** + * the background color for the filling under + * the line. + * + * @see .drawBackground + */ + var backgroundColor = Color.argb(100, 172, 218, 255) + } + + /** + * wrapped styles + */ + private lateinit var mStyles: Styles + + /** + * internal paint object + */ + private lateinit var mPaint: Paint + + /** + * paint for the background + */ + private lateinit var mPaintBackground: Paint + + /** + * path for the background filling + */ + private lateinit var mPathBackground: Path + + /** + * path to the line + */ + private lateinit var mPath: Path + + /** + * custom paint that can be used. + * this will ignore the thickness and color styles. + */ + private var mCustomPaint: Paint? = null + + /** + * creates a series without data + */ + @Suppress("unused") + constructor() { + init() + } + + /** + * creates a series with data + * + * @param data data points + */ + constructor(data: Array?) : super(data) { + init() + } + + /** + * do the initialization + * creates internal objects + */ + private fun init() { + mStyles = Styles() + mPaint = Paint() + mPaint.strokeCap = Paint.Cap.ROUND + mPaint.style = Paint.Style.STROKE + mPaintBackground = Paint() + mPathBackground = Path() + mPath = Path() + } + + /** + * plots the series + * draws the line and the background + * + * @param graphView graphview + * @param canvas canvas + * @param isSecondScale flag if it is the second scale + */ + override fun draw(graphView: GraphView, canvas: Canvas, isSecondScale: Boolean) { + resetDataPoints() + + // get data + val maxX = graphView.viewport.getMaxX(false) + val minX = graphView.viewport.getMinX(false) + val maxY: Double + val minY: Double + if (isSecondScale) { + maxY = graphView.secondScale.maxY + minY = graphView.secondScale.minY + } else { + maxY = graphView.viewport.getMaxY(false) + minY = graphView.viewport.getMinY(false) + } + val values = getValues(minX, maxX) + + // draw background + var lastEndY: Double + var lastEndX: Double + + // draw data + mPaint.strokeWidth = mStyles.thickness.toFloat() + mPaint.color = color + mPaintBackground.color = mStyles.backgroundColor + val paint = mCustomPaint ?: mPaint + if (mStyles.drawBackground) { + mPathBackground.reset() + } + val diffY = maxY - minY + val diffX = maxX - minX + val graphHeight = graphView.graphContentHeight.toFloat() + val graphWidth = graphView.graphContentWidth.toFloat() + val graphLeft = graphView.graphContentLeft.toFloat() + val graphTop = graphView.graphContentTop.toFloat() + lastEndY = 0.0 + lastEndX = 0.0 + var lastUsedEndX = 0.0 + var firstX = 0f + var i = 0 + while (values.hasNext()) { + val value = values.next() ?: break + val valY = value.y - minY + val ratY = valY / diffY + var y = graphHeight * ratY + val valX = value.x - minX + val ratX = valX / diffX + var x = graphWidth * ratX + val orgX = x + val orgY = y + @Suppress("ControlFlowWithEmptyBody") + if (i > 0) { + // overdraw + if (x > graphWidth) { // end right + val b = (graphWidth - lastEndX) * (y - lastEndY) / (x - lastEndX) + y = lastEndY + b + x = graphWidth.toDouble() + } + if (y < 0) { // end bottom + val b = (0 - lastEndY) * (x - lastEndX) / (y - lastEndY) + x = lastEndX + b + y = 0.0 + } + if (y > graphHeight) { // end top + val b = (graphHeight - lastEndY) * (x - lastEndX) / (y - lastEndY) + x = lastEndX + b + y = graphHeight.toDouble() + } + if (lastEndY < 0) { // start bottom + val b = (0 - y) * (x - lastEndX) / (lastEndY - y) + lastEndX = x - b + lastEndY = 0.0 + } + if (lastEndX < 0) { // start left + val b = (0 - x) * (y - lastEndY) / (lastEndX - x) + lastEndY = y - b + lastEndX = 0.0 + } + if (lastEndY > graphHeight) { // start top + val b = (graphHeight - y) * (x - lastEndX) / (lastEndY - y) + lastEndX = x - b + lastEndY = graphHeight.toDouble() + } + val startX = lastEndX.toFloat() + (graphLeft + 1) + val startY = (graphTop - lastEndY).toFloat() + graphHeight + val endX = x.toFloat() + (graphLeft + 1) + val endY = (graphTop - y).toFloat() + graphHeight + + // draw data point + if (mStyles.drawDataPoints) { + //fix: last value was not drawn. Draw here now the end values + canvas.drawCircle(endX, endY, mStyles.dataPointsRadius, mPaint) + } + registerDataPoint(endX, endY, value) + mPath.reset() + mPath.moveTo(startX, startY) + mPath.lineTo(endX, endY) + canvas.drawPath(mPath, paint) + if (mStyles.drawBackground) { + if (i == 1) { + firstX = startX + mPathBackground.moveTo(startX, startY) + } + mPathBackground.lineTo(endX, endY) + } + lastUsedEndX = endX.toDouble() + } else if (mStyles.drawDataPoints) { + //fix: last value not drawn as datapoint. Draw first point here, and then on every step the end values (above) + // float first_X = (float) x + (graphLeft + 1); + // float first_Y = (float) (graphTop - y) + graphHeight; + // canvas.drawCircle(first_X, first_Y, dataPointsRadius, mPaint); + } + lastEndY = orgY + lastEndX = orgX + i++ + } + if (mStyles.drawBackground) { + // end / close path + mPathBackground.lineTo(lastUsedEndX.toFloat(), (graphTop - -minY / diffY * graphHeight).toFloat() + graphHeight) + mPathBackground.lineTo(firstX, (graphTop - -minY / diffY * graphHeight).toFloat() + graphHeight) + mPathBackground.close() + canvas.drawPath(mPathBackground, mPaintBackground) + } + } + + var thickness: Int + /** + * the thickness of the line. + * This option will be ignored if you are + * using a custom paint via [.setCustomPaint] + * + * @return the thickness of the line + */ + get() = mStyles.thickness + /** + * the thickness of the line. + * This option will be ignored if you are + * using a custom paint via [.setCustomPaint] + * + * @param thickness thickness of the line + */ + set(thickness) { + mStyles.thickness = thickness + } + var isDrawBackground: Boolean + /** + * flag whether the area under the line to the bottom + * of the viewport will be filled with a + * specific background color. + * + * @return whether the background will be drawn + * @see .getBackgroundColor + */ + get() = mStyles.drawBackground + /** + * flag whether the area under the line to the bottom + * of the viewport will be filled with a + * specific background color. + * + * @param drawBackground whether the background will be drawn + * @see .setBackgroundColor + */ + set(drawBackground) { + mStyles.drawBackground = drawBackground + } + var isDrawDataPoints: Boolean + /** + * flag whether the data points are highlighted as + * a visible point. + * + * @return flag whether the data points are highlighted + * @see .setDataPointsRadius + */ + get() = mStyles.drawDataPoints + /** + * flag whether the data points are highlighted as + * a visible point. + * + * @param drawDataPoints flag whether the data points are highlighted + * @see .setDataPointsRadius + */ + set(drawDataPoints) { + mStyles.drawDataPoints = drawDataPoints + } + var dataPointsRadius: Float + /** + * @return the radius for the data points. + * @see .setDrawDataPoints + */ + get() = mStyles.dataPointsRadius + /** + * @param dataPointsRadius the radius for the data points. + * @see .setDrawDataPoints + */ + set(dataPointsRadius) { + mStyles.dataPointsRadius = dataPointsRadius + } + var backgroundColor: Int + /** + * @return the background color for the filling under + * the line. + * @see .setDrawBackground + */ + get() = mStyles.backgroundColor + /** + * @param backgroundColor the background color for the filling under + * the line. + * @see .setDrawBackground + */ + set(backgroundColor) { + mStyles.backgroundColor = backgroundColor + } + + /** + * custom paint that can be used. + * this will ignore the thickness and color styles. + * + * @param customPaint the custom paint to be used for rendering the line + */ + fun setCustomPaint(customPaint: Paint?) { + mCustomPaint = customPaint + } +} diff --git a/core/main/src/main/java/info/nightscout/core/graph/data/PointsWithLabelGraphSeries.java b/core/main/src/main/java/info/nightscout/core/graph/data/PointsWithLabelGraphSeries.java deleted file mode 100644 index c0510e4a10..0000000000 --- a/core/main/src/main/java/info/nightscout/core/graph/data/PointsWithLabelGraphSeries.java +++ /dev/null @@ -1,382 +0,0 @@ -package info.nightscout.core.graph.data; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.Point; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; - -import androidx.core.content.ContextCompat; - -import com.jjoe64.graphview.GraphView; -import com.jjoe64.graphview.series.BaseSeries; - -import java.util.Iterator; - -import info.nightscout.core.main.R; - -/** - * Series that plots the data as points. - * The points can be different shapes or a - * complete custom drawing. - * - * @author jjoe64 - */ -public class PointsWithLabelGraphSeries extends BaseSeries { - // Default spSize - int spSize = 14; - - /** - * choose a predefined shape to render for - * each data point. - * You can also render a custom drawing via {@link com.jjoe64.graphview.series.PointsGraphSeries.CustomShape} - */ - public enum Shape { - BG, - PREDICTION, - TRIANGLE, - RECTANGLE, - BOLUS, - CARBS, - SMB, - EXTENDEDBOLUS, - PROFILE, - MBG, - BGCHECK, - ANNOUNCEMENT, - OPENAPS_OFFLINE, - EXERCISE, - GENERAL, - GENERAL_WITH_DURATION, - COB_FAIL_OVER, - IOB_PREDICTION, - BUCKETED_BG, - HEARTRATE, - } - - /** - * internal paint object - */ - private Paint mPaint; - - /** - * creates the series without data - */ - public PointsWithLabelGraphSeries() { - init(); - } - - /** - * creates the series with data - * - * @param data dataPoints - */ - public PointsWithLabelGraphSeries(E[] data) { - super(data); - init(); - } - - /** - * init the internal objects - * set the defaults - */ - protected void init() { - mPaint = new Paint(); - mPaint.setStrokeCap(Paint.Cap.ROUND); - } - - /** - * plot the data to the viewport - * - * @param graphView graphview - * @param canvas canvas to draw on - * @param isSecondScale whether it is the second scale - */ - @SuppressWarnings({"deprecation"}) - @Override - public void draw(GraphView graphView, Canvas canvas, boolean isSecondScale) { - // Convert the sp to pixels - float scaledTextSize = spSize * graphView.getContext().getResources().getDisplayMetrics().scaledDensity; - float scaledPxSize = graphView.getContext().getResources().getDisplayMetrics().scaledDensity * 3f; - resetDataPoints(); - - // get data - double maxX = graphView.getViewport().getMaxX(false); - double minX = graphView.getViewport().getMinX(false); - - double maxY; - double minY; - if (isSecondScale) { - maxY = graphView.getSecondScale().getMaxY(); - minY = graphView.getSecondScale().getMinY(); - } else { - maxY = graphView.getViewport().getMaxY(false); - minY = graphView.getViewport().getMinY(false); - } - - Iterator values = getValues(minX, maxX); - - // draw background - // draw data - - double diffY = maxY - minY; - double diffX = maxX - minX; - - float graphHeight = graphView.getGraphContentHeight(); - float graphWidth = graphView.getGraphContentWidth(); - float graphLeft = graphView.getGraphContentLeft(); - float graphTop = graphView.getGraphContentTop(); - - float scaleX = (float) (graphWidth / diffX); - - while (values.hasNext()) { - E value = values.next(); - - mPaint.setColor(value.color(graphView.getContext())); - - double valY = value.getY() - minY; - double ratY = valY / diffY; - double y = graphHeight * ratY; - - double valX = value.getX() - minX; - double ratX = valX / diffX; - double x = graphWidth * ratX; - - // overdraw - boolean overdraw = x > graphWidth; - // end right - if (y < 0) { // end bottom - overdraw = true; - } - if (y > graphHeight) { // end top - overdraw = true; - } - - long duration = value.getDuration(); - float endWithDuration = (float) (x + duration * scaleX + graphLeft + 1); - // cut off to graph start if needed - if (x < 0 && endWithDuration > 0) { - x = 0; - } - - /* Fix a bug that continue to show the DOT after Y axis */ - if (x < 0) { - overdraw = true; - } - - float endX = (float) x + (graphLeft + 1); - float endY = (float) (graphTop - y) + graphHeight; - registerDataPoint(endX, endY, value); - - float xPlusLength = 0; - if (duration > 0) { - xPlusLength = Math.min(endWithDuration, graphLeft + graphWidth); - } - - // draw data point - if (!overdraw) { - if (value.getShape() == Shape.BG || value.getShape() == Shape.COB_FAIL_OVER) { - mPaint.setStyle(value.getPaintStyle()); - mPaint.setStrokeWidth(0); - canvas.drawCircle(endX, endY, value.getSize() * scaledPxSize, mPaint); - } else if (value.getShape() == Shape.BG || value.getShape() == Shape.IOB_PREDICTION || value.getShape() == Shape.BUCKETED_BG) { - mPaint.setColor(value.color(graphView.getContext())); - mPaint.setStyle(value.getPaintStyle()); - mPaint.setStrokeWidth(0); - canvas.drawCircle(endX, endY, value.getSize() * scaledPxSize, mPaint); - } else if (value.getShape() == Shape.PREDICTION) { - mPaint.setColor(value.color(graphView.getContext())); - mPaint.setStyle(value.getPaintStyle()); - mPaint.setStrokeWidth(0); - canvas.drawCircle(endX, endY, scaledPxSize, mPaint); - mPaint.setStyle(value.getPaintStyle()); - mPaint.setStrokeWidth(0); - canvas.drawCircle(endX, endY, scaledPxSize / 3, mPaint); - } else if (value.getShape() == Shape.RECTANGLE) { - canvas.drawRect(endX - scaledPxSize, endY - scaledPxSize, endX + scaledPxSize, endY + scaledPxSize, mPaint); - } else if (value.getShape() == Shape.TRIANGLE) { - mPaint.setStrokeWidth(0); - Point[] points = new Point[3]; - points[0] = new Point((int) endX, (int) (endY - scaledPxSize)); - points[1] = new Point((int) (endX + scaledPxSize), (int) (endY + scaledPxSize * 0.67)); - points[2] = new Point((int) (endX - scaledPxSize), (int) (endY + scaledPxSize * 0.67)); - drawArrows(points, canvas, mPaint); - } else if (value.getShape() == Shape.BOLUS) { - mPaint.setStrokeWidth(0); - Point[] points = new Point[3]; - points[0] = new Point((int) endX, (int) (endY - scaledPxSize)); - points[1] = new Point((int) (endX + scaledPxSize), (int) (endY + scaledPxSize * 0.67)); - points[2] = new Point((int) (endX - scaledPxSize), (int) (endY + scaledPxSize * 0.67)); - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - drawArrows(points, canvas, mPaint); - if (!value.getLabel().isEmpty()) - drawLabel45Right(endX, endY, value, canvas, scaledPxSize, scaledTextSize); - } else if (value.getShape() == Shape.CARBS) { - mPaint.setStrokeWidth(0); - Point[] points = new Point[3]; - points[0] = new Point((int) endX, (int) (endY - scaledPxSize)); - points[1] = new Point((int) (endX + scaledPxSize), (int) (endY + scaledPxSize * 0.67)); - points[2] = new Point((int) (endX - scaledPxSize), (int) (endY + scaledPxSize * 0.67)); - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - drawArrows(points, canvas, mPaint); - if (!value.getLabel().isEmpty()) - drawLabel45Left(endX, endY, value, canvas, scaledPxSize, scaledTextSize); - } else if (value.getShape() == Shape.SMB) { - mPaint.setStrokeWidth(2); - Point[] points = new Point[3]; - float size = value.getSize() * scaledPxSize; - points[0] = new Point((int) endX, (int) (endY - size)); - points[1] = new Point((int) (endX + size), (int) (endY + size * 0.67)); - points[2] = new Point((int) (endX - size), (int) (endY + size * 0.67)); - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - drawArrows(points, canvas, mPaint); - } else if (value.getShape() == Shape.EXTENDEDBOLUS) { - mPaint.setStrokeWidth(0); - if (!value.getLabel().isEmpty()) { - Rect bounds = new Rect((int) endX, (int) endY + 3, (int) (xPlusLength), (int) endY + 8); - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - canvas.drawRect(bounds, mPaint); - mPaint.setTextSize(scaledTextSize); - mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); - mPaint.setFakeBoldText(true); - canvas.drawText(value.getLabel(), endX, endY, mPaint); - } - } else if (value.getShape() == Shape.HEARTRATE) { - mPaint.setStrokeWidth(0); - Rect bounds = new Rect((int) endX, (int) endY - 8, (int) (xPlusLength), (int) endY + 8); - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - canvas.drawRect(bounds, mPaint); - } else if (value.getShape() == Shape.PROFILE) { - Drawable drawable = ContextCompat.getDrawable(graphView.getContext(), R.drawable.ic_ribbon_profile); - assert drawable != null; - drawable.setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); - drawable.setBounds( - (int) (endX - drawable.getIntrinsicWidth() / 2), - (int) (endY - drawable.getIntrinsicHeight() / 2), - (int) (endX + drawable.getIntrinsicWidth() / 2), - (int) (endY + drawable.getIntrinsicHeight() / 2)); - drawable.draw(canvas); - - mPaint.setTextSize(scaledTextSize * 0.8f); - mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); - mPaint.setColor(value.color(graphView.getContext())); - Rect bounds = new Rect(); - mPaint.getTextBounds(value.getLabel(), 0, value.getLabel().length(), bounds); - float px = endX - bounds.width() / 2.0f; - float py = endY + drawable.getIntrinsicHeight(); - mPaint.setStyle(Paint.Style.FILL); - canvas.drawText(value.getLabel(), px, py, mPaint); - } else if (value.getShape() == Shape.MBG) { - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeWidth(5); - canvas.drawCircle(endX, endY, scaledPxSize, mPaint); - } else if (value.getShape() == Shape.BGCHECK) { - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - mPaint.setStrokeWidth(0); - canvas.drawCircle(endX, endY, scaledPxSize, mPaint); - if (!value.getLabel().isEmpty()) { - drawLabel45Right(endX, endY, value, canvas, scaledPxSize, scaledTextSize); - } - } else if (value.getShape() == Shape.ANNOUNCEMENT) { - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - mPaint.setStrokeWidth(0); - canvas.drawCircle(endX, endY, scaledPxSize, mPaint); - if (!value.getLabel().isEmpty()) { - drawLabel45Right(endX, endY, value, canvas, scaledPxSize, scaledTextSize); - } - } else if (value.getShape() == Shape.GENERAL) { - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - mPaint.setStrokeWidth(0); - canvas.drawCircle(endX, endY, scaledPxSize, mPaint); - if (!value.getLabel().isEmpty()) { - drawLabel45Right(endX, endY, value, canvas, scaledPxSize, scaledTextSize); - } - } else if (value.getShape() == Shape.EXERCISE) { - mPaint.setStrokeWidth(0); - if (!value.getLabel().isEmpty()) { - mPaint.setStrokeWidth(0); - mPaint.setTextSize((float) (scaledTextSize * 1.2)); - mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); - Rect bounds = new Rect(); - mPaint.getTextBounds(value.getLabel(), 0, value.getLabel().length(), bounds); - mPaint.setStyle(Paint.Style.STROKE); - float py = graphTop + 20; - canvas.drawText(value.getLabel(), endX, py, mPaint); - mPaint.setStrokeWidth(5); - canvas.drawRect(endX - 3, bounds.top + py - 3, xPlusLength + 3, bounds.bottom + py + 3, mPaint); - } - } else if (value.getShape() == Shape.OPENAPS_OFFLINE && value.getDuration() != 0) { - mPaint.setStrokeWidth(0); - if (!value.getLabel().isEmpty()) { - mPaint.setStyle(Paint.Style.FILL_AND_STROKE); - mPaint.setStrokeWidth(5); - canvas.drawRect(endX - 3, graphTop, xPlusLength + 3, graphTop + graphHeight, mPaint); - } - } else if (value.getShape() == Shape.GENERAL_WITH_DURATION) { - mPaint.setStrokeWidth(0); - if (!value.getLabel().isEmpty()) { - mPaint.setStrokeWidth(0); - mPaint.setTextSize((float) (scaledTextSize * 1.5)); - mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); - Rect bounds = new Rect(); - mPaint.getTextBounds(value.getLabel(), 0, value.getLabel().length(), bounds); - mPaint.setStyle(Paint.Style.STROKE); - float py = graphTop + 80; - canvas.drawText(value.getLabel(), endX, py, mPaint); - mPaint.setStrokeWidth(5); - canvas.drawRect(endX - 3, bounds.top + py - 3, xPlusLength + 3, bounds.bottom + py + 3, mPaint); - } - } - // set values above point - } - - } - - } - - /** - * helper to render triangle - * - * @param point array with 3 coordinates - * @param canvas canvas to draw on - * @param paint paint object - */ - private void drawArrows(Point[] point, Canvas canvas, Paint paint) { - canvas.save(); - Path path = new Path(); - path.moveTo(point[0].x, point[0].y); - path.lineTo(point[1].x, point[1].y); - path.lineTo(point[2].x, point[2].y); - path.close(); - canvas.drawPath(path, paint); - canvas.restore(); - } - - void drawLabel45Right(float endX, float endY, E value, Canvas canvas, Float scaledPxSize, Float scaledTextSize) { - float py = endY - scaledPxSize; - canvas.save(); - canvas.rotate(-45, endX, py); - mPaint.setTextSize((float) (scaledTextSize * 0.8)); - mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); - mPaint.setFakeBoldText(true); - canvas.drawText(value.getLabel(), endX + scaledPxSize, py, mPaint); - canvas.restore(); - } - - void drawLabel45Left(float endX, float endY, E value, Canvas canvas, Float scaledPxSize, Float scaledTextSize) { - float py = endY + scaledPxSize; - canvas.save(); - canvas.rotate(-45, endX, py); - mPaint.setTextSize((float) (scaledTextSize * 0.8)); - mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); - mPaint.setFakeBoldText(true); - mPaint.setTextAlign(Paint.Align.RIGHT); - canvas.drawText(value.getLabel(), endX - scaledPxSize, py, mPaint); - mPaint.setTextAlign(Paint.Align.LEFT); - canvas.restore(); - } -} diff --git a/core/main/src/main/java/info/nightscout/core/graph/data/PointsWithLabelGraphSeries.kt b/core/main/src/main/java/info/nightscout/core/graph/data/PointsWithLabelGraphSeries.kt new file mode 100644 index 0000000000..40075fa492 --- /dev/null +++ b/core/main/src/main/java/info/nightscout/core/graph/data/PointsWithLabelGraphSeries.kt @@ -0,0 +1,346 @@ +package info.nightscout.core.graph.data + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Point +import android.graphics.PorterDuff +import android.graphics.Rect +import android.graphics.Typeface +import androidx.core.content.ContextCompat +import com.jjoe64.graphview.GraphView +import com.jjoe64.graphview.series.BaseSeries +import info.nightscout.core.main.R + +/** + * Series that plots the data as points. + * The points can be different shapes or a + * complete custom drawing. + * + * @author jjoe64 + */ +class PointsWithLabelGraphSeries : BaseSeries { + + // Default spSize + var spSize = 14 + + /** + * choose a predefined shape to render for + * each data point. + * You can also render a custom drawing via [com.jjoe64.graphview.series.PointsGraphSeries.CustomShape] + */ + enum class Shape { + + BG, + PREDICTION, + TRIANGLE, + RECTANGLE, + BOLUS, + CARBS, + SMB, + EXTENDEDBOLUS, + PROFILE, + MBG, + BGCHECK, + ANNOUNCEMENT, + OPENAPS_OFFLINE, + EXERCISE, + GENERAL, + GENERAL_WITH_DURATION, + COB_FAIL_OVER, + IOB_PREDICTION, + BUCKETED_BG, + HEARTRATE + } + + /** + * internal paint object + */ + private lateinit var mPaint: Paint + + /** + * creates the series without data + */ + constructor() { + init() + } + + /** + * creates the series with data + * + * @param data dataPoints + */ + constructor(data: Array?) : super(data) { + init() + } + + /** + * init the internal objects + * set the defaults + */ + protected fun init() { + mPaint = Paint() + mPaint.strokeCap = Paint.Cap.ROUND + } + + /** + * plot the data to the viewport + * + * @param graphView graphview + * @param canvas canvas to draw on + * @param isSecondScale whether it is the second scale + */ + @Suppress("deprecation") + override fun draw(graphView: GraphView, canvas: Canvas, isSecondScale: Boolean) { + // Convert the sp to pixels + val scaledTextSize = spSize * graphView.context.resources.displayMetrics.scaledDensity + val scaledPxSize = graphView.context.resources.displayMetrics.scaledDensity * 3f + resetDataPoints() + + // get data + val maxX = graphView.viewport.getMaxX(false) + val minX = graphView.viewport.getMinX(false) + val maxY: Double + val minY: Double + if (isSecondScale) { + maxY = graphView.secondScale.maxY + minY = graphView.secondScale.minY + } else { + maxY = graphView.viewport.getMaxY(false) + minY = graphView.viewport.getMinY(false) + } + val values = getValues(minX, maxX) + + // draw background + // draw data + val diffY = maxY - minY + val diffX = maxX - minX + val graphHeight = graphView.graphContentHeight.toFloat() + val graphWidth = graphView.graphContentWidth.toFloat() + val graphLeft = graphView.graphContentLeft.toFloat() + val graphTop = graphView.graphContentTop.toFloat() + val scaleX = (graphWidth / diffX).toFloat() + while (values.hasNext()) { + val value = values.next() ?: break + mPaint.color = value.color(graphView.context) + val valY = value.y - minY + val ratY = valY / diffY + val y = graphHeight * ratY + val valX = value.x - minX + val ratX = valX / diffX + var x = graphWidth * ratX + + // overdraw + var overdraw = x > graphWidth + // end right + if (y < 0) { // end bottom + overdraw = true + } + if (y > graphHeight) { // end top + overdraw = true + } + val duration = value.duration + val endWithDuration = (x + duration * scaleX + graphLeft + 1).toFloat() + // cut off to graph start if needed + if (x < 0 && endWithDuration > 0) { + x = 0.0 + } + + /* Fix a bug that continue to show the DOT after Y axis */if (x < 0) { + overdraw = true + } + val endX = x.toFloat() + (graphLeft + 1) + val endY = (graphTop - y).toFloat() + graphHeight + registerDataPoint(endX, endY, value) + var xPlusLength = 0f + if (duration > 0) { + xPlusLength = Math.min(endWithDuration, graphLeft + graphWidth) + } + + // draw data point + if (!overdraw) { + if (value.shape == Shape.BG || value.shape == Shape.COB_FAIL_OVER) { + mPaint.style = value.paintStyle + mPaint.strokeWidth = 0f + canvas.drawCircle(endX, endY, value.size * scaledPxSize, mPaint) + } else if (value.shape == Shape.BG || value.shape == Shape.IOB_PREDICTION || value.shape == Shape.BUCKETED_BG) { + mPaint.color = value.color(graphView.context) + mPaint.style = value.paintStyle + mPaint.strokeWidth = 0f + canvas.drawCircle(endX, endY, value.size * scaledPxSize, mPaint) + } else if (value.shape == Shape.PREDICTION) { + mPaint.color = value.color(graphView.context) + mPaint.style = value.paintStyle + mPaint.strokeWidth = 0f + canvas.drawCircle(endX, endY, scaledPxSize, mPaint) + mPaint.style = value.paintStyle + mPaint.strokeWidth = 0f + canvas.drawCircle(endX, endY, scaledPxSize / 3, mPaint) + } else if (value.shape == Shape.RECTANGLE) { + canvas.drawRect(endX - scaledPxSize, endY - scaledPxSize, endX + scaledPxSize, endY + scaledPxSize, mPaint) + } else if (value.shape == Shape.TRIANGLE) { + mPaint.strokeWidth = 0f + val points = arrayOf( + Point(endX.toInt(), (endY - scaledPxSize).toInt()), + Point((endX + scaledPxSize).toInt(), (endY + scaledPxSize * 0.67).toInt()), + Point((endX - scaledPxSize).toInt(), (endY + scaledPxSize * 0.67).toInt()) + ) + drawArrows(points, canvas, mPaint) + } else if (value.shape == Shape.BOLUS) { + mPaint.strokeWidth = 0f + val points = arrayOf( + Point(endX.toInt(), (endY - scaledPxSize).toInt()), + Point((endX + scaledPxSize).toInt(), (endY + scaledPxSize * 0.67).toInt()), + Point((endX - scaledPxSize).toInt(), (endY + scaledPxSize * 0.67).toInt()) + ) + mPaint.style = Paint.Style.FILL_AND_STROKE + drawArrows(points, canvas, mPaint) + if (value.label.isNotEmpty()) drawLabel45Right(endX, endY, value, canvas, scaledPxSize, scaledTextSize) + } else if (value.shape == Shape.CARBS) { + mPaint.strokeWidth = 0f + val points = arrayOf( + Point(endX.toInt(), (endY - scaledPxSize).toInt()), + Point((endX + scaledPxSize).toInt(), (endY + scaledPxSize * 0.67).toInt()), + Point((endX - scaledPxSize).toInt(), (endY + scaledPxSize * 0.67).toInt()) + ) + mPaint.style = Paint.Style.FILL_AND_STROKE + drawArrows(points, canvas, mPaint) + if (value.label.isNotEmpty()) drawLabel45Left(endX, endY, value, canvas, scaledPxSize, scaledTextSize) + } else if (value.shape == Shape.SMB) { + mPaint.strokeWidth = 2f + val size = value.size * scaledPxSize + val points = arrayOf( + Point(endX.toInt(), (endY - size).toInt()), + Point((endX + size).toInt(), (endY + size * 0.67).toInt()), + Point((endX - size).toInt(), (endY + size * 0.67).toInt()) + ) + mPaint.style = Paint.Style.FILL_AND_STROKE + drawArrows(points, canvas, mPaint) + } else if (value.shape == Shape.EXTENDEDBOLUS) { + mPaint.strokeWidth = 0f + if (value.label.isNotEmpty()) { + val bounds = Rect(endX.toInt(), endY.toInt() + 3, xPlusLength.toInt(), endY.toInt() + 8) + mPaint.style = Paint.Style.FILL_AND_STROKE + canvas.drawRect(bounds, mPaint) + mPaint.textSize = scaledTextSize + mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)) + mPaint.isFakeBoldText = true + canvas.drawText(value.label, endX, endY, mPaint) + } + } else if (value.shape == Shape.HEARTRATE) { + mPaint.strokeWidth = 0f + val bounds = Rect(endX.toInt(), endY.toInt() - 8, xPlusLength.toInt(), endY.toInt() + 8) + mPaint.style = Paint.Style.FILL_AND_STROKE + canvas.drawRect(bounds, mPaint) + } else if (value.shape == Shape.PROFILE) { + val drawable = ContextCompat.getDrawable(graphView.context, R.drawable.ic_ribbon_profile) ?: break + drawable.setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY) + drawable.setBounds( + (endX - drawable.intrinsicWidth / 2).toInt(), + (endY - drawable.intrinsicHeight / 2).toInt(), + (endX + drawable.intrinsicWidth / 2).toInt(), + (endY + drawable.intrinsicHeight / 2).toInt() + ) + drawable.draw(canvas) + mPaint.textSize = scaledTextSize * 0.8f + mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)) + mPaint.color = value.color(graphView.context) + val bounds = Rect() + mPaint.getTextBounds(value.label, 0, value.label.length, bounds) + val px = endX - bounds.width() / 2.0f + val py = endY + drawable.intrinsicHeight + mPaint.style = Paint.Style.FILL + canvas.drawText(value.label, px, py, mPaint) + } else if (value.shape == Shape.MBG) { + mPaint.style = Paint.Style.STROKE + mPaint.strokeWidth = 5f + canvas.drawCircle(endX, endY, scaledPxSize, mPaint) + } else if (value.shape == Shape.BGCHECK || value.shape == Shape.ANNOUNCEMENT || value.shape == Shape.GENERAL) { + mPaint.style = Paint.Style.FILL_AND_STROKE + mPaint.strokeWidth = 0f + canvas.drawCircle(endX, endY, scaledPxSize, mPaint) + if (value.label.isNotEmpty()) drawLabel45Right(endX, endY, value, canvas, scaledPxSize, scaledTextSize) + } else if (value.shape == Shape.EXERCISE) { + mPaint.strokeWidth = 0f + if (!value.label.isEmpty()) { + mPaint.strokeWidth = 0f + mPaint.textSize = (scaledTextSize * 1.2).toFloat() + mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)) + val bounds = Rect() + mPaint.getTextBounds(value.label, 0, value.label.length, bounds) + mPaint.style = Paint.Style.STROKE + val py = graphTop + 20 + canvas.drawText(value.label, endX, py, mPaint) + mPaint.strokeWidth = 5f + canvas.drawRect(endX - 3, bounds.top + py - 3, xPlusLength + 3, bounds.bottom + py + 3, mPaint) + } + } else if (value.shape == Shape.OPENAPS_OFFLINE && value.duration != 0L) { + mPaint.strokeWidth = 0f + if (!value.label.isEmpty()) { + mPaint.style = Paint.Style.FILL_AND_STROKE + mPaint.strokeWidth = 5f + canvas.drawRect(endX - 3, graphTop, xPlusLength + 3, graphTop + graphHeight, mPaint) + } + } else if (value.shape == Shape.GENERAL_WITH_DURATION) { + mPaint.strokeWidth = 0f + if (!value.label.isEmpty()) { + mPaint.strokeWidth = 0f + mPaint.textSize = (scaledTextSize * 1.5).toFloat() + mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)) + val bounds = Rect() + mPaint.getTextBounds(value.label, 0, value.label.length, bounds) + mPaint.style = Paint.Style.STROKE + val py = graphTop + 80 + canvas.drawText(value.label, endX, py, mPaint) + mPaint.strokeWidth = 5f + canvas.drawRect(endX - 3, bounds.top + py - 3, xPlusLength + 3, bounds.bottom + py + 3, mPaint) + } + } + // set values above point + } + } + } + + /** + * helper to render triangle + * + * @param point array with 3 coordinates + * @param canvas canvas to draw on + * @param paint paint object + */ + private fun drawArrows(point: Array, canvas: Canvas, paint: Paint) { + canvas.save() + val path = Path() + path.moveTo(point[0].x.toFloat(), point[0].y.toFloat()) + path.lineTo(point[1].x.toFloat(), point[1].y.toFloat()) + path.lineTo(point[2].x.toFloat(), point[2].y.toFloat()) + path.close() + canvas.drawPath(path, paint) + canvas.restore() + } + + fun drawLabel45Right(endX: Float, endY: Float, value: E, canvas: Canvas, scaledPxSize: Float, scaledTextSize: Float) { + val py = endY - scaledPxSize + canvas.save() + canvas.rotate(-45f, endX, py) + mPaint.textSize = (scaledTextSize * 0.8).toFloat() + mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)) + mPaint.isFakeBoldText = true + canvas.drawText(value.label, endX + scaledPxSize, py, mPaint) + canvas.restore() + } + + fun drawLabel45Left(endX: Float, endY: Float, value: E, canvas: Canvas, scaledPxSize: Float, scaledTextSize: Float) { + val py = endY + scaledPxSize + canvas.save() + canvas.rotate(-45f, endX, py) + mPaint.textSize = (scaledTextSize * 0.8).toFloat() + mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)) + mPaint.isFakeBoldText = true + mPaint.textAlign = Paint.Align.RIGHT + canvas.drawText(value.label, endX - scaledPxSize, py, mPaint) + mPaint.textAlign = Paint.Align.LEFT + canvas.restore() + } +}