| |
| package com.github.mikephil.charting; |
| |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Paint.Align; |
| import android.graphics.Paint.Style; |
| import android.graphics.PointF; |
| import android.graphics.RectF; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| |
| import java.text.DecimalFormat; |
| |
| /** |
| * View that represents a pie chart. |
| * |
| * @author Philipp Jahoda |
| */ |
| public class PieChart extends Chart { |
| |
| /** |
| * rect object that represents the bounds of the piechart, needed for |
| * drawing the circle |
| */ |
| private RectF mCircleBox; |
| |
| /** holds the current rotation angle of the chart */ |
| private float mChartAngle = 0f; |
| |
| /** array that holds the width of each pie-slice in degrees */ |
| private float[] mDrawAngles; |
| |
| /** array that holds the absolute angle in degrees of each slice */ |
| private float[] mAbsoluteAngles; |
| |
| /** if true, the white hole inside the chart will be drawn */ |
| private boolean mDrawHole = true; |
| |
| /** |
| * variable for the text that is drawn in the center of the pie-chart. If |
| * this value is null, the default is "Total Value\n + getYValueSum()" |
| */ |
| private String mCenterText = null; |
| |
| /** indicates the selection distance of a pie slice */ |
| private float mShift = 20f; |
| |
| /** if enabled, centertext is drawn */ |
| private boolean mDrawCenterText = true; |
| |
| /** |
| * set this to true to draw the x-values next to the values in the pie |
| * slices |
| */ |
| private boolean mDrawXVals = true; |
| |
| /** |
| * if set to true, all values show up in percent instead of their real value |
| */ |
| private boolean mUsePercentValues = false; |
| |
| /** |
| * array of integers that reference the highlighted slices in the pie chart |
| */ |
| private int[] mIndicesToHightlight = new int[0]; |
| |
| /** |
| * paint for the hole in the center of the pie chart |
| */ |
| private Paint mHolePaint; |
| |
| /** |
| * paint object for the text that can be displayed in the center of the |
| * chart |
| */ |
| private Paint mCenterTextPaint; |
| |
| public PieChart(Context context) { |
| super(context); |
| } |
| |
| public PieChart(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| public PieChart(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| } |
| |
| @Override |
| protected void init() { |
| super.init(); |
| |
| // piechart has no offsets |
| mOffsetTop = 0; |
| mOffsetBottom = 0; |
| mOffsetLeft = 0; |
| mOffsetRight = 0; |
| |
| mShift = Utils.convertDpToPixel(mShift); |
| |
| mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mHolePaint.setColor(Color.WHITE); |
| |
| mCenterTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mCenterTextPaint.setColor(mColorDarkBlue); |
| mCenterTextPaint.setTextSize(Utils.convertDpToPixel(12f)); |
| mCenterTextPaint.setTextAlign(Align.CENTER); |
| |
| mValuePaint.setTextSize(Utils.convertDpToPixel(13f)); |
| mValuePaint.setColor(Color.WHITE); |
| mValuePaint.setTextAlign(Align.CENTER); |
| |
| mListener = new PieChartTouchListener(this); |
| |
| // for the piechart, drawing values is enabled |
| mDrawValues = true; |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| |
| if (mDataNotSet) |
| return; |
| |
| long starttime = System.currentTimeMillis(); |
| |
| drawData(); |
| |
| drawAdditional(); |
| |
| drawValues(); |
| |
| drawDescription(); |
| |
| drawCenterText(); |
| |
| drawMarkerView(); |
| |
| canvas.drawBitmap(mDrawBitmap, 0, 0, mDrawPaint); |
| |
| Log.i(LOG_TAG, "DrawTime: " + (System.currentTimeMillis() - starttime) + " ms"); |
| } |
| |
| /** |
| * does all necessary preparations, needed when data is changed or flags |
| * that effect the data are changed |
| */ |
| @Override |
| protected void prepare() { |
| |
| if (mDataNotSet) |
| return; |
| |
| calcMinMax(); |
| |
| if (mCenterText == null) |
| mCenterText = "Total Value\n" + (int) getYValueSum(); |
| |
| // calculate how many digits are needed |
| calcFormats(); |
| |
| prepareMatrix(); |
| |
| Log.i(LOG_TAG, "xVals: " + mXVals.size() + ", yVals: " + mYVals.size()); |
| } |
| |
| /** the decimalformat responsible for formatting the values in the chart */ |
| protected DecimalFormat mFormatValue = null; |
| |
| /** |
| * the number of digits values that are drawn in the chart are formatted |
| * with |
| */ |
| protected int mValueFormatDigits = 1; |
| |
| /** |
| * calculates the required number of digits for the y-legend and for the |
| * values that might be drawn in the chart (if enabled) |
| */ |
| protected void calcFormats() { |
| |
| mValueFormatDigits = Utils.getPieFormatDigits(mDeltaY); |
| |
| StringBuffer b = new StringBuffer(); |
| for (int i = 0; i < mValueFormatDigits; i++) { |
| if (i == 0) |
| b.append("."); |
| b.append("0"); |
| } |
| |
| mFormatValue = new DecimalFormat("###,###,###,##0" + b.toString()); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| return mListener.onTouch(this, event); |
| } |
| |
| /** the angle where the dragging started */ |
| private float mStartAngle = 0f; |
| |
| /** |
| * 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 -= mChartAngle; |
| } |
| |
| /** |
| * 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) { |
| |
| mChartAngle = getAngleForPoint(x, y); |
| |
| // take the offset into consideration |
| mChartAngle -= mStartAngle; |
| |
| // keep the angle >= 0 and <= 360 |
| mChartAngle = (mChartAngle + 360f) % 360f; |
| } |
| |
| @Override |
| protected void prepareContentRect() { |
| super.prepareContentRect(); |
| |
| int width = mContentRect.width() + mOffsetLeft + mOffsetRight; |
| int height = mContentRect.height() + mOffsetTop + mOffsetBottom; |
| |
| float diameter = getDiameter(); |
| |
| // create the circle box that will contain the pie-chart (the bounds of |
| // the pie-chart) |
| mCircleBox = new RectF(width / 2 - diameter / 2 + mShift, height / 2 - diameter / 2 |
| + mShift + mOffsetTop, width / 2 + diameter / 2 - mShift, height / 2 + diameter / 2 |
| - mOffsetBottom - mShift); |
| } |
| |
| @Override |
| protected void prepareDataPaints(ColorTemplate ct) { |
| |
| mDrawPaints = new Paint[ct.getColors().size()]; |
| |
| // setup all paint objects |
| for (int i = 0; i < ct.getColors().size(); i++) { |
| mDrawPaints[i] = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mDrawPaints[i].setStyle(Style.FILL); |
| mDrawPaints[i].setColor(ct.getColors().get(i)); |
| } |
| } |
| |
| @Override |
| protected void calcMinMax() { |
| super.calcMinMax(); |
| |
| calcAngles(); |
| } |
| |
| /** |
| * calculates the needed angles for the chart slices |
| */ |
| private void calcAngles() { |
| |
| mDrawAngles = new float[mYVals.size()]; |
| mAbsoluteAngles = new float[mYVals.size()]; |
| |
| for (int i = 0; i < mYVals.size(); i++) { |
| mDrawAngles[i] = calcAngle(mYVals.get(i)); |
| |
| if (i > 0) |
| mAbsoluteAngles[i] = mAbsoluteAngles[i - 1] + mDrawAngles[i]; |
| else |
| mAbsoluteAngles[i] = mDrawAngles[i]; |
| } |
| } |
| |
| @Override |
| protected void drawData() { |
| |
| float angle = mChartAngle; |
| |
| for (int i = 0; i < mYVals.size(); i++) { |
| |
| float newanlge = mDrawAngles[i]; |
| |
| if (needsHighlight(i)) { // if true, highlight the slice |
| float shiftangle = (float) Math.toRadians(angle + newanlge / 2f); |
| |
| float xShift = mShift * (float) Math.cos(shiftangle); |
| float yShift = mShift * (float) Math.sin(shiftangle); |
| |
| RectF highlighted = new RectF(mCircleBox.left + xShift, mCircleBox.top + yShift, |
| mCircleBox.right + xShift, mCircleBox.bottom + yShift); |
| |
| // redefine the rect that contains the arc so that the |
| // highlighted pie is not cut off |
| mDrawCanvas.drawArc(highlighted, angle, newanlge, true, mDrawPaints[i |
| % mDrawPaints.length]); |
| |
| } else { |
| |
| mDrawCanvas.drawArc(mCircleBox, angle, newanlge, true, mDrawPaints[i |
| % mDrawPaints.length]); |
| } |
| angle += newanlge; |
| } |
| } |
| |
| /** |
| * draws the hole in the center of the chart |
| */ |
| private void drawHole() { |
| |
| if (mDrawHole) { |
| |
| mDrawCanvas.drawCircle(mContentRect.width() / 2, mContentRect.height() / 2, |
| getDiameter() / 4, mHolePaint); |
| } |
| } |
| |
| /** |
| * draws the description text in the center of the pie chart makes most |
| * sense when center-hole is enabled |
| */ |
| private void drawCenterText() { |
| |
| if (mDrawCenterText) { |
| |
| PointF c = getCenter(); |
| |
| // get all lines from the text |
| String[] lines = mCenterText.split("\n"); |
| |
| // calculate the height for each line |
| float lineHeight = (mValuePaint.ascent() + mValuePaint.descent()) * 1.6f; |
| |
| // calculate the height of the whole text |
| float textheight = lines.length * lineHeight; |
| |
| for (int i = 0; i < lines.length; i++) { |
| mDrawCanvas.drawText(lines[lines.length - i - 1], c.x, c.y - textheight/2 + lineHeight * i, |
| mCenterTextPaint); |
| } |
| } |
| } |
| |
| @Override |
| protected void drawValues() { |
| |
| // if neither xvals nor yvals are drawn, return |
| if (!mDrawXVals && !mDrawValues) |
| return; |
| |
| PointF center = getCenter(); |
| |
| float off = mCircleBox.width() / 8; |
| |
| // increase offset if there is no hole |
| if (!mDrawHole) |
| off += off / 2; |
| |
| // get the radius |
| float r = mCircleBox.width() / 2 - off; // offset to keep things inside |
| // the chart |
| |
| for (int i = 0; i < mYVals.size(); i++) { |
| |
| // offset needed to center the drawn text in the slice |
| float offset = mDrawAngles[i] / 2; |
| |
| // calculate the text position |
| float x = (float) (r |
| * Math.cos(Math.toRadians(mChartAngle + mAbsoluteAngles[i] - offset)) + center.x); |
| float y = (float) (r |
| * Math.sin(Math.toRadians(mChartAngle + mAbsoluteAngles[i] - offset)) + center.y); |
| |
| // if (y > center.y) { |
| // y += 10; |
| // x += 3; |
| // } |
| |
| String val = ""; |
| |
| if(mUsePercentValues) val = mFormatValue.format(getPercentOfTotal(mYVals.get(i))) + " %"; |
| else val = mFormatValue.format(mYVals.get(i)); |
| |
| // draw everything, depending on settings |
| if (mDrawXVals && mDrawValues) { |
| |
| // use ascent and descent to calculate the new line position, |
| // 1.6f is the line spacing |
| float lineHeight = (mValuePaint.ascent() + mValuePaint.descent()) * 1.6f; |
| y -= lineHeight / 2; |
| |
| mDrawCanvas.drawText(val, x, |
| y, mValuePaint); |
| mDrawCanvas.drawText(mXVals.get(i), x, |
| y + lineHeight, mValuePaint); |
| |
| } else if (mDrawXVals && !mDrawValues) { |
| mDrawCanvas.drawText(mXVals.get(i), x, y, mValuePaint); |
| } else if (!mDrawXVals && mDrawValues) { |
| |
| mDrawCanvas.drawText(val, x, y, mValuePaint); |
| } |
| } |
| } |
| |
| @Override |
| protected void drawAdditional() { |
| drawHole(); |
| } |
| |
| /** |
| * checks if the given index is set for highlighting or not |
| * |
| * @param index |
| * @return |
| */ |
| private boolean needsHighlight(int index) { |
| |
| for (int i = 0; i < mIndicesToHightlight.length; i++) |
| if (mIndicesToHightlight[i] == index) |
| return true; |
| |
| return false; |
| } |
| |
| /** |
| * calculates the needed angle for a given value |
| * |
| * @param value |
| * @return |
| */ |
| private float calcAngle(float value) { |
| return value / mYValueSum * 360f; |
| } |
| |
| @Override |
| public void highlightValues(int[] indices) { |
| super.highlightValues(indices); |
| |
| mIndicesToHightlight = indices; |
| invalidate(); |
| } |
| |
| /** |
| * returns the pie index for the pie at the given angle |
| * |
| * @param angle |
| * @return |
| */ |
| public int getIndexForAngle(float angle) { |
| |
| // take the current angle of the chart into consideration |
| float a = (angle - mChartAngle + 360) % 360f; |
| |
| for (int i = 0; i < mAbsoluteAngles.length; i++) { |
| if (mAbsoluteAngles[i] > a) |
| return i; |
| } |
| |
| return -1; // return -1 if no index found |
| } |
| |
| /** |
| * returns an integer array of all the different angles the chart slices |
| * have the angles in the returned array determine how much space (of 360°) |
| * each slice takes |
| * |
| * @return |
| */ |
| public float[] getDrawAngles() { |
| return mDrawAngles; |
| } |
| |
| /** |
| * returns the absolute angles of the different chart slices (where the |
| * slices end) |
| * |
| * @return |
| */ |
| public float[] getAbsoluteAngles() { |
| return mAbsoluteAngles; |
| } |
| |
| /** |
| * set a new starting angle for the pie chart (0-360) default is 0° --> |
| * right side (EAST) |
| * |
| * @param angle |
| */ |
| public void setStartAngle(float angle) { |
| mChartAngle = angle; |
| } |
| |
| /** |
| * gets the current rotation angle of the pie chart |
| * |
| * @return |
| */ |
| public float getCurrentRotation() { |
| return mChartAngle; |
| } |
| |
| /** |
| * sets the distance of the highlighted value to the piechart default 20f |
| * |
| * @param shift |
| */ |
| public void setShift(float shift) { |
| mShift = Utils.convertDpToPixel(shift); |
| } |
| |
| /** |
| * set this to true to draw the pie center empty |
| * |
| * @param enabled |
| */ |
| public void setDrawHoleEnabled(boolean enabled) { |
| this.mDrawHole = enabled; |
| } |
| |
| /** |
| * returns true if the hole in the center of the pie-chart is set to be |
| * visible, false if not |
| * |
| * @return |
| */ |
| public boolean isDrawHoleEnabled() { |
| return mDrawHole; |
| } |
| |
| /** |
| * sets the text that is displayed in the center of the pie-chart. By |
| * default, the text is "Total Value + sumofallvalues" |
| * |
| * @param text |
| */ |
| public void setCenterText(String text) { |
| mCenterText = text; |
| } |
| |
| /** |
| * returns the text that is drawn in the center of the pie-chart |
| * @return |
| */ |
| public String getCenterText() { |
| return mCenterText; |
| } |
| |
| /** |
| * set this to true to draw the text that is displayed in the center of the |
| * pie chart |
| * |
| * @param enabled |
| */ |
| public void setDrawCenterText(boolean enabled) { |
| this.mDrawCenterText = enabled; |
| } |
| |
| /** |
| * returns true if drawing the center text is enabled |
| * |
| * @return |
| */ |
| public boolean isDrawCenterTextEnabled() { |
| return mDrawCenterText; |
| } |
| |
| /** |
| * set this to true to draw percent values instead of the actual values |
| * @param enabled |
| */ |
| public void setUsePercentValues(boolean enabled) { |
| mUsePercentValues = enabled; |
| } |
| |
| /** |
| * returns true if drawing percent values is enabled |
| * @return |
| */ |
| public boolean isUsePercentValuesEnabled() { |
| return mUsePercentValues; |
| } |
| |
| /** |
| * set this to true to draw the x-value text into the pie slices |
| * |
| * @param enabled |
| */ |
| public void setDrawXVals(boolean enabled) { |
| mDrawXVals = enabled; |
| } |
| |
| /** |
| * returns true if drawing x-values is enabled, false if not |
| * |
| * @return |
| */ |
| public boolean isDrawXValsEnabled() { |
| return mDrawXVals; |
| } |
| |
| /** |
| * returns the radius of the pie-chart |
| * |
| * @return |
| */ |
| public float getRadius() { |
| if (mCircleBox == null) |
| return 0; |
| else |
| return mCircleBox.width() / 2f; |
| } |
| |
| /** |
| * returns the diameter of the pie-chart |
| * |
| * @return |
| */ |
| public float getDiameter() { |
| if (mContentRect == null) |
| return 0; |
| else |
| return Math.min(mContentRect.width(), mContentRect.height()); |
| } |
| |
| /** |
| * 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 EAST |
| * |
| * @param x |
| * @param y |
| * @return |
| */ |
| public float getAngleForPoint(float x, float y) { |
| |
| PointF c = getCenter(); |
| |
| 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 > getCenter().x) |
| angle = 360f - angle; |
| |
| // add 90° because chart starts EAST |
| angle = angle + 90f; |
| |
| // neutralize overflow |
| if (angle > 360f) |
| angle = angle - 360f; |
| |
| return angle; |
| } |
| |
| /** |
| * returns the distance of a certain point on the chart to the center of the |
| * piechart |
| * |
| * @param x |
| * @param y |
| * @return |
| */ |
| public float distanceToCenter(float x, float y) { |
| |
| PointF c = getCenter(); |
| |
| 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; |
| } |
| } |