blob: ec4d38402b84cde6670b95dc65e8e51717d153a5 [file] [log] [blame]
package com.github.mikephil.charting.charts;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Environment;
import android.provider.MediaStore.Images;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.github.mikephil.charting.animation.ChartAnimator;
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.animation.EasingFunction;
import com.github.mikephil.charting.components.Legend;
import com.github.mikephil.charting.components.MarkerView;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.formatter.DefaultValueFormatter;
import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.highlight.ChartHighlighter;
import com.github.mikephil.charting.highlight.Highlight;
import com.github.mikephil.charting.highlight.Highlighter;
import com.github.mikephil.charting.interfaces.dataprovider.ChartInterface;
import com.github.mikephil.charting.interfaces.datasets.IDataSet;
import com.github.mikephil.charting.listener.ChartTouchListener;
import com.github.mikephil.charting.listener.OnChartGestureListener;
import com.github.mikephil.charting.listener.OnChartValueSelectedListener;
import com.github.mikephil.charting.renderer.DataRenderer;
import com.github.mikephil.charting.renderer.LegendRenderer;
import com.github.mikephil.charting.utils.MPPointF;
import com.github.mikephil.charting.utils.Utils;
import com.github.mikephil.charting.utils.ViewPortHandler;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Baseclass of all Chart-Views.
*
* @author Philipp Jahoda
*/
@SuppressLint("NewApi")
public abstract class Chart<T extends ChartData<? extends IDataSet<? extends Entry>>> extends
ViewGroup
implements ChartInterface {
public static final String LOG_TAG = "MPAndroidChart";
/**
* flag that indicates if logging is enabled or not
*/
protected boolean mLogEnabled = false;
/**
* object that holds all data that was originally set for the chart, before
* it was modified or any filtering algorithms had been applied
*/
protected T mData = null;
/**
* Flag that indicates if highlighting per tap (touch) is enabled
*/
protected boolean mHighLightPerTapEnabled = true;
/**
* If set to true, chart continues to scroll after touch up
*/
private boolean mDragDecelerationEnabled = true;
/**
* Deceleration friction coefficient in [0 ; 1] interval, higher values
* indicate that speed will decrease slowly, for example if it set to 0, it
* will stop immediately. 1 is an invalid value, and will be converted to
* 0.999f automatically.
*/
private float mDragDecelerationFrictionCoef = 0.9f;
/**
* default value-formatter, number of digits depends on provided chart-data
*/
protected DefaultValueFormatter mDefaultFormatter = new DefaultValueFormatter(0);
/**
* paint object used for drawing the description text in the bottom right
* corner of the chart
*/
protected Paint mDescPaint;
/**
* paint object for drawing the information text when there are no values in
* the chart
*/
protected Paint mInfoPaint;
/**
* description text that appears in the bottom right corner of the chart
*/
protected String mDescription = "Description";
/**
* the object representing the labels on the x-axis
*/
protected XAxis mXAxis;
/**
* if true, touch gestures are enabled on the chart
*/
protected boolean mTouchEnabled = true;
/**
* the legend object containing all data associated with the legend
*/
protected Legend mLegend;
/**
* listener that is called when a value on the chart is selected
*/
protected OnChartValueSelectedListener mSelectionListener;
protected ChartTouchListener mChartTouchListener;
/**
* text that is displayed when the chart is empty
*/
private String mNoDataText = "No chart data available.";
/**
* Gesture listener for custom callbacks when making gestures on the chart.
*/
private OnChartGestureListener mGestureListener;
/**
* text that is displayed when the chart is empty that describes why the
* chart is empty
*/
private String mNoDataTextDescription;
protected LegendRenderer mLegendRenderer;
/**
* object responsible for rendering the data
*/
protected DataRenderer mRenderer;
protected Highlighter mHighlighter;
/**
* object that manages the bounds and drawing constraints of the chart
*/
protected ViewPortHandler mViewPortHandler = new ViewPortHandler();
/**
* object responsible for animations
*/
protected ChartAnimator mAnimator;
/**
* Extra offsets to be appended to the viewport
*/
private float mExtraTopOffset = 0.f,
mExtraRightOffset = 0.f,
mExtraBottomOffset = 0.f,
mExtraLeftOffset = 0.f;
/**
* default constructor for initialization in code
*/
public Chart(Context context) {
super(context);
init();
}
/**
* constructor for initialization in xml
*/
public Chart(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* even more awesome constructor
*/
public Chart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* initialize all paints and stuff
*/
protected void init() {
setWillNotDraw(false);
// setLayerType(View.LAYER_TYPE_HARDWARE, null);
if (android.os.Build.VERSION.SDK_INT < 11)
mAnimator = new ChartAnimator();
else
mAnimator = new ChartAnimator(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// ViewCompat.postInvalidateOnAnimation(Chart.this);
postInvalidate();
}
});
// initialize the utils
Utils.init(getContext());
mMaxHighlightDistance = Utils.convertDpToPixel(500f);
mLegend = new Legend();
mLegendRenderer = new LegendRenderer(mViewPortHandler, mLegend);
mXAxis = new XAxis();
mDescPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDescPaint.setColor(Color.BLACK);
mDescPaint.setTextAlign(Align.RIGHT);
mDescPaint.setTextSize(Utils.convertDpToPixel(9f));
mInfoPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mInfoPaint.setColor(Color.rgb(247, 189, 51)); // orange
mInfoPaint.setTextAlign(Align.CENTER);
mInfoPaint.setTextSize(Utils.convertDpToPixel(12f));
mDrawPaint = new Paint(Paint.DITHER_FLAG);
if (mLogEnabled)
Log.i("", "Chart.init()");
}
// public void initWithDummyData() {
// ColorTemplate template = new ColorTemplate();
// template.addColorsForDataSets(ColorTemplate.COLORFUL_COLORS,
// getContext());
//
// setColorTemplate(template);
// setDrawYValues(false);
//
// ArrayList<String> xVals = new ArrayList<String>();
// Calendar calendar = Calendar.getInstance();
// for (int i = 0; i < 12; i++) {
// xVals.add(calendar.getDisplayName(Calendar.MONTH, Calendar.SHORT,
// Locale.getDefault()));
// }
//
// ArrayList<DataSet> dataSets = new ArrayList<DataSet>();
// for (int i = 0; i < 3; i++) {
//
// ArrayList<Entry> yVals = new ArrayList<Entry>();
//
// for (int j = 0; j < 12; j++) {
// float val = (float) (Math.random() * 100);
// yVals.add(new Entry(val, j));
// }
//
// DataSet set = new DataSet(yVals, "DataSet " + i);
// dataSets.add(set); // add the datasets
// }
// // create a data object with the datasets
// ChartData data = new ChartData(xVals, dataSets);
// setData(data);
// invalidate();
// }
/**
* Sets a new data object for the chart. The data object contains all values
* and information needed for displaying.
*
* @param data
*/
public void setData(T data) {
mData = data;
mOffsetsCalculated = false;
if (data == null) {
return;
}
// calculate how many digits are needed
setupDefaultFormatter(data.getYMin(), data.getYMax());
IDataSet set;
final List sets = mData.getDataSets();
final int count = sets.size();
for (int i = 0; i < count; i++) {
set = (IDataSet) sets.get(i);
if (set.needsFormatter() || set.getValueFormatter() == mDefaultFormatter)
set.setValueFormatter(mDefaultFormatter);
}
// let the chart know there is new data
notifyDataSetChanged();
if (mLogEnabled)
Log.i(LOG_TAG, "Data is set.");
}
/**
* Clears the chart from all data (sets it to null) and refreshes it (by
* calling invalidate()).
*/
public void clear() {
mData = null;
mOffsetsCalculated = false;
mIndicesToHighlight = null;
invalidate();
}
/**
* Removes all DataSets (and thereby Entries) from the chart. Does not set the data object to null. Also refreshes the
* chart by calling invalidate().
*/
public void clearValues() {
mData.clearValues();
invalidate();
}
/**
* Returns true if the chart is empty (meaning it's data object is either
* null or contains no entries).
*
* @return
*/
public boolean isEmpty() {
if (mData == null)
return true;
else {
if (mData.getEntryCount() <= 0)
return true;
else
return false;
}
}
/**
* Lets the chart know its underlying data has changed and performs all
* necessary recalculations. It is crucial that this method is called
* everytime data is changed dynamically. Not calling this method can lead
* to crashes or unexpected behaviour.
*/
public abstract void notifyDataSetChanged();
/**
* calculates the offsets of the chart to the border depending on the
* position of an eventual legend or depending on the length of the y-axis
* and x-axis labels and their position
*/
protected abstract void calculateOffsets();
/**
* calcualtes the y-min and y-max value and the y-delta and x-delta value
*/
protected abstract void calcMinMax();
/**
* Calculates the required number of digits for the values that might be
* drawn in the chart (if enabled), and creates the default-value-formatter
*/
protected void setupDefaultFormatter(float min, float max) {
float reference = 0f;
if (mData == null || mData.getEntryCount() < 2) {
reference = Math.max(Math.abs(min), Math.abs(max));
} else {
reference = Math.abs(max - min);
}
int digits = Utils.getDecimals(reference);
// setup the formatter with a new number of digits
mDefaultFormatter.setup(digits);
}
/**
* flag that indicates if offsets calculation has already been done or not
*/
private boolean mOffsetsCalculated = false;
/**
* paint object used for drawing the bitmap
*/
protected Paint mDrawPaint;
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
if (mData == null) {
boolean hasText = !TextUtils.isEmpty(mNoDataText);
boolean hasDescription = !TextUtils.isEmpty(mNoDataTextDescription);
float line1height = hasText ? Utils.calcTextHeight(mInfoPaint, mNoDataText) : 0.f;
float line2height = hasDescription ? Utils.calcTextHeight(mInfoPaint, mNoDataTextDescription) : 0.f;
float lineSpacing = (hasText && hasDescription) ?
(mInfoPaint.getFontSpacing() - line1height) : 0.f;
// if no data, inform the user
float y = (getHeight() -
(line1height + lineSpacing + line2height)) / 2.f
+ line1height;
if (hasText) {
canvas.drawText(mNoDataText, getWidth() / 2, y, mInfoPaint);
if (hasDescription) {
y = y + line1height + lineSpacing;
}
}
if (hasDescription) {
canvas.drawText(mNoDataTextDescription, getWidth() / 2, y, mInfoPaint);
}
return;
}
if (!mOffsetsCalculated) {
calculateOffsets();
mOffsetsCalculated = true;
}
}
/**
* the custom position of the description text
*/
private MPPointF mDescriptionPosition;
/**
* draws the description text in the bottom right corner of the chart
*/
protected void drawDescription(Canvas c) {
if (!mDescription.equals("")) {
if (mDescriptionPosition == null) {
c.drawText(mDescription, getWidth() - mViewPortHandler.offsetRight() - 10,
getHeight() - mViewPortHandler.offsetBottom()
- 10, mDescPaint);
} else {
c.drawText(mDescription, mDescriptionPosition.x, mDescriptionPosition.y, mDescPaint);
}
}
}
/**
* ################ ################ ################ ################
*/
/** BELOW THIS CODE FOR HIGHLIGHTING */
/**
* array of Highlight objects that reference the highlighted slices in the
* chart
*/
protected Highlight[] mIndicesToHighlight;
/**
* The maximum distance in dp away from an entry causing it to highlight.
*/
protected float mMaxHighlightDistance = 0f;
@Override
public float getMaxHighlightDistance() {
return mMaxHighlightDistance;
}
/**
* Sets the maximum distance in screen dp a touch can be away from an entry to cause it to get highlighted.
* Default: 500dp
*
* @param distDp
*/
public void setMaxHighlightDistance(float distDp) {
mMaxHighlightDistance = Utils.convertDpToPixel(distDp);
}
/**
* Returns the array of currently highlighted values. This might a null or
* empty array if nothing is highlighted.
*
* @return
*/
public Highlight[] getHighlighted() {
return mIndicesToHighlight;
}
/**
* Returns true if values can be highlighted via tap gesture, false if not.
*
* @return
*/
public boolean isHighlightPerTapEnabled() {
return mHighLightPerTapEnabled;
}
/**
* Set this to false to prevent values from being highlighted by tap gesture.
* Values can still be highlighted via drag or programmatically. Default: true
*
* @param enabled
*/
public void setHighlightPerTapEnabled(boolean enabled) {
mHighLightPerTapEnabled = enabled;
}
/**
* Returns true if there are values to highlight, false if there are no
* values to highlight. Checks if the highlight array is null, has a length
* of zero or if the first object is null.
*
* @return
*/
public boolean valuesToHighlight() {
return mIndicesToHighlight == null || mIndicesToHighlight.length <= 0
|| mIndicesToHighlight[0] == null ? false
: true;
}
/**
* Sets the last highlighted value for the touchlistener.
*
* @param highs
*/
protected void setLastHighlighted(Highlight[] highs) {
if (highs == null || highs.length <= 0 || highs[0] == null) {
mChartTouchListener.setLastHighlighted(null);
} else {
mChartTouchListener.setLastHighlighted(highs[0]);
}
}
/**
* Highlights the values at the given indices in the given DataSets. Provide
* null or an empty array to undo all highlighting. This should be used to
* programmatically highlight values. This DOES NOT generate a callback to
* the OnChartValueSelectedListener.
*
* @param highs
*/
public void highlightValues(Highlight[] highs) {
// set the indices to highlight
mIndicesToHighlight = highs;
setLastHighlighted(highs);
// redraw the chart
invalidate();
}
/**
* Highlights the value at the given x-position in the given DataSet. Provide
* -1 as the dataSetIndex to undo all highlighting. This will trigger a callback to the OnChartValueSelectedListener.
*
* @param x
* @param dataSetIndex
*/
public void highlightValue(float x, int dataSetIndex) {
highlightValue(x, dataSetIndex, true);
}
/**
* Highlights the value at the given x position in the given DataSet. Provide
* -1 as the dataSetIndex to undo all highlighting.
*
* @param x
* @param dataSetIndex
*/
public void highlightValue(float x, int dataSetIndex, boolean callListener) {
if (dataSetIndex < 0 || dataSetIndex >= mData.getDataSetCount()) {
highlightValue(null, callListener);
} else {
highlightValue(new Highlight(x, dataSetIndex), callListener);
}
}
/**
* Highlights the values represented by the provided Highlight object
* This DOES NOT generate a callback to the OnChartValueSelectedListener.
*
* @param highlight contains information about which entry should be highlighted
*/
public void highlightValue(Highlight highlight) {
highlightValue(highlight, false);
}
/**
* Highlights the value selected by touch gesture. Unlike
* highlightValues(...), this generates a callback to the
* OnChartValueSelectedListener.
*
* @param high - the highlight object
* @param callListener - call the listener
*/
public void highlightValue(Highlight high, boolean callListener) {
Entry e = null;
if (high == null)
mIndicesToHighlight = null;
else {
if (mLogEnabled)
Log.i(LOG_TAG, "Highlighted: " + high.toString());
e = mData.getEntryForHighlight(high);
if (e == null) {
mIndicesToHighlight = null;
high = null;
} else {
// set the indices to highlight
mIndicesToHighlight = new Highlight[]{
high
};
}
}
setLastHighlighted(mIndicesToHighlight);
if (callListener && mSelectionListener != null) {
if (!valuesToHighlight())
mSelectionListener.onNothingSelected();
else {
// notify the listener
mSelectionListener.onValueSelected(e, high);
}
}
// redraw the chart
invalidate();
}
/**
* Returns the Highlight object (contains x-index and DataSet index) of the
* selected value at the given touch point inside the Line-, Scatter-, or
* CandleStick-Chart.
*
* @param x
* @param y
* @return
*/
public Highlight getHighlightByTouchPoint(float x, float y) {
if (mData == null) {
Log.e(LOG_TAG, "Can't select by touch. No data set.");
return null;
} else
return getHighlighter().getHighlight(x, y);
}
/**
* Set a new (e.g. custom) ChartTouchListener NOTE: make sure to
* setTouchEnabled(true); if you need touch gestures on the chart
*
* @param l
*/
public void setOnTouchListener(ChartTouchListener l) {
this.mChartTouchListener = l;
}
/**
* Returns an instance of the currently active touch listener.
*
* @return
*/
public ChartTouchListener getOnTouchListener() {
return mChartTouchListener;
}
/**
* ################ ################ ################ ################
*/
/** BELOW CODE IS FOR THE MARKER VIEW */
/**
* if set to true, the marker view is drawn when a value is clicked
*/
protected boolean mDrawMarkerViews = true;
/**
* the view that represents the marker
*/
protected MarkerView mMarkerView;
/**
* draws all MarkerViews on the highlighted positions
*/
protected void drawMarkers(Canvas canvas) {
// if there is no marker view or drawing marker is disabled
if (mMarkerView == null || !mDrawMarkerViews || !valuesToHighlight())
return;
for (int i = 0; i < mIndicesToHighlight.length; i++) {
Highlight highlight = mIndicesToHighlight[i];
IDataSet set = mData.getDataSetByIndex(highlight.getDataSetIndex());
Entry e = mData.getEntryForHighlight(mIndicesToHighlight[i]);
int entryIndex = set.getEntryIndex(e);
// make sure entry not null
if (e == null || entryIndex > set.getEntryCount() * mAnimator.getPhaseX())
continue;
float[] pos = getMarkerPosition(highlight);
// check bounds
if (!mViewPortHandler.isInBounds(pos[0], pos[1]))
continue;
// callbacks to update the content
mMarkerView.refreshContent(e, highlight);
mMarkerView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mMarkerView.layout(0, 0, mMarkerView.getMeasuredWidth(),
mMarkerView.getMeasuredHeight());
if (pos[1] - mMarkerView.getHeight() <= 0) {
float y = mMarkerView.getHeight() - pos[1];
mMarkerView.draw(canvas, pos[0], pos[1] + y);
} else {
mMarkerView.draw(canvas, pos[0], pos[1]);
}
}
}
/**
* Returns the actual position in pixels of the MarkerView for the given
* Highlight object.
*
* @param high
* @return
*/
protected float[] getMarkerPosition(Highlight high) {
return new float[]{high.getDrawX(), high.getDrawY()};
}
/**
* ################ ################ ################ ################
* ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER.
*/
/** CODE BELOW THIS RELATED TO ANIMATION */
/**
* Returns the animator responsible for animating chart values.
*
* @return
*/
public ChartAnimator getAnimator() {
return mAnimator;
}
/**
* If set to true, chart continues to scroll after touch up default: true
*/
public boolean isDragDecelerationEnabled() {
return mDragDecelerationEnabled;
}
/**
* If set to true, chart continues to scroll after touch up. Default: true.
*
* @param enabled
*/
public void setDragDecelerationEnabled(boolean enabled) {
mDragDecelerationEnabled = enabled;
}
/**
* Returns drag deceleration friction coefficient
*
* @return
*/
public float getDragDecelerationFrictionCoef() {
return mDragDecelerationFrictionCoef;
}
/**
* Deceleration friction coefficient in [0 ; 1] interval, higher values
* indicate that speed will decrease slowly, for example if it set to 0, it
* will stop immediately. 1 is an invalid value, and will be converted to
* 0.999f automatically.
*
* @param newValue
*/
public void setDragDecelerationFrictionCoef(float newValue) {
if (newValue < 0.f)
newValue = 0.f;
if (newValue >= 1f)
newValue = 0.999f;
mDragDecelerationFrictionCoef = newValue;
}
/**
* ################ ################ ################ ################
* ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER.
*/
/** CODE BELOW FOR PROVIDING EASING FUNCTIONS */
/**
* Animates the drawing / rendering of the chart on both x- and y-axis with
* the specified animation time. If animate(...) is called, no further
* calling of invalidate() is necessary to refresh the chart. ANIMATIONS
* ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillisX
* @param durationMillisY
* @param easingX a custom easing function to be used on the animation phase
* @param easingY a custom easing function to be used on the animation phase
*/
public void animateXY(int durationMillisX, int durationMillisY, EasingFunction easingX,
EasingFunction easingY) {
mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
/**
* Animates the rendering of the chart on the x-axis with the specified
* animation time. If animate(...) is called, no further calling of
* invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR
* API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillis
* @param easing a custom easing function to be used on the animation phase
*/
public void animateX(int durationMillis, EasingFunction easing) {
mAnimator.animateX(durationMillis, easing);
}
/**
* Animates the rendering of the chart on the y-axis with the specified
* animation time. If animate(...) is called, no further calling of
* invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR
* API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillis
* @param easing a custom easing function to be used on the animation phase
*/
public void animateY(int durationMillis, EasingFunction easing) {
mAnimator.animateY(durationMillis, easing);
}
/**
* ################ ################ ################ ################
* ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER.
*/
/** CODE BELOW FOR PREDEFINED EASING OPTIONS */
/**
* Animates the drawing / rendering of the chart on both x- and y-axis with
* the specified animation time. If animate(...) is called, no further
* calling of invalidate() is necessary to refresh the chart. ANIMATIONS
* ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillisX
* @param durationMillisY
* @param easingX a predefined easing option
* @param easingY a predefined easing option
*/
public void animateXY(int durationMillisX, int durationMillisY, Easing.EasingOption easingX,
Easing.EasingOption easingY) {
mAnimator.animateXY(durationMillisX, durationMillisY, easingX, easingY);
}
/**
* Animates the rendering of the chart on the x-axis with the specified
* animation time. If animate(...) is called, no further calling of
* invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR
* API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillis
* @param easing a predefined easing option
*/
public void animateX(int durationMillis, Easing.EasingOption easing) {
mAnimator.animateX(durationMillis, easing);
}
/**
* Animates the rendering of the chart on the y-axis with the specified
* animation time. If animate(...) is called, no further calling of
* invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR
* API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillis
* @param easing a predefined easing option
*/
public void animateY(int durationMillis, Easing.EasingOption easing) {
mAnimator.animateY(durationMillis, easing);
}
/**
* ################ ################ ################ ################
* ANIMATIONS ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER.
*/
/** CODE BELOW FOR ANIMATIONS WITHOUT EASING */
/**
* Animates the rendering of the chart on the x-axis with the specified
* animation time. If animate(...) is called, no further calling of
* invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR
* API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillis
*/
public void animateX(int durationMillis) {
mAnimator.animateX(durationMillis);
}
/**
* Animates the rendering of the chart on the y-axis with the specified
* animation time. If animate(...) is called, no further calling of
* invalidate() is necessary to refresh the chart. ANIMATIONS ONLY WORK FOR
* API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillis
*/
public void animateY(int durationMillis) {
mAnimator.animateY(durationMillis);
}
/**
* Animates the drawing / rendering of the chart on both x- and y-axis with
* the specified animation time. If animate(...) is called, no further
* calling of invalidate() is necessary to refresh the chart. ANIMATIONS
* ONLY WORK FOR API LEVEL 11 (Android 3.0.x) AND HIGHER.
*
* @param durationMillisX
* @param durationMillisY
*/
public void animateXY(int durationMillisX, int durationMillisY) {
mAnimator.animateXY(durationMillisX, durationMillisY);
}
/**
* ################ ################ ################ ################
*/
/** BELOW THIS ONLY GETTERS AND SETTERS */
/**
* Returns the object representing all x-labels, this method can be used to
* acquire the XAxis object and modify it (e.g. change the position of the
* labels, styling, etc.)
*
* @return
*/
public XAxis getXAxis() {
return mXAxis;
}
/**
* Returns the default ValueFormatter that has been determined by the chart
* considering the provided minimum and maximum values.
*
* @return
*/
public ValueFormatter getDefaultValueFormatter() {
return mDefaultFormatter;
}
/**
* set a selection listener for the chart
*
* @param l
*/
public void setOnChartValueSelectedListener(OnChartValueSelectedListener l) {
this.mSelectionListener = l;
}
/**
* Sets a gesture-listener for the chart for custom callbacks when executing
* gestures on the chart surface.
*
* @param l
*/
public void setOnChartGestureListener(OnChartGestureListener l) {
this.mGestureListener = l;
}
/**
* Returns the custom gesture listener.
*
* @return
*/
public OnChartGestureListener getOnChartGestureListener() {
return mGestureListener;
}
/**
* returns the current y-max value across all DataSets
*
* @return
*/
public float getYMax() {
return mData.getYMax();
}
/**
* returns the current y-min value across all DataSets
*
* @return
*/
public float getYMin() {
return mData.getYMin();
}
@Override
public float getXChartMax() {
return mXAxis.mAxisMaximum;
}
@Override
public float getXChartMin() {
return mXAxis.mAxisMinimum;
}
@Override
public float getXRange() {
return mXAxis.mAxisRange;
}
/**
* Returns a recyclable MPPointF instance.
* Returns the center point of the chart (the whole View) in pixels.
*
* @return
*/
public MPPointF getCenter() {
return MPPointF.getInstance(getWidth() / 2f, getHeight() / 2f);
}
/**
* Returns a recyclable MPPointF instance.
* Returns the center of the chart taking offsets under consideration.
* (returns the center of the content rectangle)
*
* @return
*/
@Override
public MPPointF getCenterOffsets() {
return mViewPortHandler.getContentCenter();
}
/**
* set a description text that appears in the bottom right corner of the
* chart, size = Y-legend text size
*
* @param desc
*/
public void setDescription(String desc) {
if (desc == null)
desc = "";
this.mDescription = desc;
}
/**
* Sets a custom position for the description text in pixels on the screen.
*
* @param x - xcoordinate
* @param y - ycoordinate
*/
public void setDescriptionPosition(float x, float y) {
if (mDescriptionPosition == null) {
mDescriptionPosition = MPPointF.getInstance(x, y);
} else {
mDescriptionPosition.x = x;
mDescriptionPosition.y = y;
}
}
/**
* sets the typeface for the description paint
*
* @param t
*/
public void setDescriptionTypeface(Typeface t) {
mDescPaint.setTypeface(t);
}
/**
* sets the size of the description text in pixels, min 6f, max 16f
*
* @param size
*/
public void setDescriptionTextSize(float size) {
if (size > 16f)
size = 16f;
if (size < 6f)
size = 6f;
mDescPaint.setTextSize(Utils.convertDpToPixel(size));
}
/**
* Sets the color of the description text.
*
* @param color
*/
public void setDescriptionColor(int color) {
mDescPaint.setColor(color);
}
/**
* Sets extra offsets (around the chart view) to be appended to the
* auto-calculated offsets.
*
* @param left
* @param top
* @param right
* @param bottom
*/
public void setExtraOffsets(float left, float top, float right, float bottom) {
setExtraLeftOffset(left);
setExtraTopOffset(top);
setExtraRightOffset(right);
setExtraBottomOffset(bottom);
}
/**
* Set an extra offset to be appended to the viewport's top
*/
public void setExtraTopOffset(float offset) {
mExtraTopOffset = Utils.convertDpToPixel(offset);
}
/**
* @return the extra offset to be appended to the viewport's top
*/
public float getExtraTopOffset() {
return mExtraTopOffset;
}
/**
* Set an extra offset to be appended to the viewport's right
*/
public void setExtraRightOffset(float offset) {
mExtraRightOffset = Utils.convertDpToPixel(offset);
}
/**
* @return the extra offset to be appended to the viewport's right
*/
public float getExtraRightOffset() {
return mExtraRightOffset;
}
/**
* Set an extra offset to be appended to the viewport's bottom
*/
public void setExtraBottomOffset(float offset) {
mExtraBottomOffset = Utils.convertDpToPixel(offset);
}
/**
* @return the extra offset to be appended to the viewport's bottom
*/
public float getExtraBottomOffset() {
return mExtraBottomOffset;
}
/**
* Set an extra offset to be appended to the viewport's left
*/
public void setExtraLeftOffset(float offset) {
mExtraLeftOffset = Utils.convertDpToPixel(offset);
}
/**
* @return the extra offset to be appended to the viewport's left
*/
public float getExtraLeftOffset() {
return mExtraLeftOffset;
}
/**
* Set this to true to enable logcat outputs for the chart. Beware that
* logcat output decreases rendering performance. Default: disabled.
*
* @param enabled
*/
public void setLogEnabled(boolean enabled) {
mLogEnabled = enabled;
}
/**
* Returns true if log-output is enabled for the chart, fals if not.
*
* @return
*/
public boolean isLogEnabled() {
return mLogEnabled;
}
/**
* Sets the text that informs the user that there is no data available with
* which to draw the chart.
*
* @param text
*/
public void setNoDataText(String text) {
mNoDataText = text;
}
/**
* Sets the color of the no data text.
*
* @param color
*/
public void setNoDataTextColor(int color) {
mInfoPaint.setColor(color);
}
/**
* Sets descriptive text to explain to the user why there is no chart
* available Defaults to empty if not set
*
* @param text
*/
public void setNoDataTextDescription(String text) {
mNoDataTextDescription = text;
}
/**
* Set this to false to disable all gestures and touches on the chart,
* default: true
*
* @param enabled
*/
public void setTouchEnabled(boolean enabled) {
this.mTouchEnabled = enabled;
}
/**
* sets the view that is displayed when a value is clicked on the chart
*
* @param v
*/
public void setMarkerView(MarkerView v) {
mMarkerView = v;
}
/**
* returns the view that is set as a marker view for the chart
*
* @return
*/
public MarkerView getMarkerView() {
return mMarkerView;
}
/**
* Returns the Legend object of the chart. This method can be used to get an
* instance of the legend in order to customize the automatically generated
* Legend.
*
* @return
*/
public Legend getLegend() {
return mLegend;
}
/**
* Returns the renderer object responsible for rendering / drawing the
* Legend.
*
* @return
*/
public LegendRenderer getLegendRenderer() {
return mLegendRenderer;
}
/**
* Returns the rectangle that defines the borders of the chart-value surface
* (into which the actual values are drawn).
*
* @return
*/
@Override
public RectF getContentRect() {
return mViewPortHandler.getContentRect();
}
/**
* disables intercept touchevents
*/
public void disableScroll() {
ViewParent parent = getParent();
if (parent != null)
parent.requestDisallowInterceptTouchEvent(true);
}
/**
* enables intercept touchevents
*/
public void enableScroll() {
ViewParent parent = getParent();
if (parent != null)
parent.requestDisallowInterceptTouchEvent(false);
}
/**
* paint for the grid background (only line and barchart)
*/
public static final int PAINT_GRID_BACKGROUND = 4;
/**
* paint for the info text that is displayed when there are no values in the
* chart
*/
public static final int PAINT_INFO = 7;
/**
* paint for the description text in the bottom right corner
*/
public static final int PAINT_DESCRIPTION = 11;
/**
* paint for the hole in the middle of the pie chart
*/
public static final int PAINT_HOLE = 13;
/**
* paint for the text in the middle of the pie chart
*/
public static final int PAINT_CENTER_TEXT = 14;
/**
* paint used for the legend
*/
public static final int PAINT_LEGEND_LABEL = 18;
/**
* set a new paint object for the specified parameter in the chart e.g.
* Chart.PAINT_VALUES
*
* @param p the new paint object
* @param which Chart.PAINT_VALUES, Chart.PAINT_GRID, Chart.PAINT_VALUES,
* ...
*/
public void setPaint(Paint p, int which) {
switch (which) {
case PAINT_INFO:
mInfoPaint = p;
break;
case PAINT_DESCRIPTION:
mDescPaint = p;
break;
}
}
/**
* Returns the paint object associated with the provided constant.
*
* @param which e.g. Chart.PAINT_LEGEND_LABEL
* @return
*/
public Paint getPaint(int which) {
switch (which) {
case PAINT_INFO:
return mInfoPaint;
case PAINT_DESCRIPTION:
return mDescPaint;
}
return null;
}
/**
* returns true if drawing the marker-view is enabled when tapping on values
* (use the setMarkerView(View v) method to specify a marker view)
*
* @return
*/
public boolean isDrawMarkerViewEnabled() {
return mDrawMarkerViews;
}
/**
* Set this to true to draw a user specified marker-view when tapping on
* chart values (use the setMarkerView(MarkerView mv) method to specify a
* marker view). Default: true
*
* @param enabled
*/
public void setDrawMarkerViews(boolean enabled) {
mDrawMarkerViews = enabled;
}
/**
* Returns the ChartData object that has been set for the chart.
*
* @return
*/
public T getData() {
return mData;
}
/**
* Returns the ViewPortHandler of the chart that is responsible for the
* content area of the chart and its offsets and dimensions.
*
* @return
*/
public ViewPortHandler getViewPortHandler() {
return mViewPortHandler;
}
/**
* Returns the Renderer object the chart uses for drawing data.
*
* @return
*/
public DataRenderer getRenderer() {
return mRenderer;
}
/**
* Sets a new DataRenderer object for the chart.
*
* @param renderer
*/
public void setRenderer(DataRenderer renderer) {
if (renderer != null)
mRenderer = renderer;
}
public Highlighter getHighlighter() {
return mHighlighter;
}
/**
* Sets a custom highligher object for the chart that handles / processes
* all highlight touch events performed on the chart-view.
*
* @param highlighter
*/
public void setHighlighter(ChartHighlighter highlighter) {
mHighlighter = highlighter;
}
/**
* Returns a recyclable MPPointF instance.
*
* @return
*/
@Override
public MPPointF getCenterOfView() {
return getCenter();
}
/**
* Returns the bitmap that represents the chart.
*
* @return
*/
public Bitmap getChartBitmap() {
// Define a bitmap with the same size as the view
Bitmap returnedBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565);
// Bind a canvas to it
Canvas canvas = new Canvas(returnedBitmap);
// Get the view's background
Drawable bgDrawable = getBackground();
if (bgDrawable != null)
// has background drawable, then draw it on the canvas
bgDrawable.draw(canvas);
else
// does not have background drawable, then draw white background on
// the canvas
canvas.drawColor(Color.WHITE);
// draw the view on the canvas
draw(canvas);
// return the bitmap
return returnedBitmap;
}
/**
* Saves the current chart state with the given name to the given path on
* the sdcard leaving the path empty "" will put the saved file directly on
* the SD card chart is saved as a PNG image, example:
* saveToPath("myfilename", "foldername1/foldername2");
*
* @param title
* @param pathOnSD e.g. "folder1/folder2/folder3"
* @return returns true on success, false on error
*/
public boolean saveToPath(String title, String pathOnSD) {
Bitmap b = getChartBitmap();
OutputStream stream = null;
try {
stream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath()
+ pathOnSD + "/" + title
+ ".png");
/*
* Write bitmap to file using JPEG or PNG and 40% quality hint for
* JPEG.
*/
b.compress(CompressFormat.PNG, 40, stream);
stream.close();
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* Saves the current state of the chart to the gallery as an image type. The
* compression must be set for JPEG only. 0 == maximum compression, 100 = low
* compression (high quality). NOTE: Needs permission WRITE_EXTERNAL_STORAGE
*
* @param fileName e.g. "my_image"
* @param subFolderPath e.g. "ChartPics"
* @param fileDescription e.g. "Chart details"
* @param format e.g. Bitmap.CompressFormat.PNG
* @param quality e.g. 50, min = 0, max = 100
* @return returns true if saving was successful, false if not
*/
public boolean saveToGallery(String fileName, String subFolderPath, String fileDescription, Bitmap.CompressFormat
format, int quality) {
// restrain quality
if (quality < 0 || quality > 100)
quality = 50;
long currentTime = System.currentTimeMillis();
File extBaseDir = Environment.getExternalStorageDirectory();
File file = new File(extBaseDir.getAbsolutePath() + "/DCIM/" + subFolderPath);
if (!file.exists()) {
if (!file.mkdirs()) {
return false;
}
}
String mimeType = "";
switch (format) {
case PNG:
mimeType = "image/png";
if (!fileName.endsWith(".png"))
fileName += ".png";
break;
case WEBP:
mimeType = "image/webp";
if (!fileName.endsWith(".webp"))
fileName += ".webp";
break;
case JPEG:
default:
mimeType = "image/jpeg";
if (!(fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")))
fileName += ".jpg";
break;
}
String filePath = file.getAbsolutePath() + "/" + fileName;
FileOutputStream out = null;
try {
out = new FileOutputStream(filePath);
Bitmap b = getChartBitmap();
b.compress(format, quality, out);
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
long size = new File(filePath).length();
ContentValues values = new ContentValues(8);
// store the details
values.put(Images.Media.TITLE, fileName);
values.put(Images.Media.DISPLAY_NAME, fileName);
values.put(Images.Media.DATE_ADDED, currentTime);
values.put(Images.Media.MIME_TYPE, mimeType);
values.put(Images.Media.DESCRIPTION, fileDescription);
values.put(Images.Media.ORIENTATION, 0);
values.put(Images.Media.DATA, filePath);
values.put(Images.Media.SIZE, size);
return getContext().getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values) != null;
}
/**
* Saves the current state of the chart to the gallery as a JPEG image. The
* filename and compression can be set. 0 == maximum compression, 100 = low
* compression (high quality). NOTE: Needs permission WRITE_EXTERNAL_STORAGE
*
* @param fileName e.g. "my_image"
* @param quality e.g. 50, min = 0, max = 100
* @return returns true if saving was successful, false if not
*/
public boolean saveToGallery(String fileName, int quality) {
return saveToGallery(fileName, "", "MPAndroidChart-Library Save", Bitmap.CompressFormat.JPEG, quality);
}
/**
* tasks to be done after the view is setup
*/
protected ArrayList<Runnable> mJobs = new ArrayList<Runnable>();
public void removeViewportJob(Runnable job) {
mJobs.remove(job);
}
public void clearAllViewportJobs() {
mJobs.clear();
}
/**
* Either posts a job immediately if the chart has already setup it's
* dimensions or adds the job to the execution queue.
*
* @param job
*/
public void addViewportJob(Runnable job) {
if (mViewPortHandler.hasChartDimens()) {
post(job);
} else {
mJobs.add(job);
}
}
/**
* Returns all jobs that are scheduled to be executed after
* onSizeChanged(...).
*
* @return
*/
public ArrayList<Runnable> getJobs() {
return mJobs;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).layout(left, top, right, bottom);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int size = (int) Utils.convertDpToPixel(50f);
setMeasuredDimension(
Math.max(getSuggestedMinimumWidth(),
resolveSize(size,
widthMeasureSpec)),
Math.max(getSuggestedMinimumHeight(),
resolveSize(size,
heightMeasureSpec)));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (mLogEnabled)
Log.i(LOG_TAG, "OnSizeChanged()");
if (w > 0 && h > 0 && w < 10000 && h < 10000) {
mViewPortHandler.setChartDimens(w, h);
if (mLogEnabled)
Log.i(LOG_TAG, "Setting chart dimens, width: " + w + ", height: " + h);
for (Runnable r : mJobs) {
post(r);
}
mJobs.clear();
}
notifyDataSetChanged();
super.onSizeChanged(w, h, oldw, oldh);
}
/**
* Setting this to true will set the layer-type HARDWARE for the view, false
* will set layer-type SOFTWARE.
*
* @param enabled
*/
public void setHardwareAccelerationEnabled(boolean enabled) {
if (android.os.Build.VERSION.SDK_INT >= 11) {
if (enabled)
setLayerType(View.LAYER_TYPE_HARDWARE, null);
else
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
} else {
Log.e(LOG_TAG,
"Cannot enable/disable hardware acceleration for devices below API level 11.");
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//Log.i(LOG_TAG, "Detaching...");
if (mUnbind)
unbindDrawables(this);
}
/**
* unbind flag
*/
private boolean mUnbind = false;
/**
* Unbind all drawables to avoid memory leaks.
* Link: http://stackoverflow.com/a/6779164/1590502
*
* @param view
*/
private void unbindDrawables(View view) {
if (view.getBackground() != null) {
view.getBackground().setCallback(null);
}
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
unbindDrawables(((ViewGroup) view).getChildAt(i));
}
((ViewGroup) view).removeAllViews();
}
}
/**
* Set this to true to enable "unbinding" of drawables. When a View is detached
* from a window. This helps avoid memory leaks.
* Default: false
* Link: http://stackoverflow.com/a/6779164/1590502
*
* @param enabled
*/
public void setUnbindEnabled(boolean enabled) {
this.mUnbind = enabled;
}
}