/** * 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.PointF; import android.util.Log; import com.jjoe64.graphview.GraphView; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; /** * Basis implementation for series. * Used for series that are plotted on * a default x/y 2d viewport. * * Extend this class to implement your own custom * graph type. * * This implementation uses a internal Array to store * the data. If you want to implement a custom data provider * you may want to implement {@link com.jjoe64.graphview.series.Series}. * * @author jjoe64 */ public abstract class BaseSeries<E extends DataPointInterface> implements Series<E> { /** * holds the data */ final private List<E> mData = new ArrayList<E>(); /** * stores the used coordinates to find the * corresponding data point on a tap * * Key => x/y pixel * Value => Plotted Datapoint * * will be filled while drawing via {@link #registerDataPoint(float, float, DataPointInterface)} */ private Map<PointF, E> mDataPoints = new HashMap<PointF, E>(); /** * title for this series that can be displayed * in the legend. */ private String mTitle; /** * base color for this series. will be used also in * the legend */ private int mColor = 0xff0077cc; /** * cache for lowest y value */ private double mLowestYCache = Double.NaN; /** * cahce for highest y value */ private double mHighestYCache = Double.NaN; /** * listener to handle tap events on a data point */ protected OnDataPointTapListener mOnDataPointTapListener; /** * stores the graphviews where this series is used. * Can be more than one. */ private List<WeakReference<GraphView>> mGraphViews; private Boolean mIsCursorModeCache; /** * creates series without data */ public BaseSeries() { mGraphViews = new ArrayList<>(); } /** * creates series with data * * @param data data points * important: array has to be sorted from lowest x-value to the highest */ public BaseSeries(E[] data) { mGraphViews = new ArrayList<>(); for (E d : data) { mData.add(d); } checkValueOrder(null); } /** * @return the lowest x value, or 0 if there is no data */ public double getLowestValueX() { if (mData.isEmpty()) return 0d; return mData.get(0).getX(); } /** * @return the highest x value, or 0 if there is no data */ public double getHighestValueX() { if (mData.isEmpty()) return 0d; return mData.get(mData.size()-1).getX(); } /** * @return the lowest y value, or 0 if there is no data */ public double getLowestValueY() { if (mData.isEmpty()) return 0d; if (!Double.isNaN(mLowestYCache)) { return mLowestYCache; } double l = mData.get(0).getY(); for (int i = 1; i < mData.size(); i++) { double c = mData.get(i).getY(); if (l > c) { l = c; } } return mLowestYCache = l; } /** * @return the highest y value, or 0 if there is no data */ public double getHighestValueY() { if (mData.isEmpty()) return 0d; if (!Double.isNaN(mHighestYCache)) { return mHighestYCache; } double h = mData.get(0).getY(); for (int i = 1; i < mData.size(); i++) { double c = mData.get(i).getY(); if (h < c) { h = c; } } return mHighestYCache = h; } /** * get the values for a given x range. if from and until are bigger or equal than * all the data, the original data is returned. * If it is only a part of the data, the range is returned plus one datapoint * before and after to get a nice scrolling. * * @param from minimal x-value * @param until maximal x-value * @return data for the range +/- 1 datapoint */ @Override public Iterator<E> getValues(final double from, final double until) { if (from <= getLowestValueX() && until >= getHighestValueX()) { return mData.iterator(); } else { return new Iterator<E>() { Iterator<E> org = mData.iterator(); E nextValue = null; E nextNextValue = null; boolean plusOne = true; { // go to first boolean found = false; E prevValue = null; if (org.hasNext()) { prevValue = org.next(); } if (prevValue != null) { if (prevValue.getX() >= from) { nextValue = prevValue; found = true; } else { while (org.hasNext()) { nextValue = org.next(); if (nextValue.getX() >= from) { found = true; nextNextValue = nextValue; nextValue = prevValue; break; } prevValue = nextValue; } } } if (!found) { nextValue = null; } } @Override public void remove() { throw new UnsupportedOperationException(); } @Override public E next() { if (hasNext()) { E r = nextValue; if (r.getX() > until) { plusOne = false; } if (nextNextValue != null) { nextValue = nextNextValue; nextNextValue = null; } else if (org.hasNext()) nextValue = org.next(); else nextValue = null; return r; } else { throw new NoSuchElementException(); } } @Override public boolean hasNext() { return nextValue != null && (nextValue.getX() <= until || plusOne); } }; } } /** * @return the title of the series */ public String getTitle() { return mTitle; } /** * set the title of the series. This will be used in * the legend. * * @param mTitle title of the series */ public void setTitle(String mTitle) { this.mTitle = mTitle; } /** * @return color of the series */ public int getColor() { return mColor; } /** * set the color of the series. This will be used in * plotting (depends on the series implementation) and * is used in the legend. * * @param mColor */ public void setColor(int mColor) { this.mColor = mColor; } /** * set a listener for tap on a data point. * * @param l listener */ public void setOnDataPointTapListener(OnDataPointTapListener l) { this.mOnDataPointTapListener = l; } /** * called by the tap detector in order to trigger * the on tap on datapoint event. * * @param x pixel * @param y pixel */ @Override public void onTap(float x, float y) { if (mOnDataPointTapListener != null) { E p = findDataPoint(x, y); if (p != null) { mOnDataPointTapListener.onTap(this, p); } } } /** * find the data point which is next to the * coordinates * * @param x pixel * @param y pixel * @return the data point or null if nothing was found */ protected E findDataPoint(float x, float y) { float shortestDistance = Float.NaN; E shortest = null; for (Map.Entry<PointF, E> entry : mDataPoints.entrySet()) { float x1 = entry.getKey().x; float y1 = entry.getKey().y; float x2 = x; float y2 = y; float distance = (float) Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)); if (shortest == null || distance < shortestDistance) { shortestDistance = distance; shortest = entry.getValue(); } } if (shortest != null) { if (shortestDistance < 120) { return shortest; } } return null; } public E findDataPointAtX(float x) { float shortestDistance = Float.NaN; E shortest = null; for (Map.Entry<PointF, E> entry : mDataPoints.entrySet()) { float x1 = entry.getKey().x; float x2 = x; float distance = Math.abs(x1 - x2); if (shortest == null || distance < shortestDistance) { shortestDistance = distance; shortest = entry.getValue(); } } if (shortest != null) { if (shortestDistance < 200) { return shortest; } } return null; } /** * register the datapoint to find it at a tap * * @param x pixel * @param y pixel * @param dp the data point to save */ protected void registerDataPoint(float x, float y, E dp) { // performance // TODO maybe invalidate after setting the listener if (mOnDataPointTapListener != null || isCursorMode()) { mDataPoints.put(new PointF(x, y), dp); } } private boolean isCursorMode() { if (mIsCursorModeCache != null) { return mIsCursorModeCache; } for (WeakReference<GraphView> graphView : mGraphViews) { if (graphView != null && graphView.get() != null && graphView.get().isCursorMode()) { return mIsCursorModeCache = true; } } return mIsCursorModeCache = false; } /** * clears the cached data point coordinates */ protected void resetDataPoints() { mDataPoints.clear(); } /** * clears the data of this series and sets new. * will redraw the graph * * @param data 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. */ public void resetData(E[] data) { mData.clear(); for (E d : data) { mData.add(d); } checkValueOrder(null); mHighestYCache = mLowestYCache = Double.NaN; // update graphview for (WeakReference<GraphView> gv : mGraphViews) { if (gv != null && gv.get() != null) { gv.get().onDataChanged(true, false); } } } /** * stores the reference of the used graph * * @param graphView graphview */ @Override public void onGraphViewAttached(GraphView graphView) { mGraphViews.add(new WeakReference<>(graphView)); } /** * * @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) { checkValueOrder(dataPoint); if (!mData.isEmpty() && dataPoint.getX() < mData.get(mData.size()-1).getX()) { throw new IllegalArgumentException("new x-value must be greater then the last value. x-values has to be ordered in ASC."); } synchronized (mData) { int curDataCount = mData.size(); if (curDataCount < maxDataPoints) { // enough space mData.add(dataPoint); } else { // we have to trim one data mData.remove(0); mData.add(dataPoint); } // update lowest/highest cache double dataPointY = dataPoint.getY(); if (!Double.isNaN(mHighestYCache)) { if (dataPointY > mHighestYCache) { mHighestYCache = dataPointY; } } if (!Double.isNaN(mLowestYCache)) { if (dataPointY < mLowestYCache) { mLowestYCache = dataPointY; } } } if (!silent) { // recalc the labels when it was the first data boolean keepLabels = mData.size() != 1; // update linked graph views // update graphview for (WeakReference<GraphView> gv : mGraphViews) { if (gv != null && gv.get() != null) { if (scrollToEnd) { gv.get().getViewport().scrollToEnd(); } else { gv.get().onDataChanged(keepLabels, scrollToEnd); } } } } } /** * * @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 */ public void appendData(E dataPoint, boolean scrollToEnd, int maxDataPoints) { appendData(dataPoint, scrollToEnd, maxDataPoints, false); } /** * @return whether there are data points */ @Override public boolean isEmpty() { return mData.isEmpty(); } /** * checks that the data is in the correct order * * @param onlyLast if not null, it will only check that this * datapoint is after the last point. */ protected void checkValueOrder(DataPointInterface onlyLast) { if (mData.size()>1) { if (onlyLast != null) { // only check last if (onlyLast.getX() < mData.get(mData.size()-1).getX()) { throw new IllegalArgumentException("new x-value must be greater then the last value. x-values has to be ordered in ASC."); } } else { double lx = mData.get(0).getX(); for (int i = 1; i < mData.size(); i++) { if (mData.get(i).getX() != Double.NaN) { if (lx > mData.get(i).getX()) { throw new IllegalArgumentException("The order of the values is not correct. X-Values have to be ordered ASC. First the lowest x value and at least the highest x value."); } lx = mData.get(i).getX(); } } } } } public abstract void drawSelection(GraphView mGraphView, Canvas canvas, boolean b, DataPointInterface value); public void clearCursorModeCache() { mIsCursorModeCache = null; } @Override public void clearReference(GraphView graphView) { // find and remove for (WeakReference<GraphView> view : mGraphViews) { if (view != null && view.get() != null && view.get() == graphView) { mGraphViews.remove(view); break; } } } }