GraphSeries -> kt
This commit is contained in:
parent
3ba774edc0
commit
83fc6a0490
6 changed files with 1114 additions and 1227 deletions
|
@ -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 <g.jjoe64@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;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Series to plot the data as line.
|
||||
* The line can be styled with many options.
|
||||
*
|
||||
* @author jjoe64
|
||||
*/
|
||||
public class AreaGraphSeries<E extends DoubleDataPoint> extends BaseSeries<E> {
|
||||
/**
|
||||
* 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<E> 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;
|
||||
}
|
||||
}
|
|
@ -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 <g.jjoe64></g.jjoe64>@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<E : DoubleDataPoint?> : BaseSeries<E> {
|
||||
|
||||
/**
|
||||
* 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<E>?) : 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
|
||||
}
|
||||
}
|
|
@ -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<E extends DataPointInterface> extends BaseSeries<E> {
|
||||
/**
|
||||
* 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<E> 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;
|
||||
}
|
||||
}
|
|
@ -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<E : DataPointInterface?> : BaseSeries<E> {
|
||||
|
||||
/**
|
||||
* 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<E>?) : 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
|
||||
}
|
||||
}
|
|
@ -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<E extends DataPointWithLabelInterface> extends BaseSeries<E> {
|
||||
// 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<E> 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();
|
||||
}
|
||||
}
|
|
@ -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<E : DataPointWithLabelInterface> : BaseSeries<E> {
|
||||
|
||||
// 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<E>?) : 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<Point>, 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()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue