1 2 package com.github.mikephil.charting.charts; 3 4 import android.content.Context; 5 import android.graphics.Canvas; 6 import android.graphics.Paint; 7 import android.graphics.RectF; 8 import android.graphics.Typeface; 9 import android.util.AttributeSet; 10 11 import com.github.mikephil.charting.components.XAxis; 12 import com.github.mikephil.charting.data.PieData; 13 import com.github.mikephil.charting.highlight.Highlight; 14 import com.github.mikephil.charting.highlight.PieHighlighter; 15 import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; 16 import com.github.mikephil.charting.renderer.PieChartRenderer; 17 import com.github.mikephil.charting.utils.MPPointF; 18 import com.github.mikephil.charting.utils.Utils; 19 20 import java.util.List; 21 22 /** 23 * View that represents a pie chart. Draws cake like slices. 24 * 25 * @author Philipp Jahoda 26 */ 27 public class PieChart extends PieRadarChartBase<PieData> { 28 29 /** 30 * rect object that represents the bounds of the piechart, needed for 31 * drawing the circle 32 */ 33 private RectF mCircleBox = new RectF(); 34 35 /** 36 * flag indicating if entry labels should be drawn or not 37 */ 38 private boolean mDrawEntryLabels = true; 39 40 /** 41 * array that holds the width of each pie-slice in degrees 42 */ 43 private float[] mDrawAngles = new float[1]; 44 45 /** 46 * array that holds the absolute angle in degrees of each slice 47 */ 48 private float[] mAbsoluteAngles = new float[1]; 49 50 /** 51 * if true, the white hole inside the chart will be drawn 52 */ 53 private boolean mDrawHole = true; 54 55 /** 56 * if true, the hole will see-through to the inner tips of the slices 57 */ 58 private boolean mDrawSlicesUnderHole = false; 59 60 /** 61 * if true, the values inside the piechart are drawn as percent values 62 */ 63 private boolean mUsePercentValues = false; 64 65 /** 66 * if true, the slices of the piechart are rounded 67 */ 68 private boolean mDrawRoundedSlices = false; 69 70 /** 71 * variable for the text that is drawn in the center of the pie-chart 72 */ 73 private CharSequence mCenterText = ""; 74 75 private MPPointF mCenterTextOffset = MPPointF.getInstance(0, 0); 76 77 /** 78 * indicates the size of the hole in the center of the piechart, default: 79 * radius / 2 80 */ 81 private float mHoleRadiusPercent = 50f; 82 83 /** 84 * the radius of the transparent circle next to the chart-hole in the center 85 */ 86 protected float mTransparentCircleRadiusPercent = 55f; 87 88 /** 89 * if enabled, centertext is drawn 90 */ 91 private boolean mDrawCenterText = true; 92 93 private float mCenterTextRadiusPercent = 100.f; 94 95 protected float mMaxAngle = 360f; 96 97 /** 98 * Minimum angle to draw slices, this only works if there is enough room for all slices to have 99 * the minimum angle, default 0f. 100 */ 101 private float mMinAngleForSlices = 0f; 102 PieChart(Context context)103 public PieChart(Context context) { 104 super(context); 105 } 106 PieChart(Context context, AttributeSet attrs)107 public PieChart(Context context, AttributeSet attrs) { 108 super(context, attrs); 109 } 110 PieChart(Context context, AttributeSet attrs, int defStyle)111 public PieChart(Context context, AttributeSet attrs, int defStyle) { 112 super(context, attrs, defStyle); 113 } 114 115 @Override init()116 protected void init() { 117 super.init(); 118 119 mRenderer = new PieChartRenderer(this, mAnimator, mViewPortHandler); 120 mXAxis = null; 121 122 mHighlighter = new PieHighlighter(this); 123 } 124 125 @Override onDraw(Canvas canvas)126 protected void onDraw(Canvas canvas) { 127 super.onDraw(canvas); 128 129 if (mData == null) 130 return; 131 132 mRenderer.drawData(canvas); 133 134 if (valuesToHighlight()) 135 mRenderer.drawHighlighted(canvas, mIndicesToHighlight); 136 137 mRenderer.drawExtras(canvas); 138 139 mRenderer.drawValues(canvas); 140 141 mLegendRenderer.renderLegend(canvas); 142 143 drawDescription(canvas); 144 145 drawMarkers(canvas); 146 } 147 148 @Override calculateOffsets()149 public void calculateOffsets() { 150 super.calculateOffsets(); 151 152 // prevent nullpointer when no data set 153 if (mData == null) 154 return; 155 156 float diameter = getDiameter(); 157 float radius = diameter / 2f; 158 159 MPPointF c = getCenterOffsets(); 160 161 float shift = mData.getDataSet().getSelectionShift(); 162 163 // create the circle box that will contain the pie-chart (the bounds of 164 // the pie-chart) 165 mCircleBox.set(c.x - radius + shift, 166 c.y - radius + shift, 167 c.x + radius - shift, 168 c.y + radius - shift); 169 170 MPPointF.recycleInstance(c); 171 } 172 173 @Override calcMinMax()174 protected void calcMinMax() { 175 calcAngles(); 176 } 177 178 @Override getMarkerPosition(Highlight highlight)179 protected float[] getMarkerPosition(Highlight highlight) { 180 181 MPPointF center = getCenterCircleBox(); 182 float r = getRadius(); 183 184 float off = r / 10f * 3.6f; 185 186 if (isDrawHoleEnabled()) { 187 off = (r - (r / 100f * getHoleRadius())) / 2f; 188 } 189 190 r -= off; // offset to keep things inside the chart 191 192 float rotationAngle = getRotationAngle(); 193 194 int entryIndex = (int) highlight.getX(); 195 196 // offset needed to center the drawn text in the slice 197 float offset = mDrawAngles[entryIndex] / 2; 198 199 // calculate the text position 200 float x = (float) (r 201 * Math.cos(Math.toRadians((rotationAngle + mAbsoluteAngles[entryIndex] - offset) 202 * mAnimator.getPhaseY())) + center.x); 203 float y = (float) (r 204 * Math.sin(Math.toRadians((rotationAngle + mAbsoluteAngles[entryIndex] - offset) 205 * mAnimator.getPhaseY())) + center.y); 206 207 MPPointF.recycleInstance(center); 208 return new float[]{x, y}; 209 } 210 211 /** 212 * calculates the needed angles for the chart slices 213 */ calcAngles()214 private void calcAngles() { 215 216 int entryCount = mData.getEntryCount(); 217 218 if (mDrawAngles.length != entryCount) { 219 mDrawAngles = new float[entryCount]; 220 } else { 221 for (int i = 0; i < entryCount; i++) { 222 mDrawAngles[i] = 0; 223 } 224 } 225 if (mAbsoluteAngles.length != entryCount) { 226 mAbsoluteAngles = new float[entryCount]; 227 } else { 228 for (int i = 0; i < entryCount; i++) { 229 mAbsoluteAngles[i] = 0; 230 } 231 } 232 233 float yValueSum = mData.getYValueSum(); 234 235 List<IPieDataSet> dataSets = mData.getDataSets(); 236 237 boolean hasMinAngle = mMinAngleForSlices != 0f && entryCount * mMinAngleForSlices <= mMaxAngle; 238 float[] minAngles = new float[entryCount]; 239 240 int cnt = 0; 241 float offset = 0f; 242 float diff = 0f; 243 244 for (int i = 0; i < mData.getDataSetCount(); i++) { 245 246 IPieDataSet set = dataSets.get(i); 247 248 for (int j = 0; j < set.getEntryCount(); j++) { 249 250 float drawAngle = calcAngle(Math.abs(set.getEntryForIndex(j).getY()), yValueSum); 251 252 if (hasMinAngle) { 253 float temp = drawAngle - mMinAngleForSlices; 254 if (temp <= 0) { 255 minAngles[cnt] = mMinAngleForSlices; 256 offset += -temp; 257 } else { 258 minAngles[cnt] = drawAngle; 259 diff += temp; 260 } 261 } 262 263 mDrawAngles[cnt] = drawAngle; 264 265 if (cnt == 0) { 266 mAbsoluteAngles[cnt] = mDrawAngles[cnt]; 267 } else { 268 mAbsoluteAngles[cnt] = mAbsoluteAngles[cnt - 1] + mDrawAngles[cnt]; 269 } 270 271 cnt++; 272 } 273 } 274 275 if (hasMinAngle) { 276 // Correct bigger slices by relatively reducing their angles based on the total angle needed to subtract 277 // This requires that `entryCount * mMinAngleForSlices <= mMaxAngle` be true to properly work! 278 for (int i = 0; i < entryCount; i++) { 279 minAngles[i] -= (minAngles[i] - mMinAngleForSlices) / diff * offset; 280 if (i == 0) { 281 mAbsoluteAngles[0] = minAngles[0]; 282 } else { 283 mAbsoluteAngles[i] = mAbsoluteAngles[i - 1] + minAngles[i]; 284 } 285 } 286 287 mDrawAngles = minAngles; 288 } 289 } 290 291 /** 292 * Checks if the given index is set to be highlighted. 293 * 294 * @param index 295 * @return 296 */ needsHighlight(int index)297 public boolean needsHighlight(int index) { 298 299 // no highlight 300 if (!valuesToHighlight()) 301 return false; 302 303 for (int i = 0; i < mIndicesToHighlight.length; i++) 304 305 // check if the xvalue for the given dataset needs highlight 306 if ((int) mIndicesToHighlight[i].getX() == index) 307 return true; 308 309 return false; 310 } 311 312 /** 313 * calculates the needed angle for a given value 314 * 315 * @param value 316 * @return 317 */ calcAngle(float value)318 private float calcAngle(float value) { 319 return calcAngle(value, mData.getYValueSum()); 320 } 321 322 /** 323 * calculates the needed angle for a given value 324 * 325 * @param value 326 * @param yValueSum 327 * @return 328 */ calcAngle(float value, float yValueSum)329 private float calcAngle(float value, float yValueSum) { 330 return value / yValueSum * mMaxAngle; 331 } 332 333 /** 334 * This will throw an exception, PieChart has no XAxis object. 335 * 336 * @return 337 */ 338 @Deprecated 339 @Override getXAxis()340 public XAxis getXAxis() { 341 throw new RuntimeException("PieChart has no XAxis"); 342 } 343 344 @Override getIndexForAngle(float angle)345 public int getIndexForAngle(float angle) { 346 347 // take the current angle of the chart into consideration 348 float a = Utils.getNormalizedAngle(angle - getRotationAngle()); 349 350 for (int i = 0; i < mAbsoluteAngles.length; i++) { 351 if (mAbsoluteAngles[i] > a) 352 return i; 353 } 354 355 return -1; // return -1 if no index found 356 } 357 358 /** 359 * Returns the index of the DataSet this x-index belongs to. 360 * 361 * @param xIndex 362 * @return 363 */ getDataSetIndexForIndex(int xIndex)364 public int getDataSetIndexForIndex(int xIndex) { 365 366 List<IPieDataSet> dataSets = mData.getDataSets(); 367 368 for (int i = 0; i < dataSets.size(); i++) { 369 if (dataSets.get(i).getEntryForXValue(xIndex, Float.NaN) != null) 370 return i; 371 } 372 373 return -1; 374 } 375 376 /** 377 * returns an integer array of all the different angles the chart slices 378 * have the angles in the returned array determine how much space (of 360°) 379 * each slice takes 380 * 381 * @return 382 */ getDrawAngles()383 public float[] getDrawAngles() { 384 return mDrawAngles; 385 } 386 387 /** 388 * returns the absolute angles of the different chart slices (where the 389 * slices end) 390 * 391 * @return 392 */ getAbsoluteAngles()393 public float[] getAbsoluteAngles() { 394 return mAbsoluteAngles; 395 } 396 397 /** 398 * Sets the color for the hole that is drawn in the center of the PieChart 399 * (if enabled). 400 * 401 * @param color 402 */ setHoleColor(int color)403 public void setHoleColor(int color) { 404 ((PieChartRenderer) mRenderer).getPaintHole().setColor(color); 405 } 406 407 /** 408 * Enable or disable the visibility of the inner tips of the slices behind the hole 409 */ setDrawSlicesUnderHole(boolean enable)410 public void setDrawSlicesUnderHole(boolean enable) { 411 mDrawSlicesUnderHole = enable; 412 } 413 414 /** 415 * Returns true if the inner tips of the slices are visible behind the hole, 416 * false if not. 417 * 418 * @return true if slices are visible behind the hole. 419 */ isDrawSlicesUnderHoleEnabled()420 public boolean isDrawSlicesUnderHoleEnabled() { 421 return mDrawSlicesUnderHole; 422 } 423 424 /** 425 * set this to true to draw the pie center empty 426 * 427 * @param enabled 428 */ setDrawHoleEnabled(boolean enabled)429 public void setDrawHoleEnabled(boolean enabled) { 430 this.mDrawHole = enabled; 431 } 432 433 /** 434 * returns true if the hole in the center of the pie-chart is set to be 435 * visible, false if not 436 * 437 * @return 438 */ isDrawHoleEnabled()439 public boolean isDrawHoleEnabled() { 440 return mDrawHole; 441 } 442 443 /** 444 * Sets the text String that is displayed in the center of the PieChart. 445 * 446 * @param text 447 */ setCenterText(CharSequence text)448 public void setCenterText(CharSequence text) { 449 if (text == null) 450 mCenterText = ""; 451 else 452 mCenterText = text; 453 } 454 455 /** 456 * returns the text that is drawn in the center of the pie-chart 457 * 458 * @return 459 */ getCenterText()460 public CharSequence getCenterText() { 461 return mCenterText; 462 } 463 464 /** 465 * set this to true to draw the text that is displayed in the center of the 466 * pie chart 467 * 468 * @param enabled 469 */ setDrawCenterText(boolean enabled)470 public void setDrawCenterText(boolean enabled) { 471 this.mDrawCenterText = enabled; 472 } 473 474 /** 475 * returns true if drawing the center text is enabled 476 * 477 * @return 478 */ isDrawCenterTextEnabled()479 public boolean isDrawCenterTextEnabled() { 480 return mDrawCenterText; 481 } 482 483 @Override getRequiredLegendOffset()484 protected float getRequiredLegendOffset() { 485 return mLegendRenderer.getLabelPaint().getTextSize() * 2.f; 486 } 487 488 @Override getRequiredBaseOffset()489 protected float getRequiredBaseOffset() { 490 return 0; 491 } 492 493 @Override getRadius()494 public float getRadius() { 495 if (mCircleBox == null) 496 return 0; 497 else 498 return Math.min(mCircleBox.width() / 2f, mCircleBox.height() / 2f); 499 } 500 501 /** 502 * returns the circlebox, the boundingbox of the pie-chart slices 503 * 504 * @return 505 */ getCircleBox()506 public RectF getCircleBox() { 507 return mCircleBox; 508 } 509 510 /** 511 * returns the center of the circlebox 512 * 513 * @return 514 */ getCenterCircleBox()515 public MPPointF getCenterCircleBox() { 516 return MPPointF.getInstance(mCircleBox.centerX(), mCircleBox.centerY()); 517 } 518 519 /** 520 * sets the typeface for the center-text paint 521 * 522 * @param t 523 */ setCenterTextTypeface(Typeface t)524 public void setCenterTextTypeface(Typeface t) { 525 ((PieChartRenderer) mRenderer).getPaintCenterText().setTypeface(t); 526 } 527 528 /** 529 * Sets the size of the center text of the PieChart in dp. 530 * 531 * @param sizeDp 532 */ setCenterTextSize(float sizeDp)533 public void setCenterTextSize(float sizeDp) { 534 ((PieChartRenderer) mRenderer).getPaintCenterText().setTextSize( 535 Utils.convertDpToPixel(sizeDp)); 536 } 537 538 /** 539 * Sets the size of the center text of the PieChart in pixels. 540 * 541 * @param sizePixels 542 */ setCenterTextSizePixels(float sizePixels)543 public void setCenterTextSizePixels(float sizePixels) { 544 ((PieChartRenderer) mRenderer).getPaintCenterText().setTextSize(sizePixels); 545 } 546 547 /** 548 * Sets the offset the center text should have from it's original position in dp. Default x = 0, y = 0 549 * 550 * @param x 551 * @param y 552 */ setCenterTextOffset(float x, float y)553 public void setCenterTextOffset(float x, float y) { 554 mCenterTextOffset.x = Utils.convertDpToPixel(x); 555 mCenterTextOffset.y = Utils.convertDpToPixel(y); 556 } 557 558 /** 559 * Returns the offset on the x- and y-axis the center text has in dp. 560 * 561 * @return 562 */ getCenterTextOffset()563 public MPPointF getCenterTextOffset() { 564 return MPPointF.getInstance(mCenterTextOffset.x, mCenterTextOffset.y); 565 } 566 567 /** 568 * Sets the color of the center text of the PieChart. 569 * 570 * @param color 571 */ setCenterTextColor(int color)572 public void setCenterTextColor(int color) { 573 ((PieChartRenderer) mRenderer).getPaintCenterText().setColor(color); 574 } 575 576 /** 577 * sets the radius of the hole in the center of the piechart in percent of 578 * the maximum radius (max = the radius of the whole chart), default 50% 579 * 580 * @param percent 581 */ setHoleRadius(final float percent)582 public void setHoleRadius(final float percent) { 583 mHoleRadiusPercent = percent; 584 } 585 586 /** 587 * Returns the size of the hole radius in percent of the total radius. 588 * 589 * @return 590 */ getHoleRadius()591 public float getHoleRadius() { 592 return mHoleRadiusPercent; 593 } 594 595 /** 596 * Sets the color the transparent-circle should have. 597 * 598 * @param color 599 */ setTransparentCircleColor(int color)600 public void setTransparentCircleColor(int color) { 601 602 Paint p = ((PieChartRenderer) mRenderer).getPaintTransparentCircle(); 603 int alpha = p.getAlpha(); 604 p.setColor(color); 605 p.setAlpha(alpha); 606 } 607 608 /** 609 * sets the radius of the transparent circle that is drawn next to the hole 610 * in the piechart in percent of the maximum radius (max = the radius of the 611 * whole chart), default 55% -> means 5% larger than the center-hole by 612 * default 613 * 614 * @param percent 615 */ setTransparentCircleRadius(final float percent)616 public void setTransparentCircleRadius(final float percent) { 617 mTransparentCircleRadiusPercent = percent; 618 } 619 getTransparentCircleRadius()620 public float getTransparentCircleRadius() { 621 return mTransparentCircleRadiusPercent; 622 } 623 624 /** 625 * Sets the amount of transparency the transparent circle should have 0 = fully transparent, 626 * 255 = fully opaque. 627 * Default value is 100. 628 * 629 * @param alpha 0-255 630 */ setTransparentCircleAlpha(int alpha)631 public void setTransparentCircleAlpha(int alpha) { 632 ((PieChartRenderer) mRenderer).getPaintTransparentCircle().setAlpha(alpha); 633 } 634 635 /** 636 * Set this to true to draw the entry labels into the pie slices (Provided by the getLabel() method of the PieEntry class). 637 * Deprecated -> use setDrawEntryLabels(...) instead. 638 * 639 * @param enabled 640 */ 641 @Deprecated setDrawSliceText(boolean enabled)642 public void setDrawSliceText(boolean enabled) { 643 mDrawEntryLabels = enabled; 644 } 645 646 /** 647 * Set this to true to draw the entry labels into the pie slices (Provided by the getLabel() method of the PieEntry class). 648 * 649 * @param enabled 650 */ setDrawEntryLabels(boolean enabled)651 public void setDrawEntryLabels(boolean enabled) { 652 mDrawEntryLabels = enabled; 653 } 654 655 /** 656 * Returns true if drawing the entry labels is enabled, false if not. 657 * 658 * @return 659 */ isDrawEntryLabelsEnabled()660 public boolean isDrawEntryLabelsEnabled() { 661 return mDrawEntryLabels; 662 } 663 664 /** 665 * Sets the color the entry labels are drawn with. 666 * 667 * @param color 668 */ setEntryLabelColor(int color)669 public void setEntryLabelColor(int color) { 670 ((PieChartRenderer) mRenderer).getPaintEntryLabels().setColor(color); 671 } 672 673 /** 674 * Sets a custom Typeface for the drawing of the entry labels. 675 * 676 * @param tf 677 */ setEntryLabelTypeface(Typeface tf)678 public void setEntryLabelTypeface(Typeface tf) { 679 ((PieChartRenderer) mRenderer).getPaintEntryLabels().setTypeface(tf); 680 } 681 682 /** 683 * Sets the size of the entry labels in dp. Default: 13dp 684 * 685 * @param size 686 */ setEntryLabelTextSize(float size)687 public void setEntryLabelTextSize(float size) { 688 ((PieChartRenderer) mRenderer).getPaintEntryLabels().setTextSize(Utils.convertDpToPixel(size)); 689 } 690 691 /** 692 * Sets whether to draw slices in a curved fashion, only works if drawing the hole is enabled 693 * and if the slices are not drawn under the hole. 694 * 695 * @param enabled draw curved ends of slices 696 */ setDrawRoundedSlices(boolean enabled)697 public void setDrawRoundedSlices(boolean enabled) { 698 mDrawRoundedSlices = enabled; 699 } 700 701 /** 702 * Returns true if the chart is set to draw each end of a pie-slice 703 * "rounded". 704 * 705 * @return 706 */ isDrawRoundedSlicesEnabled()707 public boolean isDrawRoundedSlicesEnabled() { 708 return mDrawRoundedSlices; 709 } 710 711 /** 712 * If this is enabled, values inside the PieChart are drawn in percent and 713 * not with their original value. Values provided for the IValueFormatter to 714 * format are then provided in percent. 715 * 716 * @param enabled 717 */ setUsePercentValues(boolean enabled)718 public void setUsePercentValues(boolean enabled) { 719 mUsePercentValues = enabled; 720 } 721 722 /** 723 * Returns true if using percentage values is enabled for the chart. 724 * 725 * @return 726 */ isUsePercentValuesEnabled()727 public boolean isUsePercentValuesEnabled() { 728 return mUsePercentValues; 729 } 730 731 /** 732 * the rectangular radius of the bounding box for the center text, as a percentage of the pie 733 * hole 734 * default 1.f (100%) 735 */ setCenterTextRadiusPercent(float percent)736 public void setCenterTextRadiusPercent(float percent) { 737 mCenterTextRadiusPercent = percent; 738 } 739 740 /** 741 * the rectangular radius of the bounding box for the center text, as a percentage of the pie 742 * hole 743 * default 1.f (100%) 744 */ getCenterTextRadiusPercent()745 public float getCenterTextRadiusPercent() { 746 return mCenterTextRadiusPercent; 747 } 748 getMaxAngle()749 public float getMaxAngle() { 750 return mMaxAngle; 751 } 752 753 /** 754 * Sets the max angle that is used for calculating the pie-circle. 360f means 755 * it's a full PieChart, 180f results in a half-pie-chart. Default: 360f 756 * 757 * @param maxangle min 90, max 360 758 */ setMaxAngle(float maxangle)759 public void setMaxAngle(float maxangle) { 760 761 if (maxangle > 360) 762 maxangle = 360f; 763 764 if (maxangle < 90) 765 maxangle = 90f; 766 767 this.mMaxAngle = maxangle; 768 } 769 770 /** 771 * The minimum angle slices on the chart are rendered with, default is 0f. 772 * 773 * @return minimum angle for slices 774 */ getMinAngleForSlices()775 public float getMinAngleForSlices() { 776 return mMinAngleForSlices; 777 } 778 779 /** 780 * Set the angle to set minimum size for slices, you must call {@link #notifyDataSetChanged()} 781 * and {@link #invalidate()} when changing this, only works if there is enough room for all 782 * slices to have the minimum angle. 783 * 784 * @param minAngle minimum 0, maximum is half of {@link #setMaxAngle(float)} 785 */ setMinAngleForSlices(float minAngle)786 public void setMinAngleForSlices(float minAngle) { 787 788 if (minAngle > (mMaxAngle / 2f)) 789 minAngle = mMaxAngle / 2f; 790 else if (minAngle < 0) 791 minAngle = 0f; 792 793 this.mMinAngleForSlices = minAngle; 794 } 795 796 @Override onDetachedFromWindow()797 protected void onDetachedFromWindow() { 798 // releases the bitmap in the renderer to avoid oom error 799 if (mRenderer != null && mRenderer instanceof PieChartRenderer) { 800 ((PieChartRenderer) mRenderer).releaseBitmap(); 801 } 802 super.onDetachedFromWindow(); 803 } 804 } 805