/** * GraphView * Copyright 2016 Jonas Gehring * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jjoe64.graphview.series; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.support.v4.view.ViewCompat; import android.view.animation.AccelerateInterpolator; import com.jjoe64.graphview.GraphView; import java.util.Iterator; /** * Series to plot the data as line. * The line can be styled with many options. * * @author jjoe64 */ public class LineGraphSeries<E extends DataPointInterface> extends BaseSeries<E> { private static final long ANIMATION_DURATION = 333; /** * wrapped styles regarding the line */ private 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; private Paint mSelectionPaint; /** * 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; /** * rendering is animated */ private boolean mAnimated; /** * last animated value */ private double mLastAnimatedValue = Double.NaN; /** * time of animation start */ private long mAnimationStart; /** * animation interpolator */ private AccelerateInterpolator mAnimationInterpolator; /** * number of animation frame to avoid lagging */ private int mAnimationStartFrameNo; /** * flag whether the line should be drawn as a path * or with single drawLine commands (more performance) * By default we use drawLine because it has much more peformance. * For some styling reasons it can make sense to draw as path. */ private boolean mDrawAsPath = false; /** * creates a series without data */ public LineGraphSeries() { init(); } /** * creates a series with data * * @param data data points * important: array has to be sorted from lowest x-value to the highest */ public LineGraphSeries(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(); mSelectionPaint = new Paint(); mSelectionPaint.setColor(Color.argb(80, 0, 0, 0)); mSelectionPaint.setStyle(Paint.Style.FILL); mPathBackground = new Path(); mPath = new Path(); mAnimationInterpolator = new AccelerateInterpolator(2f); } /** * 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(false); minY = graphView.getSecondScale().getMinY(false); } else { maxY = graphView.getViewport().getMaxY(false); minY = graphView.getViewport().getMinY(false); } Iterator<E> values = getValues(minX, maxX); // draw background double lastEndY = 0; double lastEndX = 0; // draw data mPaint.setStrokeWidth(mStyles.thickness); mPaint.setColor(getColor()); mPaintBackground.setColor(mStyles.backgroundColor); Paint paint; if (mCustomPaint != null) { paint = mCustomPaint; } else { paint = mPaint; } mPath.reset(); 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; // needed to end the path for background double lastUsedEndX = 0; double lastUsedEndY = 0; float firstX = -1; float firstY = -1; float lastRenderedX = Float.NaN; int i=0; float lastAnimationReferenceX = graphLeft; boolean sameXSkip = false; float minYOnSameX = 0f; float maxYOnSameX = 0f; while (values.hasNext()) { E value = values.next(); double valY = value.getY() - minY; double ratY = valY / diffY; double y = graphHeight * ratY; double valueX = value.getX(); double valX = valueX - minX; double ratX = valX / diffX; double x = graphWidth * ratX; double orgX = x; double orgY = y; if (i > 0) { // overdraw boolean isOverdrawY = false; boolean isOverdrawEndPoint = false; boolean skipDraw = false; if (x > graphWidth) { // end right double b = ((graphWidth - lastEndX) * (y - lastEndY)/(x - lastEndX)); y = lastEndY+b; x = graphWidth; isOverdrawEndPoint = true; } if (y < 0) { // end bottom // skip when previous and this point is out of bound if (lastEndY < 0) { skipDraw=true; } else { double b = ((0 - lastEndY) * (x - lastEndX)/(y - lastEndY)); x = lastEndX+b; } y = 0; isOverdrawY = isOverdrawEndPoint = true; } if (y > graphHeight) { // end top // skip when previous and this point is out of bound if (lastEndY > graphHeight) { skipDraw=true; } else { double b = ((graphHeight - lastEndY) * (x - lastEndX)/(y - lastEndY)); x = lastEndX+b; } y = graphHeight; isOverdrawY = isOverdrawEndPoint = true; } if (lastEndX < 0) { // start left double b = ((0 - x) * (y - lastEndY)/(lastEndX - x)); lastEndY = y-b; lastEndX = 0; } // we need to save the X before it will be corrected when overdraw y float orgStartX = (float) lastEndX + (graphLeft + 1); if (lastEndY < 0) { // start bottom if (!skipDraw) { double b = ((0 - y) * (x - lastEndX) / (lastEndY - y)); lastEndX = x-b; } lastEndY = 0; isOverdrawY = true; } if (lastEndY > graphHeight) { // start top // skip when previous and this point is out of bound if (!skipDraw) { double b = ((graphHeight - y) * (x - lastEndX)/(lastEndY - y)); lastEndX = x-b; } lastEndY = graphHeight; isOverdrawY = true; } float startX = (float) lastEndX + (graphLeft + 1); float startY = (float) (graphTop - lastEndY) + graphHeight; float endX = (float) x + (graphLeft + 1); float endY = (float) (graphTop - y) + graphHeight; float startXAnimated = startX; float endXAnimated = endX; if (endX < startX) { // dont draw from right to left skipDraw = true; } // NaN can happen when previous and current value is out of y bounds if (!skipDraw && !Float.isNaN(startY) && !Float.isNaN(endY)) { // animation if (mAnimated) { if ((Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { long currentTime = System.currentTimeMillis(); if (mAnimationStart == 0) { // start animation mAnimationStart = currentTime; mAnimationStartFrameNo = 0; } else { // anti-lag: wait a few frames if (mAnimationStartFrameNo < 15) { // second time mAnimationStart = currentTime; mAnimationStartFrameNo++; } } float timeFactor = (float) (currentTime-mAnimationStart) / ANIMATION_DURATION; float factor = mAnimationInterpolator.getInterpolation(timeFactor); if (timeFactor <= 1.0) { startXAnimated = (startX-lastAnimationReferenceX) * factor + lastAnimationReferenceX; startXAnimated = Math.max(startXAnimated, lastAnimationReferenceX); endXAnimated = (endX-lastAnimationReferenceX) * factor + lastAnimationReferenceX; ViewCompat.postInvalidateOnAnimation(graphView); } else { // animation finished mLastAnimatedValue = valueX; } } else { lastAnimationReferenceX = endX; } } // draw data point if (!isOverdrawEndPoint) { if (mStyles.drawDataPoints) { // draw first datapoint Paint.Style prevStyle = paint.getStyle(); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(endXAnimated, endY, mStyles.dataPointsRadius, paint); paint.setStyle(prevStyle); } registerDataPoint(endX, endY, value); } if (mDrawAsPath) { mPath.moveTo(startXAnimated, startY); } // performance opt. if (Float.isNaN(lastRenderedX) || Math.abs(endX-lastRenderedX) > .3f) { if (mDrawAsPath) { mPath.lineTo(endXAnimated, endY); } else { // draw vertical lines that were skipped if (sameXSkip) { sameXSkip = false; renderLine(canvas, new float[] {lastRenderedX, minYOnSameX, lastRenderedX, maxYOnSameX}, paint); } renderLine(canvas, new float[] {startXAnimated, startY, endXAnimated, endY}, paint); } lastRenderedX = endX; } else { // rendering on same x position // save min+max y position and draw it as line if (sameXSkip) { minYOnSameX = Math.min(minYOnSameX, endY); maxYOnSameX = Math.max(maxYOnSameX, endY); } else { // first sameXSkip = true; minYOnSameX = Math.min(startY, endY); maxYOnSameX = Math.max(startY, endY); } } } if (mStyles.drawBackground) { if (isOverdrawY) { // start draw original x if (firstX == -1) { firstX = orgStartX; firstY = startY; mPathBackground.moveTo(orgStartX, startY); } // from original start to new start mPathBackground.lineTo(startXAnimated, startY); } if (firstX == -1) { firstX = startXAnimated; firstY = startY; mPathBackground.moveTo(startXAnimated, startY); } mPathBackground.lineTo(startXAnimated, startY); mPathBackground.lineTo(endXAnimated, endY); } lastUsedEndX = endXAnimated; lastUsedEndY = endY; } 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; if (first_X >= graphLeft && first_Y <= (graphTop+graphHeight)) { if (mAnimated && (Double.isNaN(mLastAnimatedValue) || mLastAnimatedValue < valueX)) { long currentTime = System.currentTimeMillis(); if (mAnimationStart == 0) { // start animation mAnimationStart = currentTime; } float timeFactor = (float) (currentTime-mAnimationStart) / ANIMATION_DURATION; float factor = mAnimationInterpolator.getInterpolation(timeFactor); if (timeFactor <= 1.0) { first_X = (first_X-lastAnimationReferenceX) * factor + lastAnimationReferenceX; ViewCompat.postInvalidateOnAnimation(graphView); } else { // animation finished mLastAnimatedValue = valueX; } } Paint.Style prevStyle = paint.getStyle(); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(first_X, first_Y, mStyles.dataPointsRadius, paint); paint.setStyle(prevStyle); } } lastEndY = orgY; lastEndX = orgX; i++; } if (mDrawAsPath) { // draw at the end canvas.drawPath(mPath, paint); } if (mStyles.drawBackground && firstX != -1) { // end / close path if (lastUsedEndY != graphHeight + graphTop) { // dont draw line to same point, otherwise the path is completely broken mPathBackground.lineTo((float) lastUsedEndX, graphHeight + graphTop); } mPathBackground.lineTo(firstX, graphHeight + graphTop); if (firstY != graphHeight + graphTop) { // dont draw line to same point, otherwise the path is completely broken mPathBackground.lineTo(firstX, firstY); } //mPathBackground.close(); canvas.drawPath(mPathBackground, mPaintBackground); } } /** * just a wrapper to draw lines on canvas * * @param canvas * @param pts * @param paint */ private void renderLine(Canvas canvas, float[] pts, Paint paint) { canvas.drawLines(pts, paint); } /** * 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 */ 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() */ 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; } /** * @param animated activate the animated rendering */ public void setAnimated(boolean animated) { this.mAnimated = animated; } /** * flag whether the line should be drawn as a path * or with single drawLine commands (more performance) * By default we use drawLine because it has much more peformance. * For some styling reasons it can make sense to draw as path. */ public boolean isDrawAsPath() { return mDrawAsPath; } /** * flag whether the line should be drawn as a path * or with single drawLine commands (more performance) * By default we use drawLine because it has much more peformance. * For some styling reasons it can make sense to draw as path. * * @param mDrawAsPath true to draw as path */ public void setDrawAsPath(boolean mDrawAsPath) { this.mDrawAsPath = mDrawAsPath; } /** * * @param dataPoint values the values must be in the correct order! * x-value has to be ASC. First the lowest x value and at least the highest x value. * @param scrollToEnd true => graphview will scroll to the end (maxX) * @param maxDataPoints if max data count is reached, the oldest data * value will be lost to avoid memory leaks * @param silent set true to avoid rerender the graph */ public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints, boolean silent) { if (!isAnimationActive()) { mAnimationStart = 0; } super.appendData(dataPoint, scrollToEnd, maxDataPoints, silent); } /** * @return currently animation is active */ private boolean isAnimationActive() { if (mAnimated) { long curr = System.currentTimeMillis(); return curr-mAnimationStart <= ANIMATION_DURATION; } return false; } @Override public void drawSelection(GraphView graphView, Canvas canvas, boolean b, DataPointInterface value) { double spanX = graphView.getViewport().getMaxX(false) - graphView.getViewport().getMinX(false); double spanXPixel = graphView.getGraphContentWidth(); double spanY = graphView.getViewport().getMaxY(false) - graphView.getViewport().getMinY(false); double spanYPixel = graphView.getGraphContentHeight(); double pointX = (value.getX() - graphView.getViewport().getMinX(false)) * spanXPixel / spanX; pointX += graphView.getGraphContentLeft(); double pointY = (value.getY() - graphView.getViewport().getMinY(false)) * spanYPixel / spanY; pointY = graphView.getGraphContentTop() + spanYPixel - pointY; // border canvas.drawCircle((float) pointX, (float) pointY, 30f, mSelectionPaint); // fill Paint.Style prevStyle = mPaint.getStyle(); mPaint.setStyle(Paint.Style.FILL); canvas.drawCircle((float) pointX, (float) pointY, 23f, mPaint); mPaint.setStyle(prevStyle); } }