1 package com.github.mikephil.charting.components; 2 3 import android.graphics.DashPathEffect; 4 import android.graphics.Paint; 5 6 import com.github.mikephil.charting.utils.ColorTemplate; 7 import com.github.mikephil.charting.utils.FSize; 8 import com.github.mikephil.charting.utils.Utils; 9 import com.github.mikephil.charting.utils.ViewPortHandler; 10 11 import java.util.ArrayList; 12 import java.util.List; 13 14 /** 15 * Class representing the legend of the chart. The legend will contain one entry 16 * per color and DataSet. Multiple colors in one DataSet are grouped together. 17 * The legend object is NOT available before setting data to the chart. 18 * 19 * @author Philipp Jahoda 20 */ 21 public class Legend extends ComponentBase { 22 23 public enum LegendForm { 24 /** 25 * Avoid drawing a form 26 */ 27 NONE, 28 29 /** 30 * Do not draw the a form, but leave space for it 31 */ 32 EMPTY, 33 34 /** 35 * Use default (default dataset's form to the legend's form) 36 */ 37 DEFAULT, 38 39 /** 40 * Draw a square 41 */ 42 SQUARE, 43 44 /** 45 * Draw a circle 46 */ 47 CIRCLE, 48 49 /** 50 * Draw a horizontal line 51 */ 52 LINE 53 } 54 55 public enum LegendHorizontalAlignment { 56 LEFT, CENTER, RIGHT 57 } 58 59 public enum LegendVerticalAlignment { 60 TOP, CENTER, BOTTOM 61 } 62 63 public enum LegendOrientation { 64 HORIZONTAL, VERTICAL 65 } 66 67 public enum LegendDirection { 68 LEFT_TO_RIGHT, RIGHT_TO_LEFT 69 } 70 71 /** 72 * The legend entries array 73 */ 74 private LegendEntry[] mEntries = new LegendEntry[]{}; 75 76 /** 77 * Entries that will be appended to the end of the auto calculated entries after calculating the legend. 78 * (if the legend has already been calculated, you will need to call notifyDataSetChanged() to let the changes take effect) 79 */ 80 private LegendEntry[] mExtraEntries; 81 82 /** 83 * Are the legend labels/colors a custom value or auto calculated? If false, 84 * then it's auto, if true, then custom. default false (automatic legend) 85 */ 86 private boolean mIsLegendCustom = false; 87 88 private LegendHorizontalAlignment mHorizontalAlignment = LegendHorizontalAlignment.LEFT; 89 private LegendVerticalAlignment mVerticalAlignment = LegendVerticalAlignment.BOTTOM; 90 private LegendOrientation mOrientation = LegendOrientation.HORIZONTAL; 91 private boolean mDrawInside = false; 92 93 /** 94 * the text direction for the legend 95 */ 96 private LegendDirection mDirection = LegendDirection.LEFT_TO_RIGHT; 97 98 /** 99 * the shape/form the legend colors are drawn in 100 */ 101 private LegendForm mShape = LegendForm.SQUARE; 102 103 /** 104 * the size of the legend forms/shapes 105 */ 106 private float mFormSize = 8f; 107 108 /** 109 * the size of the legend forms/shapes 110 */ 111 private float mFormLineWidth = 3f; 112 113 /** 114 * Line dash path effect used for shapes that consist of lines. 115 */ 116 private DashPathEffect mFormLineDashEffect = null; 117 118 /** 119 * the space between the legend entries on a horizontal axis, default 6f 120 */ 121 private float mXEntrySpace = 6f; 122 123 /** 124 * the space between the legend entries on a vertical axis, default 5f 125 */ 126 private float mYEntrySpace = 0f; 127 128 /** 129 * the space between the legend entries on a vertical axis, default 2f 130 * private float mYEntrySpace = 2f; /** the space between the form and the 131 * actual label/text 132 */ 133 private float mFormToTextSpace = 5f; 134 135 /** 136 * the space that should be left between stacked forms 137 */ 138 private float mStackSpace = 3f; 139 140 /** 141 * the maximum relative size out of the whole chart view in percent 142 */ 143 private float mMaxSizePercent = 0.95f; 144 145 /** 146 * default constructor 147 */ Legend()148 public Legend() { 149 150 this.mTextSize = Utils.convertDpToPixel(10f); 151 this.mXOffset = Utils.convertDpToPixel(5f); 152 this.mYOffset = Utils.convertDpToPixel(3f); // 2 153 } 154 155 /** 156 * Constructor. Provide entries for the legend. 157 * 158 * @param entries 159 */ Legend(LegendEntry[] entries)160 public Legend(LegendEntry[] entries) { 161 this(); 162 163 if (entries == null) { 164 throw new IllegalArgumentException("entries array is NULL"); 165 } 166 167 this.mEntries = entries; 168 } 169 170 /** 171 * This method sets the automatically computed colors for the legend. Use setCustom(...) to set custom colors. 172 * 173 * @param entries 174 */ setEntries(List<LegendEntry> entries)175 public void setEntries(List<LegendEntry> entries) { 176 mEntries = entries.toArray(new LegendEntry[entries.size()]); 177 } 178 getEntries()179 public LegendEntry[] getEntries() { 180 return mEntries; 181 } 182 183 /** 184 * returns the maximum length in pixels across all legend labels + formsize 185 * + formtotextspace 186 * 187 * @param p the paint object used for rendering the text 188 * @return 189 */ getMaximumEntryWidth(Paint p)190 public float getMaximumEntryWidth(Paint p) { 191 192 float max = 0f; 193 float maxFormSize = 0f; 194 float formToTextSpace = Utils.convertDpToPixel(mFormToTextSpace); 195 196 for (LegendEntry entry : mEntries) { 197 final float formSize = Utils.convertDpToPixel( 198 Float.isNaN(entry.formSize) 199 ? mFormSize : entry.formSize); 200 if (formSize > maxFormSize) 201 maxFormSize = formSize; 202 203 String label = entry.label; 204 if (label == null) continue; 205 206 float length = (float) Utils.calcTextWidth(p, label); 207 208 if (length > max) 209 max = length; 210 } 211 212 return max + maxFormSize + formToTextSpace; 213 } 214 215 /** 216 * returns the maximum height in pixels across all legend labels 217 * 218 * @param p the paint object used for rendering the text 219 * @return 220 */ getMaximumEntryHeight(Paint p)221 public float getMaximumEntryHeight(Paint p) { 222 223 float max = 0f; 224 225 for (LegendEntry entry : mEntries) { 226 String label = entry.label; 227 if (label == null) continue; 228 229 float length = (float) Utils.calcTextHeight(p, label); 230 231 if (length > max) 232 max = length; 233 } 234 235 return max; 236 } 237 getExtraEntries()238 public LegendEntry[] getExtraEntries() { 239 240 return mExtraEntries; 241 } 242 setExtra(List<LegendEntry> entries)243 public void setExtra(List<LegendEntry> entries) { 244 mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); 245 } 246 setExtra(LegendEntry[] entries)247 public void setExtra(LegendEntry[] entries) { 248 if (entries == null) 249 entries = new LegendEntry[]{}; 250 mExtraEntries = entries; 251 } 252 253 /** 254 * Entries that will be appended to the end of the auto calculated 255 * entries after calculating the legend. 256 * (if the legend has already been calculated, you will need to call notifyDataSetChanged() 257 * to let the changes take effect) 258 */ setExtra(int[] colors, String[] labels)259 public void setExtra(int[] colors, String[] labels) { 260 261 List<LegendEntry> entries = new ArrayList<>(); 262 263 for (int i = 0; i < Math.min(colors.length, labels.length); i++) { 264 final LegendEntry entry = new LegendEntry(); 265 entry.formColor = colors[i]; 266 entry.label = labels[i]; 267 268 if (entry.formColor == ColorTemplate.COLOR_SKIP || 269 entry.formColor == 0) 270 entry.form = LegendForm.NONE; 271 else if (entry.formColor == ColorTemplate.COLOR_NONE) 272 entry.form = LegendForm.EMPTY; 273 274 entries.add(entry); 275 } 276 277 mExtraEntries = entries.toArray(new LegendEntry[entries.size()]); 278 } 279 280 /** 281 * Sets a custom legend's entries array. 282 * * A null label will start a group. 283 * This will disable the feature that automatically calculates the legend 284 * entries from the datasets. 285 * Call resetCustom() to re-enable automatic calculation (and then 286 * notifyDataSetChanged() is needed to auto-calculate the legend again) 287 */ setCustom(LegendEntry[] entries)288 public void setCustom(LegendEntry[] entries) { 289 290 mEntries = entries; 291 mIsLegendCustom = true; 292 } 293 294 /** 295 * Sets a custom legend's entries array. 296 * * A null label will start a group. 297 * This will disable the feature that automatically calculates the legend 298 * entries from the datasets. 299 * Call resetCustom() to re-enable automatic calculation (and then 300 * notifyDataSetChanged() is needed to auto-calculate the legend again) 301 */ setCustom(List<LegendEntry> entries)302 public void setCustom(List<LegendEntry> entries) { 303 304 mEntries = entries.toArray(new LegendEntry[entries.size()]); 305 mIsLegendCustom = true; 306 } 307 308 /** 309 * Calling this will disable the custom legend entries (set by 310 * setCustom(...)). Instead, the entries will again be calculated 311 * automatically (after notifyDataSetChanged() is called). 312 */ resetCustom()313 public void resetCustom() { 314 mIsLegendCustom = false; 315 } 316 317 /** 318 * @return true if a custom legend entries has been set default 319 * false (automatic legend) 320 */ isLegendCustom()321 public boolean isLegendCustom() { 322 return mIsLegendCustom; 323 } 324 325 /** 326 * returns the horizontal alignment of the legend 327 * 328 * @return 329 */ getHorizontalAlignment()330 public LegendHorizontalAlignment getHorizontalAlignment() { 331 return mHorizontalAlignment; 332 } 333 334 /** 335 * sets the horizontal alignment of the legend 336 * 337 * @param value 338 */ setHorizontalAlignment(LegendHorizontalAlignment value)339 public void setHorizontalAlignment(LegendHorizontalAlignment value) { 340 mHorizontalAlignment = value; 341 } 342 343 /** 344 * returns the vertical alignment of the legend 345 * 346 * @return 347 */ getVerticalAlignment()348 public LegendVerticalAlignment getVerticalAlignment() { 349 return mVerticalAlignment; 350 } 351 352 /** 353 * sets the vertical alignment of the legend 354 * 355 * @param value 356 */ setVerticalAlignment(LegendVerticalAlignment value)357 public void setVerticalAlignment(LegendVerticalAlignment value) { 358 mVerticalAlignment = value; 359 } 360 361 /** 362 * returns the orientation of the legend 363 * 364 * @return 365 */ getOrientation()366 public LegendOrientation getOrientation() { 367 return mOrientation; 368 } 369 370 /** 371 * sets the orientation of the legend 372 * 373 * @param value 374 */ setOrientation(LegendOrientation value)375 public void setOrientation(LegendOrientation value) { 376 mOrientation = value; 377 } 378 379 /** 380 * returns whether the legend will draw inside the chart or outside 381 * 382 * @return 383 */ isDrawInsideEnabled()384 public boolean isDrawInsideEnabled() { 385 return mDrawInside; 386 } 387 388 /** 389 * sets whether the legend will draw inside the chart or outside 390 * 391 * @param value 392 */ setDrawInside(boolean value)393 public void setDrawInside(boolean value) { 394 mDrawInside = value; 395 } 396 397 /** 398 * returns the text direction of the legend 399 * 400 * @return 401 */ getDirection()402 public LegendDirection getDirection() { 403 return mDirection; 404 } 405 406 /** 407 * sets the text direction of the legend 408 * 409 * @param pos 410 */ setDirection(LegendDirection pos)411 public void setDirection(LegendDirection pos) { 412 mDirection = pos; 413 } 414 415 /** 416 * returns the current form/shape that is set for the legend 417 * 418 * @return 419 */ getForm()420 public LegendForm getForm() { 421 return mShape; 422 } 423 424 /** 425 * sets the form/shape of the legend forms 426 * 427 * @param shape 428 */ setForm(LegendForm shape)429 public void setForm(LegendForm shape) { 430 mShape = shape; 431 } 432 433 /** 434 * sets the size in dp of the legend forms, default 8f 435 * 436 * @param size 437 */ setFormSize(float size)438 public void setFormSize(float size) { 439 mFormSize = size; 440 } 441 442 /** 443 * returns the size in dp of the legend forms 444 * 445 * @return 446 */ getFormSize()447 public float getFormSize() { 448 return mFormSize; 449 } 450 451 /** 452 * sets the line width in dp for forms that consist of lines, default 3f 453 * 454 * @param size 455 */ setFormLineWidth(float size)456 public void setFormLineWidth(float size) { 457 mFormLineWidth = size; 458 } 459 460 /** 461 * returns the line width in dp for drawing forms that consist of lines 462 * 463 * @return 464 */ getFormLineWidth()465 public float getFormLineWidth() { 466 return mFormLineWidth; 467 } 468 469 /** 470 * Sets the line dash path effect used for shapes that consist of lines. 471 * 472 * @param dashPathEffect 473 */ setFormLineDashEffect(DashPathEffect dashPathEffect)474 public void setFormLineDashEffect(DashPathEffect dashPathEffect) { 475 mFormLineDashEffect = dashPathEffect; 476 } 477 478 /** 479 * @return The line dash path effect used for shapes that consist of lines. 480 */ getFormLineDashEffect()481 public DashPathEffect getFormLineDashEffect() { 482 return mFormLineDashEffect; 483 } 484 485 /** 486 * returns the space between the legend entries on a horizontal axis in 487 * pixels 488 * 489 * @return 490 */ getXEntrySpace()491 public float getXEntrySpace() { 492 return mXEntrySpace; 493 } 494 495 /** 496 * sets the space between the legend entries on a horizontal axis in pixels, 497 * converts to dp internally 498 * 499 * @param space 500 */ setXEntrySpace(float space)501 public void setXEntrySpace(float space) { 502 mXEntrySpace = space; 503 } 504 505 /** 506 * returns the space between the legend entries on a vertical axis in pixels 507 * 508 * @return 509 */ getYEntrySpace()510 public float getYEntrySpace() { 511 return mYEntrySpace; 512 } 513 514 /** 515 * sets the space between the legend entries on a vertical axis in pixels, 516 * converts to dp internally 517 * 518 * @param space 519 */ setYEntrySpace(float space)520 public void setYEntrySpace(float space) { 521 mYEntrySpace = space; 522 } 523 524 /** 525 * returns the space between the form and the actual label/text 526 * 527 * @return 528 */ getFormToTextSpace()529 public float getFormToTextSpace() { 530 return mFormToTextSpace; 531 } 532 533 /** 534 * sets the space between the form and the actual label/text, converts to dp 535 * internally 536 * 537 * @param space 538 */ setFormToTextSpace(float space)539 public void setFormToTextSpace(float space) { 540 this.mFormToTextSpace = space; 541 } 542 543 /** 544 * returns the space that is left out between stacked forms (with no label) 545 * 546 * @return 547 */ getStackSpace()548 public float getStackSpace() { 549 return mStackSpace; 550 } 551 552 /** 553 * sets the space that is left out between stacked forms (with no label) 554 * 555 * @param space 556 */ setStackSpace(float space)557 public void setStackSpace(float space) { 558 mStackSpace = space; 559 } 560 561 /** 562 * the total width of the legend (needed width space) 563 */ 564 public float mNeededWidth = 0f; 565 566 /** 567 * the total height of the legend (needed height space) 568 */ 569 public float mNeededHeight = 0f; 570 571 public float mTextHeightMax = 0f; 572 573 public float mTextWidthMax = 0f; 574 575 /** 576 * flag that indicates if word wrapping is enabled 577 */ 578 private boolean mWordWrapEnabled = false; 579 580 /** 581 * Should the legend word wrap? / this is currently supported only for: 582 * BelowChartLeft, BelowChartRight, BelowChartCenter. / note that word 583 * wrapping a legend takes a toll on performance. / you may want to set 584 * maxSizePercent when word wrapping, to set the point where the text wraps. 585 * / default: false 586 * 587 * @param enabled 588 */ setWordWrapEnabled(boolean enabled)589 public void setWordWrapEnabled(boolean enabled) { 590 mWordWrapEnabled = enabled; 591 } 592 593 /** 594 * If this is set, then word wrapping the legend is enabled. This means the 595 * legend will not be cut off if too long. 596 * 597 * @return 598 */ isWordWrapEnabled()599 public boolean isWordWrapEnabled() { 600 return mWordWrapEnabled; 601 } 602 603 /** 604 * The maximum relative size out of the whole chart view. / If the legend is 605 * to the right/left of the chart, then this affects the width of the 606 * legend. / If the legend is to the top/bottom of the chart, then this 607 * affects the height of the legend. / If the legend is the center of the 608 * piechart, then this defines the size of the rectangular bounds out of the 609 * size of the "hole". / default: 0.95f (95%) 610 * 611 * @return 612 */ getMaxSizePercent()613 public float getMaxSizePercent() { 614 return mMaxSizePercent; 615 } 616 617 /** 618 * The maximum relative size out of the whole chart view. / If 619 * the legend is to the right/left of the chart, then this affects the width 620 * of the legend. / If the legend is to the top/bottom of the chart, then 621 * this affects the height of the legend. / default: 0.95f (95%) 622 * 623 * @param maxSize 624 */ setMaxSizePercent(float maxSize)625 public void setMaxSizePercent(float maxSize) { 626 mMaxSizePercent = maxSize; 627 } 628 629 private List<FSize> mCalculatedLabelSizes = new ArrayList<>(16); 630 private List<Boolean> mCalculatedLabelBreakPoints = new ArrayList<>(16); 631 private List<FSize> mCalculatedLineSizes = new ArrayList<>(16); 632 getCalculatedLabelSizes()633 public List<FSize> getCalculatedLabelSizes() { 634 return mCalculatedLabelSizes; 635 } 636 getCalculatedLabelBreakPoints()637 public List<Boolean> getCalculatedLabelBreakPoints() { 638 return mCalculatedLabelBreakPoints; 639 } 640 getCalculatedLineSizes()641 public List<FSize> getCalculatedLineSizes() { 642 return mCalculatedLineSizes; 643 } 644 645 /** 646 * Calculates the dimensions of the Legend. This includes the maximum width 647 * and height of a single entry, as well as the total width and height of 648 * the Legend. 649 * 650 * @param labelpaint 651 */ calculateDimensions(Paint labelpaint, ViewPortHandler viewPortHandler)652 public void calculateDimensions(Paint labelpaint, ViewPortHandler viewPortHandler) { 653 654 float defaultFormSize = Utils.convertDpToPixel(mFormSize); 655 float stackSpace = Utils.convertDpToPixel(mStackSpace); 656 float formToTextSpace = Utils.convertDpToPixel(mFormToTextSpace); 657 float xEntrySpace = Utils.convertDpToPixel(mXEntrySpace); 658 float yEntrySpace = Utils.convertDpToPixel(mYEntrySpace); 659 boolean wordWrapEnabled = mWordWrapEnabled; 660 LegendEntry[] entries = mEntries; 661 int entryCount = entries.length; 662 663 mTextWidthMax = getMaximumEntryWidth(labelpaint); 664 mTextHeightMax = getMaximumEntryHeight(labelpaint); 665 666 switch (mOrientation) { 667 case VERTICAL: { 668 669 float maxWidth = 0f, maxHeight = 0f, width = 0f; 670 float labelLineHeight = Utils.getLineHeight(labelpaint); 671 boolean wasStacked = false; 672 673 for (int i = 0; i < entryCount; i++) { 674 675 LegendEntry e = entries[i]; 676 boolean drawingForm = e.form != LegendForm.NONE; 677 float formSize = Float.isNaN(e.formSize) 678 ? defaultFormSize 679 : Utils.convertDpToPixel(e.formSize); 680 String label = e.label; 681 682 if (!wasStacked) 683 width = 0.f; 684 685 if (drawingForm) { 686 if (wasStacked) 687 width += stackSpace; 688 width += formSize; 689 } 690 691 // grouped forms have null labels 692 if (label != null) { 693 694 // make a step to the left 695 if (drawingForm && !wasStacked) 696 width += formToTextSpace; 697 else if (wasStacked) { 698 maxWidth = Math.max(maxWidth, width); 699 maxHeight += labelLineHeight + yEntrySpace; 700 width = 0.f; 701 wasStacked = false; 702 } 703 704 width += Utils.calcTextWidth(labelpaint, label); 705 706 maxHeight += labelLineHeight + yEntrySpace; 707 } else { 708 wasStacked = true; 709 width += formSize; 710 if (i < entryCount - 1) 711 width += stackSpace; 712 } 713 714 maxWidth = Math.max(maxWidth, width); 715 } 716 717 mNeededWidth = maxWidth; 718 mNeededHeight = maxHeight; 719 720 break; 721 } 722 case HORIZONTAL: { 723 724 float labelLineHeight = Utils.getLineHeight(labelpaint); 725 float labelLineSpacing = Utils.getLineSpacing(labelpaint) + yEntrySpace; 726 float contentWidth = viewPortHandler.contentWidth() * mMaxSizePercent; 727 728 // Start calculating layout 729 float maxLineWidth = 0.f; 730 float currentLineWidth = 0.f; 731 float requiredWidth = 0.f; 732 int stackedStartIndex = -1; 733 734 mCalculatedLabelBreakPoints.clear(); 735 mCalculatedLabelSizes.clear(); 736 mCalculatedLineSizes.clear(); 737 738 for (int i = 0; i < entryCount; i++) { 739 740 LegendEntry e = entries[i]; 741 boolean drawingForm = e.form != LegendForm.NONE; 742 float formSize = Float.isNaN(e.formSize) 743 ? defaultFormSize 744 : Utils.convertDpToPixel(e.formSize); 745 String label = e.label; 746 747 mCalculatedLabelBreakPoints.add(false); 748 749 if (stackedStartIndex == -1) { 750 // we are not stacking, so required width is for this label 751 // only 752 requiredWidth = 0.f; 753 } else { 754 // add the spacing appropriate for stacked labels/forms 755 requiredWidth += stackSpace; 756 } 757 758 // grouped forms have null labels 759 if (label != null) { 760 761 mCalculatedLabelSizes.add(Utils.calcTextSize(labelpaint, label)); 762 requiredWidth += drawingForm ? formToTextSpace + formSize : 0.f; 763 requiredWidth += mCalculatedLabelSizes.get(i).width; 764 } else { 765 766 mCalculatedLabelSizes.add(FSize.getInstance(0.f, 0.f)); 767 requiredWidth += drawingForm ? formSize : 0.f; 768 769 if (stackedStartIndex == -1) { 770 // mark this index as we might want to break here later 771 stackedStartIndex = i; 772 } 773 } 774 775 if (label != null || i == entryCount - 1) { 776 777 float requiredSpacing = currentLineWidth == 0.f ? 0.f : xEntrySpace; 778 779 if (!wordWrapEnabled // No word wrapping, it must fit. 780 // The line is empty, it must fit 781 || currentLineWidth == 0.f 782 // It simply fits 783 || (contentWidth - currentLineWidth >= 784 requiredSpacing + requiredWidth)) { 785 // Expand current line 786 currentLineWidth += requiredSpacing + requiredWidth; 787 } else { // It doesn't fit, we need to wrap a line 788 789 // Add current line size to array 790 mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); 791 maxLineWidth = Math.max(maxLineWidth, currentLineWidth); 792 793 // Start a new line 794 mCalculatedLabelBreakPoints.set( 795 stackedStartIndex > -1 ? stackedStartIndex 796 : i, true); 797 currentLineWidth = requiredWidth; 798 } 799 800 if (i == entryCount - 1) { 801 // Add last line size to array 802 mCalculatedLineSizes.add(FSize.getInstance(currentLineWidth, labelLineHeight)); 803 maxLineWidth = Math.max(maxLineWidth, currentLineWidth); 804 } 805 } 806 807 stackedStartIndex = label != null ? -1 : stackedStartIndex; 808 } 809 810 mNeededWidth = maxLineWidth; 811 mNeededHeight = labelLineHeight 812 * (float) (mCalculatedLineSizes.size()) 813 + labelLineSpacing * 814 (float) (mCalculatedLineSizes.size() == 0 815 ? 0 816 : (mCalculatedLineSizes.size() - 1)); 817 818 break; 819 } 820 } 821 822 mNeededHeight += mYOffset; 823 mNeededWidth += mXOffset; 824 } 825 } 826