GraphSeries -> kt

This commit is contained in:
Milos Kozak 2023-09-24 20:39:33 +02:00
parent 3ba774edc0
commit 83fc6a0490
6 changed files with 1114 additions and 1227 deletions

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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();
}
}

View file

@ -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()
}
}