package com.github.mikephil.charting.charts;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.components.Legend.LegendPosition;
import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.DataSet;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.listener.PieRadarChartTouchListener;
import com.github.mikephil.charting.utils.SelInfo;
import com.github.mikephil.charting.utils.Utils;
import java.util.ArrayList;
import java.util.List;
/**
* Baseclass of PieChart and RadarChart.
*
* @author Philipp Jahoda
*/
public abstract class PieRadarChartBase<T extends ChartData<? extends DataSet<? extends Entry>>>
extends Chart<T> {
/** holds the current rotation angle of the chart */
protected float mRotationAngle = 270f;
/** the angle where the dragging started */
private float mStartAngle = 0f;
/** flag that indicates if rotation is enabled or not */
protected boolean mRotateEnabled = true;
/** the pie- and radarchart touchlistener */
protected OnTouchListener mListener;
public PieRadarChartBase(Context context) {
super(context);
}
public PieRadarChartBase(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PieRadarChartBase(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void init() {
super.init();
mListener = new PieRadarChartTouchListener(this);
}
@Override
protected void calcMinMax() {
mDeltaX = mData.getXVals().size() - 1;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// use the pie- and radarchart listener own listener
if (mTouchEnabled && mListener != null)
return mListener.onTouch(this, event);
else
return super.onTouchEvent(event);
}
@Override
public void notifyDataSetChanged() {
if (mDataNotSet)
return;
calcMinMax();
mLegendRenderer.computeLegend(mData);
calculateOffsets();
}
@Override
protected void calculateOffsets() {
float legendLeft = 0f, legendRight = 0f, legendBottom = 0f, legendTop = 0f;
if (mLegend != null && mLegend.isEnabled()) {
if (mLegend.getPosition() == LegendPosition.RIGHT_OF_CHART_CENTER) {
// this is the space between the legend and the chart
float spacing = Utils.convertDpToPixel(13f);
legendRight = getFullLegendWidth() + spacing;
} else if (mLegend.getPosition() == LegendPosition.RIGHT_OF_CHART) {
// this is the space between the legend and the chart
float spacing = Utils.convertDpToPixel(8f);
float legendWidth = getFullLegendWidth() + spacing;
float legendHeight = mLegend.mNeededHeight + mLegend.mTextHeightMax;
PointF c = getCenter();
PointF bottomRight = new PointF(getWidth() - legendWidth + 15, legendHeight + 15);
float distLegend = distanceToCenter(bottomRight.x, bottomRight.y);
PointF reference = getPosition(c, getRadius(),
getAngleForPoint(bottomRight.x, bottomRight.y));
float distReference = distanceToCenter(reference.x, reference.y);
float min = Utils.convertDpToPixel(5f);
if (distLegend < distReference) {
float diff = distReference - distLegend;
legendRight = min + diff;
}
if (bottomRight.y >= c.y && getHeight() - legendWidth > getWidth()) {
legendRight = legendWidth;
}
} else if (mLegend.getPosition() == LegendPosition.LEFT_OF_CHART_CENTER) {
// this is the space between the legend and the chart
float spacing = Utils.convertDpToPixel(13f);
legendLeft = getFullLegendWidth() + spacing;
} else if (mLegend.getPosition() == LegendPosition.LEFT_OF_CHART) {
// this is the space between the legend and the chart
float spacing = Utils.convertDpToPixel(8f);
float legendWidth = getFullLegendWidth() + spacing;
float legendHeight = mLegend.mNeededHeight + mLegend.mTextHeightMax;
PointF c = getCenter();
PointF bottomLeft = new PointF(legendWidth - 15, legendHeight + 15);
float distLegend = distanceToCenter(bottomLeft.x, bottomLeft.y);
PointF reference = getPosition(c, getRadius(),
getAngleForPoint(bottomLeft.x, bottomLeft.y));
float distReference = distanceToCenter(reference.x, reference.y);
float min = Utils.convertDpToPixel(5f);
if (distLegend < distReference) {
float diff = distReference - distLegend;
legendLeft = min + diff;
}
if (bottomLeft.y >= c.y && getHeight() - legendWidth > getWidth()) {
legendLeft = legendWidth;
}
} else if (mLegend.getPosition() == LegendPosition.BELOW_CHART_LEFT
|| mLegend.getPosition() == LegendPosition.BELOW_CHART_RIGHT
|| mLegend.getPosition() == LegendPosition.BELOW_CHART_CENTER) {
legendBottom = getRequiredBottomOffset();
}
legendLeft += getRequiredBaseOffset();
legendRight += getRequiredBaseOffset();
legendTop += getRequiredBaseOffset();
}
float min = Utils.convertDpToPixel(10f);
float offsetLeft = Math.max(min, legendLeft);
float offsetTop = Math.max(min, legendTop);
float offsetRight = Math.max(min, legendRight);
float offsetBottom = Math.max(min, Math.max(getRequiredBaseOffset(), legendBottom));
mViewPortHandler.restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom);
if (mLogEnabled)
Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop
+ ", offsetRight: " + offsetRight + ", offsetBottom: " + offsetBottom);
}
/**
* sets the starting angle of the rotation, this is only used by the touch
* listener, x and y is the touch position
*
* @param x
* @param y
*/
public void setStartAngle(float x, float y) {
mStartAngle = getAngleForPoint(x, y);
// take the current angle into consideration when starting a new drag
mStartAngle -= mRotationAngle;
}
/**
* updates the view rotation depending on the given touch position, also
* takes the starting angle into consideration
*
* @param x
* @param y
*/
public void updateRotation(float x, float y) {
mRotationAngle = getAngleForPoint(x, y);
// take the offset into consideration
mRotationAngle -= mStartAngle;
// keep the angle >= 0 and <= 360
mRotationAngle = (mRotationAngle + 360f) % 360f;
}
/**
* returns the angle relative to the chart center for the given point on the
* chart in degrees. The angle is always between 0 and 360°, 0° is NORTH,
* 90° is EAST, ...
*
* @param x
* @param y
* @return
*/
public float getAngleForPoint(float x, float y) {
PointF c = getCenterOffsets();
double tx = x - c.x, ty = y - c.y;
double length = Math.sqrt(tx * tx + ty * ty);
double r = Math.acos(ty / length);
float angle = (float) Math.toDegrees(r);
if (x > c.x)
angle = 360f - angle;
// add 90° because chart starts EAST
angle = angle + 90f;
// neutralize overflow
if (angle > 360f)
angle = angle - 360f;
return angle;
}
/**
* Calculates the position around a center point, depending on the distance
* from the center, and the angle of the position around the center.
*
* @param center
* @param dist
* @param angle in degrees, converted to radians internally
* @return
*/
protected PointF getPosition(PointF center, float dist, float angle) {
PointF p = new PointF((float) (center.x + dist * Math.cos(Math.toRadians(angle))),
(float) (center.y + dist * Math.sin(Math.toRadians(angle))));
return p;
}
/**
* Returns the distance of a certain point on the chart to the center of the
* chart.
*
* @param c the center
* @param x
* @param y
* @return
*/
public float distanceToCenter(float x, float y) {
PointF c = getCenterOffsets();
float dist = 0f;
float xDist = 0f;
float yDist = 0f;
if (x > c.x) {
xDist = x - c.x;
} else {
xDist = c.x - x;
}
if (y > c.y) {
yDist = y - c.y;
} else {
yDist = c.y - y;
}
// pythagoras
dist = (float) Math.sqrt(Math.pow(xDist, 2.0) + Math.pow(yDist, 2.0));
return dist;
}
/**
* Returns the xIndex for the given angle around the center of the chart.
* Returns -1 if not found / outofbounds.
*
* @param angle
* @return
*/
public abstract int getIndexForAngle(float angle);
/**
* Set an offset for the rotation of the RadarChart in degrees. Default 270f
* --> top (NORTH)
*
* @param angle
*/
public void setRotationAngle(float angle) {
angle = (int) Math.abs(angle % 360);
mRotationAngle = angle;
}
/**
* gets the current rotation angle of the pie chart
*
* @return
*/
public float getRotationAngle() {
return mRotationAngle;
}
/**
* Set this to true to enable the rotation / spinning of the chart by touch.
* Set it to false to disable it. Default: true
*
* @param enabled
*/
public void setRotationEnabled(boolean enabled) {
mRotateEnabled = enabled;
}
/**
* Returns true if rotation of the chart by touch is enabled, false if not.
*
* @return
*/
public boolean isRotationEnabled() {
return mRotateEnabled;
}
/**
* returns the diameter of the pie- or radar-chart
*
* @return
*/
public float getDiameter() {
RectF content = mViewPortHandler.getContentRect();
return Math.min(content.width(), content.height());
}
/**
* Returns the radius of the chart in pixels.
*
* @return
*/
public abstract float getRadius();
/**
* Returns the required bottom offset for the chart.
*
* @return
*/
protected abstract float getRequiredBottomOffset();
/**
* Returns the base offset needed for the chart without calculating the
* legend size.
*
* @return
*/
protected abstract float getRequiredBaseOffset();
/**
* Returns the required right offset for the chart.
*
* @return
*/
private float getFullLegendWidth() {
return mLegend.mTextWidthMax + mLegend.getFormSize() + mLegend.getFormToTextSpace();
}
/**
* 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(OnTouchListener l) {
this.mListener = l;
}
@Override
public float getYChartMax() {
// TODO Auto-generated method stub
return 0;
}
@Override
public float getYChartMin() {
// TODO Auto-generated method stub
return 0;
}
/**
* Returns an array of SelInfo objects for the given x-index. The SelInfo
* objects give information about the value at the selected index and the
* DataSet it belongs to. INFORMATION: This method does calculations at
* runtime. Do not over-use in performance critical situations.
*
* @return
*/
public List<SelInfo> getYValsAtIndex(int xIndex) {
List<SelInfo> vals = new ArrayList<SelInfo>();
for (int i = 0; i < mData.getDataSetCount(); i++) {
DataSet<?> dataSet = mData.getDataSetByIndex(i);
// extract all y-values from all DataSets at the given x-index
float yVal = dataSet.getYValForXIndex(xIndex);
if (!Float.isNaN(yVal)) {
vals.add(new SelInfo(yVal, i, dataSet));
}
}
return vals;
}
/**
* ################ ################ ################ ################
*/
/** CODE BELOW THIS RELATED TO ANIMATION */
/**
* Applys a spin animation to the Chart.
*
* @param durationmillis
* @param fromangle
* @param toangle
*/
@SuppressLint("NewApi")
public void spin(int durationmillis, float fromangle, float toangle, Easing.EasingOption easing) {
if (android.os.Build.VERSION.SDK_INT < 11)
return;
mRotationAngle = fromangle;
ObjectAnimator spinAnimator = ObjectAnimator.ofFloat(this, "rotationAngle", fromangle, toangle);
spinAnimator.setDuration(durationmillis);
spinAnimator.setInterpolator(Easing.getEasingFunctionFromOption(easing));
spinAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
postInvalidate();
}
});
spinAnimator.start();
}
}