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.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;
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.interfaces.datasets.IDataSet;
import com.github.mikephil.charting.listener.PieRadarChartTouchListener;
import com.github.mikephil.charting.utils.MPPointF;
import com.github.mikephil.charting.utils.Utils;
/**
* Baseclass of PieChart and RadarChart.
*
* @author Philipp Jahoda
*/
public abstract class PieRadarChartBase<T extends ChartData<? extends IDataSet<? extends Entry>>>
extends Chart<T> {
/**
* holds the normalized version of the current rotation angle of the chart
*/
private float mRotationAngle = 270f;
/**
* holds the raw version of the current rotation angle of the chart
*/
private float mRawRotationAngle = 270f;
/**
* flag that indicates if rotation is enabled or not
*/
protected boolean mRotateEnabled = true;
/**
* Sets the minimum offset (padding) around the chart, defaults to 0.f
*/
protected float mMinOffset = 0.f;
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();
mChartTouchListener = new PieRadarChartTouchListener(this);
}
@Override
protected void calcMinMax() {
//mXAxis.mAxisRange = mData.getXVals().size() - 1;
}
@Override
public int getMaxVisibleCount() {
return mData.getEntryCount();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// use the pie- and radarchart listener own listener
if (mTouchEnabled && mChartTouchListener != null)
return mChartTouchListener.onTouch(this, event);
else
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (mChartTouchListener instanceof PieRadarChartTouchListener)
((PieRadarChartTouchListener) mChartTouchListener).computeScroll();
}
@Override
public void notifyDataSetChanged() {
if (mData == null)
return;
calcMinMax();
if (mLegend != null)
mLegendRenderer.computeLegend(mData);
calculateOffsets();
}
@Override
public void calculateOffsets() {
float legendLeft = 0f, legendRight = 0f, legendBottom = 0f, legendTop = 0f;
if (mLegend != null && mLegend.isEnabled() && !mLegend.isDrawInsideEnabled()) {
float fullLegendWidth = Math.min(mLegend.mNeededWidth,
mViewPortHandler.getChartWidth() * mLegend.getMaxSizePercent());
switch (mLegend.getOrientation()) {
case VERTICAL: {
float xLegendOffset = 0.f;
if (mLegend.getHorizontalAlignment() == Legend.LegendHorizontalAlignment.LEFT
|| mLegend.getHorizontalAlignment() == Legend.LegendHorizontalAlignment.RIGHT) {
if (mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.CENTER) {
// this is the space between the legend and the chart
final float spacing = Utils.convertDpToPixel(13f);
xLegendOffset = fullLegendWidth + spacing;
} else {
// this is the space between the legend and the chart
float spacing = Utils.convertDpToPixel(8f);
float legendWidth = fullLegendWidth + spacing;
float legendHeight = mLegend.mNeededHeight + mLegend.mTextHeightMax;
MPPointF center = getCenter();
float bottomX = mLegend.getHorizontalAlignment() ==
Legend.LegendHorizontalAlignment.RIGHT
? getWidth() - legendWidth + 15.f
: legendWidth - 15.f;
float bottomY = legendHeight + 15.f;
float distLegend = distanceToCenter(bottomX, bottomY);
MPPointF reference = getPosition(center, getRadius(),
getAngleForPoint(bottomX, bottomY));
float distReference = distanceToCenter(reference.x, reference.y);
float minOffset = Utils.convertDpToPixel(5f);
if (bottomY >= center.y && getHeight() - legendWidth > getWidth()) {
xLegendOffset = legendWidth;
} else if (distLegend < distReference) {
float diff = distReference - distLegend;
xLegendOffset = minOffset + diff;
}
MPPointF.recycleInstance(center);
MPPointF.recycleInstance(reference);
}
}
switch (mLegend.getHorizontalAlignment()) {
case LEFT:
legendLeft = xLegendOffset;
break;
case RIGHT:
legendRight = xLegendOffset;
break;
case CENTER:
switch (mLegend.getVerticalAlignment()) {
case TOP:
legendTop = Math.min(mLegend.mNeededHeight,
mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent());
break;
case BOTTOM:
legendBottom = Math.min(mLegend.mNeededHeight,
mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent());
break;
}
break;
}
}
break;
case HORIZONTAL:
float yLegendOffset = 0.f;
if (mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.TOP ||
mLegend.getVerticalAlignment() == Legend.LegendVerticalAlignment.BOTTOM) {
// It's possible that we do not need this offset anymore as it
// is available through the extraOffsets, but changing it can mean
// changing default visibility for existing apps.
float yOffset = getRequiredLegendOffset();
yLegendOffset = Math.min(mLegend.mNeededHeight + yOffset,
mViewPortHandler.getChartHeight() * mLegend.getMaxSizePercent());
switch (mLegend.getVerticalAlignment()) {
case TOP:
legendTop = yLegendOffset;
break;
case BOTTOM:
legendBottom = yLegendOffset;
break;
}
}
break;
}
legendLeft += getRequiredBaseOffset();
legendRight += getRequiredBaseOffset();
legendTop += getRequiredBaseOffset();
legendBottom += getRequiredBaseOffset();
}
float minOffset = Utils.convertDpToPixel(mMinOffset);
if (this instanceof RadarChart) {
XAxis x = this.getXAxis();
if (x.isEnabled() && x.isDrawLabelsEnabled()) {
minOffset = Math.max(minOffset, x.mLabelRotatedWidth);
}
}
legendTop += getExtraTopOffset();
legendRight += getExtraRightOffset();
legendBottom += getExtraBottomOffset();
legendLeft += getExtraLeftOffset();
float offsetLeft = Math.max(minOffset, legendLeft);
float offsetTop = Math.max(minOffset, legendTop);
float offsetRight = Math.max(minOffset, legendRight);
float offsetBottom = Math.max(minOffset, Math.max(getRequiredBaseOffset(), legendBottom));
mViewPortHandler.restrainViewPort(offsetLeft, offsetTop, offsetRight, offsetBottom);
if (mLogEnabled)
Log.i(LOG_TAG, "offsetLeft: " + offsetLeft + ", offsetTop: " + offsetTop
+ ", offsetRight: " + offsetRight + ", offsetBottom: " + offsetBottom);
}
/**
* 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) {
MPPointF 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;
MPPointF.recycleInstance(c);
return angle;
}
/**
* Returns a recyclable MPPointF instance.
* 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
*/
public MPPointF getPosition(MPPointF center, float dist, float angle) {
MPPointF p = MPPointF.getInstance(0, 0);
getPosition(center, dist, angle, p);
return p;
}
public void getPosition(MPPointF center, float dist, float angle, MPPointF outputPoint) {
outputPoint.x = (float) (center.x + dist * Math.cos(Math.toRadians(angle)));
outputPoint.y = (float) (center.y + dist * Math.sin(Math.toRadians(angle)));
}
/**
* Returns the distance of a certain point on the chart to the center of the
* chart.
*
* @param x
* @param y
* @return
*/
public float distanceToCenter(float x, float y) {
MPPointF 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));
MPPointF.recycleInstance(c);
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) {
mRawRotationAngle = angle;
mRotationAngle = Utils.getNormalizedAngle(mRawRotationAngle);
}
/**
* gets the raw version of the current rotation angle of the pie chart the
* returned value could be any value, negative or positive, outside of the
* 360 degrees. this is used when working with rotation direction, mainly by
* gestures and animations.
*
* @return
*/
public float getRawRotationAngle() {
return mRawRotationAngle;
}
/**
* gets a normalized version of the current rotation angle of the pie chart,
* which will always be between 0.0 < 360.0
*
* @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;
}
/**
* Gets the minimum offset (padding) around the chart, defaults to 0.f
*/
public float getMinOffset() {
return mMinOffset;
}
/**
* Sets the minimum offset (padding) around the chart, defaults to 0.f
*/
public void setMinOffset(float minOffset) {
mMinOffset = minOffset;
}
/**
* returns the diameter of the pie- or radar-chart
*
* @return
*/
public float getDiameter() {
RectF content = mViewPortHandler.getContentRect();
content.left += getExtraLeftOffset();
content.top += getExtraTopOffset();
content.right -= getExtraRightOffset();
content.bottom -= getExtraBottomOffset();
return Math.min(content.width(), content.height());
}
/**
* Returns the radius of the chart in pixels.
*
* @return
*/
public abstract float getRadius();
/**
* Returns the required offset for the chart legend.
*
* @return
*/
protected abstract float getRequiredLegendOffset();
/**
* Returns the base offset needed for the chart without calculating the
* legend size.
*
* @return
*/
protected abstract float getRequiredBaseOffset();
@Override
public float getYChartMax() {
// TODO Auto-generated method stub
return 0;
}
@Override
public float getYChartMin() {
// TODO Auto-generated method stub
return 0;
}
/**
* ################ ################ ################ ################
*/
/** 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;
setRotationAngle(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();
}
}