/**
* 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;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.OverScroller;
import com.jjoe64.graphview.series.DataPointInterface;
import com.jjoe64.graphview.series.Series;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* This is the default implementation for the viewport.
* This implementation so for a normal viewport
* where there is a horizontal x-axis and a
* vertical y-axis.
* This viewport is compatible with
* - {@link com.jjoe64.graphview.series.BarGraphSeries}
* - {@link com.jjoe64.graphview.series.LineGraphSeries}
* - {@link com.jjoe64.graphview.series.PointsGraphSeries}
*
* @author jjoe64
*/
public class Viewport {
/**
* this reference value is used to generate the
* vertical labels. It is used when the y axis bounds
* is set manual and humanRounding=false. it will be the minValueY value.
*/
protected double referenceY = Double.NaN;
/**
* this reference value is used to generate the
* horizontal labels. It is used when the x axis bounds
* is set manual and humanRounding=false. it will be the minValueX value.
*/
protected double referenceX = Double.NaN;
/**
* flag whether the vertical scaling is activated
*/
protected boolean scalableY;
/**
* minimal viewport used for scaling and scrolling.
* this is used if the data that is available is
* less then the viewport that we want to be able to display.
*
* Double.NaN to disable this value
*/
private RectD mMinimalViewport = new RectD(Double.NaN, Double.NaN, Double.NaN, Double.NaN);
/**
* the reference number to generate the labels
* @return by default 0, only when manual bounds and no human rounding
* is active, the min x value is returned
*/
protected double getReferenceX() {
// if the bounds is manual then we take the
// original manual min y value as reference
if (isXAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRounding()) {
if (Double.isNaN(referenceX)) {
referenceX = getMinX(false);
}
return referenceX;
} else {
// starting from 0 so that the steps have nice numbers
return 0;
}
}
/**
* listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*/
public interface OnXAxisBoundsChangedListener {
/**
* Called after scaling or scrolling with
* the new bounds
* @param minX min x value
* @param maxX max x value
*/
void onXAxisBoundsChanged(double minX, double maxX, OnXAxisBoundsChangedListener.Reason reason);
public enum Reason {
SCROLL, SCALE
}
}
/**
* listener for the scale gesture
*/
private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
= new ScaleGestureDetector.OnScaleGestureListener() {
/**
* called by android
* @param detector detector
* @return always true
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
// --- horizontal scaling ---
double viewportWidth = mCurrentViewport.width();
if (mMaxXAxisSize != 0) {
if (viewportWidth > mMaxXAxisSize) {
viewportWidth = mMaxXAxisSize;
}
}
double center = mCurrentViewport.left + viewportWidth / 2;
float scaleSpanX;
if (android.os.Build.VERSION.SDK_INT >= 11 && scalableY) {
scaleSpanX = detector.getCurrentSpanX()/detector.getPreviousSpanX();
} else {
scaleSpanX = detector.getScaleFactor();
}
viewportWidth /= scaleSpanX;
mCurrentViewport.left = center - viewportWidth / 2;
mCurrentViewport.right = mCurrentViewport.left+viewportWidth;
// viewportStart must not be < minX
double minX = getMinX(true);
if (!Double.isNaN(mMinimalViewport.left)) {
minX = Math.min(minX, mMinimalViewport.left);
}
if (mCurrentViewport.left < minX) {
mCurrentViewport.left = minX;
mCurrentViewport.right = mCurrentViewport.left+viewportWidth;
}
// viewportStart + viewportSize must not be > maxX
double maxX = getMaxX(true);
if (!Double.isNaN(mMinimalViewport.right)) {
maxX = Math.max(maxX, mMinimalViewport.right);
}
if (viewportWidth == 0) {
mCurrentViewport.right = maxX;
}
double overlap = mCurrentViewport.left + viewportWidth - maxX;
if (overlap > 0) {
// scroll left
if (mCurrentViewport.left-overlap > minX) {
mCurrentViewport.left -= overlap;
mCurrentViewport.right = mCurrentViewport.left+viewportWidth;
} else {
// maximal scale
mCurrentViewport.left = minX;
mCurrentViewport.right = maxX;
}
}
// --- vertical scaling ---
if (scalableY && android.os.Build.VERSION.SDK_INT >= 11 && detector.getCurrentSpanY() != 0f && detector.getPreviousSpanY() != 0f) {
boolean hasSecondScale = mGraphView.mSecondScale != null;
double viewportHeight = mCurrentViewport.height()*-1;
if (mMaxYAxisSize != 0) {
if (viewportHeight > mMaxYAxisSize) {
viewportHeight = mMaxYAxisSize;
}
}
center = mCurrentViewport.bottom + viewportHeight / 2;
viewportHeight /= detector.getCurrentSpanY()/detector.getPreviousSpanY();
mCurrentViewport.bottom = center - viewportHeight / 2;
mCurrentViewport.top = mCurrentViewport.bottom+viewportHeight;
// ignore bounds when second scale
if (!hasSecondScale) {
// viewportStart must not be < minY
double minY = getMinY(true);
if (!Double.isNaN(mMinimalViewport.bottom)) {
minY = Math.min(minY, mMinimalViewport.bottom);
}
if (mCurrentViewport.bottom < minY) {
mCurrentViewport.bottom = minY;
mCurrentViewport.top = mCurrentViewport.bottom+viewportHeight;
}
// viewportStart + viewportSize must not be > maxY
double maxY = getMaxY(true);
if (!Double.isNaN(mMinimalViewport.top)) {
maxY = Math.max(maxY, mMinimalViewport.top);
}
if (viewportHeight == 0) {
mCurrentViewport.top = maxY;
}
overlap = mCurrentViewport.bottom + viewportHeight - maxY;
if (overlap > 0) {
// scroll left
if (mCurrentViewport.bottom-overlap > minY) {
mCurrentViewport.bottom -= overlap;
mCurrentViewport.top = mCurrentViewport.bottom+viewportHeight;
} else {
// maximal scale
mCurrentViewport.bottom = minY;
mCurrentViewport.top = maxY;
}
}
} else {
// ---- second scale ---
viewportHeight = mGraphView.mSecondScale.mCurrentViewport.height()*-1;
center = mGraphView.mSecondScale.mCurrentViewport.bottom + viewportHeight / 2;
viewportHeight /= detector.getCurrentSpanY()/detector.getPreviousSpanY();
mGraphView.mSecondScale.mCurrentViewport.bottom = center - viewportHeight / 2;
mGraphView.mSecondScale.mCurrentViewport.top = mGraphView.mSecondScale.mCurrentViewport.bottom+viewportHeight;
}
}
// adjustSteps viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
/**
* called when scaling begins
*
* @param detector detector
* @return true if it is scalable
*/
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
// cursor mode
if (mGraphView.isCursorMode()) {
return false;
}
if (mIsScalable) {
mScalingActive = true;
return true;
} else {
return false;
}
}
/**
* called when sacling ends
* This will re-adjustSteps the viewport.
*
* @param detector detector
*/
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
mScalingActive = false;
// notify
if (mOnXAxisBoundsChangedListener != null) {
mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCALE);
}
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
};
/**
* simple gesture listener to track scroll events
*/
private final GestureDetector.SimpleOnGestureListener mGestureListener
= new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
// cursor mode
if (mGraphView.isCursorMode()) {
return true;
}
if (!mIsScrollable || mScalingActive) return false;
// Initiates the decay phase of any active edge effects.
releaseEdgeEffects();
// Aborts any active scroll animations and invalidates.
mScroller.forceFinished(true);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// cursor mode
if (mGraphView.isCursorMode()) {
return true;
}
if (!mIsScrollable || mScalingActive) return false;
// Scrolling uses math based on the viewport (as opposed to math using pixels).
/**
* Pixel offset is the offset in screen pixels, while viewport offset is the
* offset within the current viewport. For additional information on surface sizes
* and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
* additional information about the viewport, see the comments for
* {@link mCurrentViewport}.
*/
double viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth();
double viewportOffsetY = distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight();
// respect minimal viewport
double completeRangeLeft = mCompleteRange.left;
if (!Double.isNaN(mMinimalViewport.left)) {
completeRangeLeft = Math.min(completeRangeLeft, mMinimalViewport.left);
}
double completeRangeRight = mCompleteRange.right;
if (!Double.isNaN(mMinimalViewport.right)) {
completeRangeRight = Math.max(completeRangeRight, mMinimalViewport.right);
}
double completeRangeWidth = completeRangeRight - completeRangeLeft;
double completeRangeBottom = mCompleteRange.bottom;
if (!Double.isNaN(mMinimalViewport.bottom)) {
completeRangeBottom = Math.min(completeRangeBottom, mMinimalViewport.bottom);
}
double completeRangeTop = mCompleteRange.top;
if (!Double.isNaN(mMinimalViewport.top)) {
completeRangeTop = Math.max(completeRangeTop, mMinimalViewport.top);
}
double completeRangeHeight = completeRangeTop - completeRangeBottom;
int completeWidth = (int)((completeRangeWidth/mCurrentViewport.width()) * (double) mGraphView.getGraphContentWidth());
int completeHeight = (int)((completeRangeHeight/mCurrentViewport.height()) * (double) mGraphView.getGraphContentHeight());
int scrolledX = (int) (completeWidth
* (mCurrentViewport.left + viewportOffsetX - completeRangeLeft)
/ completeRangeWidth);
int scrolledY = (int) (completeHeight
* (mCurrentViewport.bottom + viewportOffsetY - completeRangeBottom)
/ completeRangeHeight*-1);
boolean canScrollX = mCurrentViewport.left > completeRangeLeft
|| mCurrentViewport.right < completeRangeRight;
boolean canScrollY = mCurrentViewport.bottom > completeRangeBottom
|| mCurrentViewport.top < completeRangeTop;
boolean hasSecondScale = mGraphView.mSecondScale != null;
// second scale
double viewportOffsetY2 = 0d;
if (hasSecondScale) {
viewportOffsetY2 = distanceY * mGraphView.mSecondScale.mCurrentViewport.height() / mGraphView.getGraphContentHeight();
canScrollY |= mGraphView.mSecondScale.mCurrentViewport.bottom > mGraphView.mSecondScale.mCompleteRange.bottom
|| mGraphView.mSecondScale.mCurrentViewport.top < mGraphView.mSecondScale.mCompleteRange.top;
}
canScrollY &= scrollableY;
if (canScrollX) {
if (viewportOffsetX < 0) {
double tooMuch = mCurrentViewport.left+viewportOffsetX - completeRangeLeft;
if (tooMuch < 0) {
viewportOffsetX -= tooMuch;
}
} else {
double tooMuch = mCurrentViewport.right+viewportOffsetX - completeRangeRight;
if (tooMuch > 0) {
viewportOffsetX -= tooMuch;
}
}
mCurrentViewport.left += viewportOffsetX;
mCurrentViewport.right += viewportOffsetX;
// notify
if (mOnXAxisBoundsChangedListener != null) {
mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCROLL);
}
}
if (canScrollY) {
// if we have the second axis we ignore the max/min range
if (!hasSecondScale) {
if (viewportOffsetY < 0) {
double tooMuch = mCurrentViewport.bottom+viewportOffsetY - completeRangeBottom;
if (tooMuch < 0) {
viewportOffsetY -= tooMuch;
}
} else {
double tooMuch = mCurrentViewport.top+viewportOffsetY - completeRangeTop;
if (tooMuch > 0) {
viewportOffsetY -= tooMuch;
}
}
}
mCurrentViewport.top += viewportOffsetY;
mCurrentViewport.bottom += viewportOffsetY;
// second scale
if (hasSecondScale) {
mGraphView.mSecondScale.mCurrentViewport.top += viewportOffsetY2;
mGraphView.mSecondScale.mCurrentViewport.bottom += viewportOffsetY2;
}
}
if (canScrollX && scrolledX < 0) {
mEdgeEffectLeft.onPull(scrolledX / (float) mGraphView.getGraphContentWidth());
}
if (!hasSecondScale && canScrollY && scrolledY < 0) {
mEdgeEffectBottom.onPull(scrolledY / (float) mGraphView.getGraphContentHeight());
}
if (canScrollX && scrolledX > completeWidth - mGraphView.getGraphContentWidth()) {
mEdgeEffectRight.onPull((scrolledX - completeWidth + mGraphView.getGraphContentWidth())
/ (float) mGraphView.getGraphContentWidth());
}
if (!hasSecondScale && canScrollY && scrolledY > completeHeight - mGraphView.getGraphContentHeight()) {
mEdgeEffectTop.onPull((scrolledY - completeHeight + mGraphView.getGraphContentHeight())
/ (float) mGraphView.getGraphContentHeight());
}
// adjustSteps viewport, labels, etc.
mGraphView.onDataChanged(true, false);
ViewCompat.postInvalidateOnAnimation(mGraphView);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
//fling((int) -velocityX, (int) -velocityY);
return true;
}
};
/**
* the state of the axis bounds
*/
public enum AxisBoundsStatus {
/**
* initial means that the bounds gets
* auto adjusted if they are not manual.
* After adjusting the status comes to
* #AUTO_ADJUSTED.
*/
INITIAL,
/**
* after the bounds got auto-adjusted,
* this status will set.
*/
AUTO_ADJUSTED,
/**
* means that the bounds are fix (manually) and
* are not to be auto-adjusted.
*/
FIX
}
/**
* paint to draw background
*/
private Paint mPaint;
/**
* reference to the graphview
*/
private final GraphView mGraphView;
/**
* this holds the current visible viewport
* left = minX, right = maxX
* bottom = minY, top = maxY
*/
protected RectD mCurrentViewport = new RectD();
/**
* maximum allowed viewport size (horizontal)
* 0 means use the bounds of the actual data that is
* available
*/
protected double mMaxXAxisSize = 0;
/**
* maximum allowed viewport size (vertical)
* 0 means use the bounds of the actual data that is
* available
*/
protected double mMaxYAxisSize = 0;
/**
* this holds the whole range of the data
* left = minX, right = maxX
* bottom = minY, top = maxY
*/
protected RectD mCompleteRange = new RectD();
/**
* flag whether scaling is currently active
*/
protected boolean mScalingActive;
/**
* flag whether the viewport is scrollable
*/
private boolean mIsScrollable;
/**
* flag whether the viewport is scalable
*/
private boolean mIsScalable;
/**
* flag whether the viewport is scalable
* on the Y axis
*/
private boolean scrollableY;
/**
* gesture detector to detect scrolling
*/
protected GestureDetector mGestureDetector;
/**
* detect scaling
*/
protected ScaleGestureDetector mScaleGestureDetector;
/**
* not used - for fling
*/
protected OverScroller mScroller;
/**
* not used
*/
private EdgeEffectCompat mEdgeEffectTop;
/**
* not used
*/
private EdgeEffectCompat mEdgeEffectBottom;
/**
* glow effect when scrolling left
*/
private EdgeEffectCompat mEdgeEffectLeft;
/**
* glow effect when scrolling right
*/
private EdgeEffectCompat mEdgeEffectRight;
/**
* state of the x axis
*/
protected AxisBoundsStatus mXAxisBoundsStatus;
/**
* state of the y axis
*/
protected AxisBoundsStatus mYAxisBoundsStatus;
/**
* flag whether the x axis bounds are manual
*/
private boolean mXAxisBoundsManual;
/**
* flag whether the y axis bounds are manual
*/
private boolean mYAxisBoundsManual;
/**
* background color of the viewport area
* it is recommended to use a semi-transparent color
*/
private int mBackgroundColor;
/**
* listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*/
protected OnXAxisBoundsChangedListener mOnXAxisBoundsChangedListener;
/**
* optional draw a border between the labels
* and the viewport
*/
private boolean mDrawBorder;
/**
* color of the border
* @see #setDrawBorder(boolean)
*/
private Integer mBorderColor;
/**
* custom paint to use for the border
* @see #setDrawBorder(boolean)
*/
private Paint mBorderPaint;
/**
* creates the viewport
*
* @param graphView graphview
*/
Viewport(GraphView graphView) {
mScroller = new OverScroller(graphView.getContext());
mEdgeEffectTop = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectBottom = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectLeft = new EdgeEffectCompat(graphView.getContext());
mEdgeEffectRight = new EdgeEffectCompat(graphView.getContext());
mGestureDetector = new GestureDetector(graphView.getContext(), mGestureListener);
mScaleGestureDetector = new ScaleGestureDetector(graphView.getContext(), mScaleGestureListener);
mGraphView = graphView;
mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
mBackgroundColor = Color.TRANSPARENT;
mPaint = new Paint();
}
/**
* will be called on a touch event.
* needed to use scaling and scrolling
*
* @param event
* @return true if it was consumed
*/
public boolean onTouchEvent(MotionEvent event) {
boolean b = mScaleGestureDetector.onTouchEvent(event);
b |= mGestureDetector.onTouchEvent(event);
if (mGraphView.isCursorMode()) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mGraphView.getCursorMode().onDown(event);
b |= true;
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
mGraphView.getCursorMode().onMove(event);
b |= true;
}
if (event.getAction() == MotionEvent.ACTION_UP) {
b |= mGraphView.getCursorMode().onUp(event);
}
}
return b;
}
/**
* change the state of the x axis.
* normally you do not call this method.
* If you want to set manual axis use
* {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
*
* @param s state
*/
public void setXAxisBoundsStatus(AxisBoundsStatus s) {
mXAxisBoundsStatus = s;
}
/**
* change the state of the y axis.
* normally you do not call this method.
* If you want to set manual axis use
* {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
*
* @param s state
*/
public void setYAxisBoundsStatus(AxisBoundsStatus s) {
mYAxisBoundsStatus = s;
}
/**
* @return whether the viewport is scrollable
*/
public boolean isScrollable() {
return mIsScrollable;
}
/**
* @param mIsScrollable whether is viewport is scrollable
*/
public void setScrollable(boolean mIsScrollable) {
this.mIsScrollable = mIsScrollable;
}
/**
* @return the x axis state
*/
public AxisBoundsStatus getXAxisBoundsStatus() {
return mXAxisBoundsStatus;
}
/**
* @return the y axis state
*/
public AxisBoundsStatus getYAxisBoundsStatus() {
return mYAxisBoundsStatus;
}
/**
* caches the complete range (minX, maxX, minY, maxY)
* by iterating all series and all datapoints and
* stores it into #mCompleteRange
*
* for the x-range it will respect the series on the
* second scale - not for y-values
*/
public void calcCompleteRange() {
List<Series> series = mGraphView.getSeries();
List<Series> seriesInclusiveSecondScale = new ArrayList<>(mGraphView.getSeries());
if (mGraphView.mSecondScale != null) {
seriesInclusiveSecondScale.addAll(mGraphView.mSecondScale.getSeries());
}
mCompleteRange.set(0d, 0d, 0d, 0d);
if (!seriesInclusiveSecondScale.isEmpty() && !seriesInclusiveSecondScale.get(0).isEmpty()) {
double d = seriesInclusiveSecondScale.get(0).getLowestValueX();
for (Series s : seriesInclusiveSecondScale) {
if (!s.isEmpty() && d > s.getLowestValueX()) {
d = s.getLowestValueX();
}
}
mCompleteRange.left = d;
d = seriesInclusiveSecondScale.get(0).getHighestValueX();
for (Series s : seriesInclusiveSecondScale) {
if (!s.isEmpty() && d < s.getHighestValueX()) {
d = s.getHighestValueX();
}
}
mCompleteRange.right = d;
if (!series.isEmpty() && !series.get(0).isEmpty()) {
d = series.get(0).getLowestValueY();
for (Series s : series) {
if (!s.isEmpty() && d > s.getLowestValueY()) {
d = s.getLowestValueY();
}
}
mCompleteRange.bottom = d;
d = series.get(0).getHighestValueY();
for (Series s : series) {
if (!s.isEmpty() && d < s.getHighestValueY()) {
d = s.getHighestValueY();
}
}
mCompleteRange.top = d;
}
}
// calc current viewport bounds
if (mYAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
}
if (mYAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
mCurrentViewport.top = mCompleteRange.top;
mCurrentViewport.bottom = mCompleteRange.bottom;
}
if (mXAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
}
if (mXAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
mCurrentViewport.left = mCompleteRange.left;
mCurrentViewport.right = mCompleteRange.right;
} else if (mXAxisBoundsManual && !mYAxisBoundsManual && mCompleteRange.width() != 0) {
// get highest/lowest of current viewport
// lowest
double d = Double.MAX_VALUE;
for (Series s : series) {
Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
while (values.hasNext()) {
double v = values.next().getY();
if (d > v) {
d = v;
}
}
}
if (d != Double.MAX_VALUE) {
mCurrentViewport.bottom = d;
}
// highest
d = Double.MIN_VALUE;
for (Series s : series) {
Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
while (values.hasNext()) {
double v = values.next().getY();
if (d < v) {
d = v;
}
}
}
if (d != Double.MIN_VALUE) {
mCurrentViewport.top = d;
}
}
// fixes blank screen when range is zero
if (mCurrentViewport.left == mCurrentViewport.right) mCurrentViewport.right++;
if (mCurrentViewport.top == mCurrentViewport.bottom) mCurrentViewport.top++;
}
/**
* @param completeRange if true => minX of the complete range of all series
* if false => minX of the current visible viewport
* @return the min x value
*/
public double getMinX(boolean completeRange) {
if (completeRange) {
return mCompleteRange.left;
} else {
return mCurrentViewport.left;
}
}
/**
* @param completeRange if true => maxX of the complete range of all series
* if false => maxX of the current visible viewport
* @return the max x value
*/
public double getMaxX(boolean completeRange) {
if (completeRange) {
return mCompleteRange.right;
} else {
return mCurrentViewport.right;
}
}
/**
* @param completeRange if true => minY of the complete range of all series
* if false => minY of the current visible viewport
* @return the min y value
*/
public double getMinY(boolean completeRange) {
if (completeRange) {
return mCompleteRange.bottom;
} else {
return mCurrentViewport.bottom;
}
}
/**
* @param completeRange if true => maxY of the complete range of all series
* if false => maxY of the current visible viewport
* @return the max y value
*/
public double getMaxY(boolean completeRange) {
if (completeRange) {
return mCompleteRange.top;
} else {
return mCurrentViewport.top;
}
}
/**
* set the maximal y value for the current viewport.
* Make sure to set the y bounds to manual via
* {@link #setYAxisBoundsManual(boolean)}
* @param y max / highest value
*/
public void setMaxY(double y) {
mCurrentViewport.top = y;
}
/**
* set the minimal y value for the current viewport.
* Make sure to set the y bounds to manual via
* {@link #setYAxisBoundsManual(boolean)}
* @param y min / lowest value
*/
public void setMinY(double y) {
mCurrentViewport.bottom = y;
}
/**
* set the maximal x value for the current viewport.
* Make sure to set the x bounds to manual via
* {@link #setXAxisBoundsManual(boolean)}
* @param x max / highest value
*/
public void setMaxX(double x) {
mCurrentViewport.right = x;
}
/**
* set the minimal x value for the current viewport.
* Make sure to set the x bounds to manual via
* {@link #setXAxisBoundsManual(boolean)}
* @param x min / lowest value
*/
public void setMinX(double x) {
mCurrentViewport.left = x;
}
/**
* release the glowing effects
*/
private void releaseEdgeEffects() {
mEdgeEffectLeft.onRelease();
mEdgeEffectRight.onRelease();
mEdgeEffectTop.onRelease();
mEdgeEffectBottom.onRelease();
}
/**
* not used currently
*
* @param velocityX
* @param velocityY
*/
private void fling(int velocityX, int velocityY) {
velocityY = 0;
releaseEdgeEffects();
// Flings use math in pixels (as opposed to math based on the viewport).
int maxX = (int)((mCurrentViewport.width()/mCompleteRange.width())*(float)mGraphView.getGraphContentWidth()) - mGraphView.getGraphContentWidth();
int maxY = (int)((mCurrentViewport.height()/mCompleteRange.height())*(float)mGraphView.getGraphContentHeight()) - mGraphView.getGraphContentHeight();
int startX = (int)((mCurrentViewport.left - mCompleteRange.left)/mCompleteRange.width())*maxX;
int startY = (int)((mCurrentViewport.top - mCompleteRange.top)/mCompleteRange.height())*maxY;
mScroller.forceFinished(true);
mScroller.fling(
startX,
startY,
velocityX,
velocityY,
0, maxX,
0, maxY,
mGraphView.getGraphContentWidth() / 2,
mGraphView.getGraphContentHeight() / 2);
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
/**
* not used currently
*/
public void computeScroll() {
}
/**
* Draws the overscroll "glow" at the four edges of the chart region, if necessary.
*
* @see EdgeEffectCompat
*/
private void drawEdgeEffectsUnclipped(Canvas canvas) {
// The methods below rotate and translate the canvas as needed before drawing the glow,
// since EdgeEffectCompat always draws a top-glow at 0,0.
boolean needsInvalidate = false;
if (!mEdgeEffectTop.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop());
mEdgeEffectTop.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
if (mEdgeEffectTop.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectBottom.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight());
canvas.rotate(180, mGraphView.getGraphContentWidth()/2, 0);
mEdgeEffectBottom.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
if (mEdgeEffectBottom.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectLeft.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop()+ mGraphView.getGraphContentHeight());
canvas.rotate(-90, 0, 0);
mEdgeEffectLeft.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
if (mEdgeEffectLeft.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectRight.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mGraphView.getGraphContentLeft()+ mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop());
canvas.rotate(90, 0, 0);
mEdgeEffectRight.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
if (mEdgeEffectRight.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(mGraphView);
}
}
/**
* will be first called in order to draw
* the canvas
* Used to draw the background
*
* @param c canvas.
*/
public void drawFirst(Canvas c) {
// draw background
if (mBackgroundColor != Color.TRANSPARENT) {
mPaint.setColor(mBackgroundColor);
c.drawRect(
mGraphView.getGraphContentLeft(),
mGraphView.getGraphContentTop(),
mGraphView.getGraphContentLeft()+mGraphView.getGraphContentWidth(),
mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(),
mPaint
);
}
if (mDrawBorder) {
Paint p;
if (mBorderPaint != null) {
p = mBorderPaint;
} else {
p = mPaint;
p.setColor(getBorderColor());
}
c.drawLine(
mGraphView.getGraphContentLeft(),
mGraphView.getGraphContentTop(),
mGraphView.getGraphContentLeft(),
mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(),
p
);
c.drawLine(
mGraphView.getGraphContentLeft(),
mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(),
mGraphView.getGraphContentLeft()+mGraphView.getGraphContentWidth(),
mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(),
p
);
// on the right side if we have second scale
if (mGraphView.mSecondScale != null) {
c.drawLine(
mGraphView.getGraphContentLeft()+mGraphView.getGraphContentWidth(),
mGraphView.getGraphContentTop(),
mGraphView.getGraphContentLeft()+mGraphView.getGraphContentWidth(),
mGraphView.getGraphContentTop()+mGraphView.getGraphContentHeight(),
p
);
}
}
}
/**
* draws the glowing edge effect
*
* @param c canvas
*/
public void draw(Canvas c) {
drawEdgeEffectsUnclipped(c);
}
/**
* @return background of the viewport area
*/
public int getBackgroundColor() {
return mBackgroundColor;
}
/**
* @param mBackgroundColor background of the viewport area
* use transparent to have no background
*/
public void setBackgroundColor(int mBackgroundColor) {
this.mBackgroundColor = mBackgroundColor;
}
/**
* @return whether the viewport is scalable
*/
public boolean isScalable() {
return mIsScalable;
}
/**
* active the scaling/zooming feature
* notice: sets the x axis bounds to manual
*
* @param mIsScalable whether the viewport is scalable
*/
public void setScalable(boolean mIsScalable) {
this.mIsScalable = mIsScalable;
if (mIsScalable) {
mIsScrollable = true;
// set viewport to manual
setXAxisBoundsManual(true);
}
}
/**
* @return whether the x axis bounds are manual.
* @see #setMinX(double)
* @see #setMaxX(double)
*/
public boolean isXAxisBoundsManual() {
return mXAxisBoundsManual;
}
/**
* @param mXAxisBoundsManual whether the x axis bounds are manual.
* @see #setMinX(double)
* @see #setMaxX(double)
*/
public void setXAxisBoundsManual(boolean mXAxisBoundsManual) {
this.mXAxisBoundsManual = mXAxisBoundsManual;
if (mXAxisBoundsManual) {
mXAxisBoundsStatus = AxisBoundsStatus.FIX;
}
}
/**
* @return whether the y axis bound are manual
*/
public boolean isYAxisBoundsManual() {
return mYAxisBoundsManual;
}
/**
* @param mYAxisBoundsManual whether the y axis bounds are manual
* @see #setMaxY(double)
* @see #setMinY(double)
*/
public void setYAxisBoundsManual(boolean mYAxisBoundsManual) {
this.mYAxisBoundsManual = mYAxisBoundsManual;
if (mYAxisBoundsManual) {
mYAxisBoundsStatus = AxisBoundsStatus.FIX;
}
}
/**
* forces the viewport to scroll to the end
* of the range by keeping the current viewport size.
*
* Important: Only takes effect if x axis bounds are manual.
*
* @see #setXAxisBoundsManual(boolean)
*/
public void scrollToEnd() {
if (mXAxisBoundsManual) {
double size = mCurrentViewport.width();
mCurrentViewport.right = mCompleteRange.right;
mCurrentViewport.left = mCompleteRange.right - size;
mGraphView.onDataChanged(true, false);
} else {
Log.w("GraphView", "scrollToEnd works only with manual x axis bounds");
}
}
/**
* @return the listener when there is one registered.
*/
public OnXAxisBoundsChangedListener getOnXAxisBoundsChangedListener() {
return mOnXAxisBoundsChangedListener;
}
/**
* set a listener to notify when x bounds changed after
* scaling or scrolling.
* This can be used to load more detailed data.
*
* @param l the listener to use
*/
public void setOnXAxisBoundsChangedListener(OnXAxisBoundsChangedListener l) {
mOnXAxisBoundsChangedListener = l;
}
/**
* optional draw a border between the labels
* and the viewport
*
* @param drawBorder true to draw the border
*/
public void setDrawBorder(boolean drawBorder) {
this.mDrawBorder = drawBorder;
}
/**
* the border color used. will be ignored when
* a custom paint is set.
*
* @see #setDrawBorder(boolean)
* @return border color. by default the grid color is used
*/
public int getBorderColor() {
if (mBorderColor != null) {
return mBorderColor;
}
return mGraphView.getGridLabelRenderer().getGridColor();
}
/**
* the border color used. will be ignored when
* a custom paint is set.
*
* @param borderColor null to reset
*/
public void setBorderColor(Integer borderColor) {
this.mBorderColor = borderColor;
}
/**
* custom paint to use for the border. border color
* will be ignored
*
* @see #setDrawBorder(boolean)
* @param borderPaint
*/
public void setBorderPaint(Paint borderPaint) {
this.mBorderPaint = borderPaint;
}
/**
* activate/deactivate the vertical scrolling
*
* @param scrollableY true to activate
*/
public void setScrollableY(boolean scrollableY) {
this.scrollableY = scrollableY;
}
/**
* the reference number to generate the labels
* @return by default 0, only when manual bounds and no human rounding
* is active, the min y value is returned
*/
protected double getReferenceY() {
// if the bounds is manual then we take the
// original manual min y value as reference
if (isYAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRounding()) {
if (Double.isNaN(referenceY)) {
referenceY = getMinY(false);
}
return referenceY;
} else {
// starting from 0 so that the steps have nice numbers
return 0;
}
}
/**
* activate or deactivate the vertical zooming/scaling functionallity.
* This will automatically activate the vertical scrolling and the
* horizontal scaling/scrolling feature.
*
* @param scalableY true to activate
*/
public void setScalableY(boolean scalableY) {
if (scalableY) {
this.scrollableY = true;
setScalable(true);
if (android.os.Build.VERSION.SDK_INT < 11) {
Log.w("GraphView", "Vertical scaling requires minimum Android 3.0 (API Level 11)");
}
}
this.scalableY = scalableY;
}
/**
* maximum allowed viewport size (horizontal)
* 0 means use the bounds of the actual data that is
* available
*/
public double getMaxXAxisSize() {
return mMaxXAxisSize;
}
/**
* maximum allowed viewport size (vertical)
* 0 means use the bounds of the actual data that is
* available
*/
public double getMaxYAxisSize() {
return mMaxYAxisSize;
}
/**
* Set the max viewport size (horizontal)
* This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
* could force the user to only be able to see 2 hours of data at a time.
* Default value is 0 (disabled)
*
* @param mMaxXAxisViewportSize maximum size of viewport
*/
public void setMaxXAxisSize(double mMaxXAxisViewportSize) {
this.mMaxXAxisSize = mMaxXAxisViewportSize;
}
/**
* Set the max viewport size (vertical)
* This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
* could force the user to only be able to see 2 hours of data at a time.
* Default value is 0 (disabled)
*
* @param mMaxYAxisViewportSize maximum size of viewport
*/
public void setMaxYAxisSize(double mMaxYAxisViewportSize) {
this.mMaxYAxisSize = mMaxYAxisViewportSize;
}
/**
* minimal viewport used for scaling and scrolling.
* this is used if the data that is available is
* less then the viewport that we want to be able to display.
*
* if Double.NaN is used, then this value is ignored
*
* @param minX
* @param maxX
* @param minY
* @param maxY
*/
public void setMinimalViewport(double minX, double maxX, double minY, double maxY) {
mMinimalViewport.set(minX, maxY, maxX, minY);
}
}