1 2 package com.github.mikephil.charting.renderer; 3 4 import android.graphics.Canvas; 5 import android.graphics.DashPathEffect; 6 import android.graphics.Paint; 7 import android.graphics.Paint.Align; 8 import android.graphics.Path; 9 import android.graphics.Typeface; 10 11 import com.github.mikephil.charting.components.Legend; 12 import com.github.mikephil.charting.components.LegendEntry; 13 import com.github.mikephil.charting.data.ChartData; 14 import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; 15 import com.github.mikephil.charting.interfaces.datasets.ICandleDataSet; 16 import com.github.mikephil.charting.interfaces.datasets.IDataSet; 17 import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; 18 import com.github.mikephil.charting.utils.ColorTemplate; 19 import com.github.mikephil.charting.utils.FSize; 20 import com.github.mikephil.charting.utils.Utils; 21 import com.github.mikephil.charting.utils.ViewPortHandler; 22 23 import java.util.ArrayList; 24 import java.util.Collections; 25 import java.util.List; 26 27 public class LegendRenderer extends Renderer { 28 29 /** 30 * paint for the legend labels 31 */ 32 protected Paint mLegendLabelPaint; 33 34 /** 35 * paint used for the legend forms 36 */ 37 protected Paint mLegendFormPaint; 38 39 /** 40 * the legend object this renderer renders 41 */ 42 protected Legend mLegend; 43 LegendRenderer(ViewPortHandler viewPortHandler, Legend legend)44 public LegendRenderer(ViewPortHandler viewPortHandler, Legend legend) { 45 super(viewPortHandler); 46 47 this.mLegend = legend; 48 49 mLegendLabelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 50 mLegendLabelPaint.setTextSize(Utils.convertDpToPixel(9f)); 51 mLegendLabelPaint.setTextAlign(Align.LEFT); 52 53 mLegendFormPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 54 mLegendFormPaint.setStyle(Paint.Style.FILL); 55 } 56 57 /** 58 * Returns the Paint object used for drawing the Legend labels. 59 * 60 * @return 61 */ getLabelPaint()62 public Paint getLabelPaint() { 63 return mLegendLabelPaint; 64 } 65 66 /** 67 * Returns the Paint object used for drawing the Legend forms. 68 * 69 * @return 70 */ getFormPaint()71 public Paint getFormPaint() { 72 return mLegendFormPaint; 73 } 74 75 76 protected List<LegendEntry> computedEntries = new ArrayList<>(16); 77 78 /** 79 * Prepares the legend and calculates all needed forms, labels and colors. 80 * 81 * @param data 82 */ computeLegend(ChartData<?> data)83 public void computeLegend(ChartData<?> data) { 84 85 if (!mLegend.isLegendCustom()) { 86 87 computedEntries.clear(); 88 89 // loop for building up the colors and labels used in the legend 90 for (int i = 0; i < data.getDataSetCount(); i++) { 91 92 IDataSet dataSet = data.getDataSetByIndex(i); 93 if (dataSet == null) continue; 94 95 List<Integer> clrs = dataSet.getColors(); 96 int entryCount = dataSet.getEntryCount(); 97 98 // if we have a barchart with stacked bars 99 if (dataSet instanceof IBarDataSet && ((IBarDataSet) dataSet).isStacked()) { 100 101 IBarDataSet bds = (IBarDataSet) dataSet; 102 String[] sLabels = bds.getStackLabels(); 103 104 int minEntries = Math.min(clrs.size(), bds.getStackSize()); 105 106 for (int j = 0; j < minEntries; j++) { 107 String label; 108 if (sLabels.length > 0) { 109 int labelIndex = j % minEntries; 110 label = labelIndex < sLabels.length ? sLabels[labelIndex] : null; 111 } else { 112 label = null; 113 } 114 115 computedEntries.add(new LegendEntry( 116 label, 117 dataSet.getForm(), 118 dataSet.getFormSize(), 119 dataSet.getFormLineWidth(), 120 dataSet.getFormLineDashEffect(), 121 clrs.get(j) 122 )); 123 } 124 125 if (bds.getLabel() != null) { 126 // add the legend description label 127 computedEntries.add(new LegendEntry( 128 dataSet.getLabel(), 129 Legend.LegendForm.NONE, 130 Float.NaN, 131 Float.NaN, 132 null, 133 ColorTemplate.COLOR_NONE 134 )); 135 } 136 137 } else if (dataSet instanceof IPieDataSet) { 138 139 IPieDataSet pds = (IPieDataSet) dataSet; 140 141 for (int j = 0; j < clrs.size() && j < entryCount; j++) { 142 143 computedEntries.add(new LegendEntry( 144 pds.getEntryForIndex(j).getLabel(), 145 dataSet.getForm(), 146 dataSet.getFormSize(), 147 dataSet.getFormLineWidth(), 148 dataSet.getFormLineDashEffect(), 149 clrs.get(j) 150 )); 151 } 152 153 if (pds.getLabel() != null) { 154 // add the legend description label 155 computedEntries.add(new LegendEntry( 156 dataSet.getLabel(), 157 Legend.LegendForm.NONE, 158 Float.NaN, 159 Float.NaN, 160 null, 161 ColorTemplate.COLOR_NONE 162 )); 163 } 164 165 } else if (dataSet instanceof ICandleDataSet && ((ICandleDataSet) dataSet).getDecreasingColor() != 166 ColorTemplate.COLOR_NONE) { 167 168 int decreasingColor = ((ICandleDataSet) dataSet).getDecreasingColor(); 169 int increasingColor = ((ICandleDataSet) dataSet).getIncreasingColor(); 170 171 computedEntries.add(new LegendEntry( 172 null, 173 dataSet.getForm(), 174 dataSet.getFormSize(), 175 dataSet.getFormLineWidth(), 176 dataSet.getFormLineDashEffect(), 177 decreasingColor 178 )); 179 180 computedEntries.add(new LegendEntry( 181 dataSet.getLabel(), 182 dataSet.getForm(), 183 dataSet.getFormSize(), 184 dataSet.getFormLineWidth(), 185 dataSet.getFormLineDashEffect(), 186 increasingColor 187 )); 188 189 } else { // all others 190 191 for (int j = 0; j < clrs.size() && j < entryCount; j++) { 192 193 String label; 194 195 // if multiple colors are set for a DataSet, group them 196 if (j < clrs.size() - 1 && j < entryCount - 1) { 197 label = null; 198 } else { // add label to the last entry 199 label = data.getDataSetByIndex(i).getLabel(); 200 } 201 202 computedEntries.add(new LegendEntry( 203 label, 204 dataSet.getForm(), 205 dataSet.getFormSize(), 206 dataSet.getFormLineWidth(), 207 dataSet.getFormLineDashEffect(), 208 clrs.get(j) 209 )); 210 } 211 } 212 } 213 214 if (mLegend.getExtraEntries() != null) { 215 Collections.addAll(computedEntries, mLegend.getExtraEntries()); 216 } 217 218 mLegend.setEntries(computedEntries); 219 } 220 221 Typeface tf = mLegend.getTypeface(); 222 223 if (tf != null) 224 mLegendLabelPaint.setTypeface(tf); 225 226 mLegendLabelPaint.setTextSize(mLegend.getTextSize()); 227 mLegendLabelPaint.setColor(mLegend.getTextColor()); 228 229 // calculate all dimensions of the mLegend 230 mLegend.calculateDimensions(mLegendLabelPaint, mViewPortHandler); 231 } 232 233 protected Paint.FontMetrics legendFontMetrics = new Paint.FontMetrics(); 234 235 public void renderLegend(Canvas c) { 236 237 if (!mLegend.isEnabled()) 238 return; 239 240 Typeface tf = mLegend.getTypeface(); 241 242 if (tf != null) 243 mLegendLabelPaint.setTypeface(tf); 244 245 mLegendLabelPaint.setTextSize(mLegend.getTextSize()); 246 mLegendLabelPaint.setColor(mLegend.getTextColor()); 247 248 float labelLineHeight = Utils.getLineHeight(mLegendLabelPaint, legendFontMetrics); 249 float labelLineSpacing = Utils.getLineSpacing(mLegendLabelPaint, legendFontMetrics) 250 + Utils.convertDpToPixel(mLegend.getYEntrySpace()); 251 float formYOffset = labelLineHeight - Utils.calcTextHeight(mLegendLabelPaint, "ABC") / 2.f; 252 253 LegendEntry[] entries = mLegend.getEntries(); 254 255 float formToTextSpace = Utils.convertDpToPixel(mLegend.getFormToTextSpace()); 256 float xEntrySpace = Utils.convertDpToPixel(mLegend.getXEntrySpace()); 257 Legend.LegendOrientation orientation = mLegend.getOrientation(); 258 Legend.LegendHorizontalAlignment horizontalAlignment = mLegend.getHorizontalAlignment(); 259 Legend.LegendVerticalAlignment verticalAlignment = mLegend.getVerticalAlignment(); 260 Legend.LegendDirection direction = mLegend.getDirection(); 261 float defaultFormSize = Utils.convertDpToPixel(mLegend.getFormSize()); 262 263 // space between the entries 264 float stackSpace = Utils.convertDpToPixel(mLegend.getStackSpace()); 265 266 float yoffset = mLegend.getYOffset(); 267 float xoffset = mLegend.getXOffset(); 268 float originPosX = 0.f; 269 270 switch (horizontalAlignment) { 271 case LEFT: 272 273 if (orientation == Legend.LegendOrientation.VERTICAL) 274 originPosX = xoffset; 275 else 276 originPosX = mViewPortHandler.contentLeft() + xoffset; 277 278 if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) 279 originPosX += mLegend.mNeededWidth; 280 281 break; 282 283 case RIGHT: 284 285 if (orientation == Legend.LegendOrientation.VERTICAL) 286 originPosX = mViewPortHandler.getChartWidth() - xoffset; 287 else 288 originPosX = mViewPortHandler.contentRight() - xoffset; 289 290 if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) 291 originPosX -= mLegend.mNeededWidth; 292 293 break; 294 295 case CENTER: 296 297 if (orientation == Legend.LegendOrientation.VERTICAL) 298 originPosX = mViewPortHandler.getChartWidth() / 2.f; 299 else 300 originPosX = mViewPortHandler.contentLeft() 301 + mViewPortHandler.contentWidth() / 2.f; 302 303 originPosX += (direction == Legend.LegendDirection.LEFT_TO_RIGHT 304 ? +xoffset 305 : -xoffset); 306 307 // Horizontally layed out legends do the center offset on a line basis, 308 // So here we offset the vertical ones only. 309 if (orientation == Legend.LegendOrientation.VERTICAL) { 310 originPosX += (direction == Legend.LegendDirection.LEFT_TO_RIGHT 311 ? -mLegend.mNeededWidth / 2.0 + xoffset 312 : mLegend.mNeededWidth / 2.0 - xoffset); 313 } 314 315 break; 316 } 317 318 switch (orientation) { 319 case HORIZONTAL: { 320 321 List<FSize> calculatedLineSizes = mLegend.getCalculatedLineSizes(); 322 List<FSize> calculatedLabelSizes = mLegend.getCalculatedLabelSizes(); 323 List<Boolean> calculatedLabelBreakPoints = mLegend.getCalculatedLabelBreakPoints(); 324 325 float posX = originPosX; 326 float posY = 0.f; 327 328 switch (verticalAlignment) { 329 case TOP: 330 posY = yoffset; 331 break; 332 333 case BOTTOM: 334 posY = mViewPortHandler.getChartHeight() - yoffset - mLegend.mNeededHeight; 335 break; 336 337 case CENTER: 338 posY = (mViewPortHandler.getChartHeight() - mLegend.mNeededHeight) / 2.f + yoffset; 339 break; 340 } 341 342 int lineIndex = 0; 343 344 for (int i = 0, count = entries.length; i < count; i++) { 345 346 LegendEntry e = entries[i]; 347 boolean drawingForm = e.form != Legend.LegendForm.NONE; 348 float formSize = Float.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); 349 350 if (i < calculatedLabelBreakPoints.size() && calculatedLabelBreakPoints.get(i)) { 351 posX = originPosX; 352 posY += labelLineHeight + labelLineSpacing; 353 } 354 355 if (posX == originPosX && 356 horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER && 357 lineIndex < calculatedLineSizes.size()) { 358 posX += (direction == Legend.LegendDirection.RIGHT_TO_LEFT 359 ? calculatedLineSizes.get(lineIndex).width 360 : -calculatedLineSizes.get(lineIndex).width) / 2.f; 361 lineIndex++; 362 } 363 364 boolean isStacked = e.label == null; // grouped forms have null labels 365 366 if (drawingForm) { 367 if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) 368 posX -= formSize; 369 370 drawForm(c, posX, posY + formYOffset, e, mLegend); 371 372 if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) 373 posX += formSize; 374 } 375 376 if (!isStacked) { 377 if (drawingForm) 378 posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -formToTextSpace : 379 formToTextSpace; 380 381 if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) 382 posX -= calculatedLabelSizes.get(i).width; 383 384 drawLabel(c, posX, posY + labelLineHeight, e.label); 385 386 if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) 387 posX += calculatedLabelSizes.get(i).width; 388 389 posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -xEntrySpace : xEntrySpace; 390 } else 391 posX += direction == Legend.LegendDirection.RIGHT_TO_LEFT ? -stackSpace : stackSpace; 392 } 393 394 break; 395 } 396 397 case VERTICAL: { 398 // contains the stacked legend size in pixels 399 float stack = 0f; 400 boolean wasStacked = false; 401 float posY = 0.f; 402 403 switch (verticalAlignment) { 404 case TOP: 405 posY = (horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER 406 ? 0.f 407 : mViewPortHandler.contentTop()); 408 posY += yoffset; 409 break; 410 411 case BOTTOM: 412 posY = (horizontalAlignment == Legend.LegendHorizontalAlignment.CENTER 413 ? mViewPortHandler.getChartHeight() 414 : mViewPortHandler.contentBottom()); 415 posY -= mLegend.mNeededHeight + yoffset; 416 break; 417 418 case CENTER: 419 posY = mViewPortHandler.getChartHeight() / 2.f 420 - mLegend.mNeededHeight / 2.f 421 + mLegend.getYOffset(); 422 break; 423 } 424 425 for (int i = 0; i < entries.length; i++) { 426 427 LegendEntry e = entries[i]; 428 boolean drawingForm = e.form != Legend.LegendForm.NONE; 429 float formSize = Float.isNaN(e.formSize) ? defaultFormSize : Utils.convertDpToPixel(e.formSize); 430 431 float posX = originPosX; 432 433 if (drawingForm) { 434 if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) 435 posX += stack; 436 else 437 posX -= formSize - stack; 438 439 drawForm(c, posX, posY + formYOffset, e, mLegend); 440 441 if (direction == Legend.LegendDirection.LEFT_TO_RIGHT) 442 posX += formSize; 443 } 444 445 if (e.label != null) { 446 447 if (drawingForm && !wasStacked) 448 posX += direction == Legend.LegendDirection.LEFT_TO_RIGHT ? formToTextSpace 449 : -formToTextSpace; 450 else if (wasStacked) 451 posX = originPosX; 452 453 if (direction == Legend.LegendDirection.RIGHT_TO_LEFT) 454 posX -= Utils.calcTextWidth(mLegendLabelPaint, e.label); 455 456 if (!wasStacked) { 457 drawLabel(c, posX, posY + labelLineHeight, e.label); 458 } else { 459 posY += labelLineHeight + labelLineSpacing; 460 drawLabel(c, posX, posY + labelLineHeight, e.label); 461 } 462 463 // make a step down 464 posY += labelLineHeight + labelLineSpacing; 465 stack = 0f; 466 } else { 467 stack += formSize + stackSpace; 468 wasStacked = true; 469 } 470 } 471 472 break; 473 474 } 475 } 476 } 477 478 private Path mLineFormPath = new Path(); 479 480 /** 481 * Draws the Legend-form at the given position with the color at the given 482 * index. 483 * 484 * @param c canvas to draw with 485 * @param x position 486 * @param y position 487 * @param entry the entry to render 488 * @param legend the legend context 489 */ 490 protected void drawForm( 491 Canvas c, 492 float x, float y, 493 LegendEntry entry, 494 Legend legend) { 495 496 if (entry.formColor == ColorTemplate.COLOR_SKIP || 497 entry.formColor == ColorTemplate.COLOR_NONE || 498 entry.formColor == 0) 499 return; 500 501 int restoreCount = c.save(); 502 503 Legend.LegendForm form = entry.form; 504 if (form == Legend.LegendForm.DEFAULT) 505 form = legend.getForm(); 506 507 mLegendFormPaint.setColor(entry.formColor); 508 509 final float formSize = Utils.convertDpToPixel( 510 Float.isNaN(entry.formSize) 511 ? legend.getFormSize() 512 : entry.formSize); 513 final float half = formSize / 2f; 514 515 switch (form) { 516 case NONE: 517 // Do nothing 518 break; 519 520 case EMPTY: 521 // Do not draw, but keep space for the form 522 break; 523 524 case DEFAULT: 525 case CIRCLE: 526 mLegendFormPaint.setStyle(Paint.Style.FILL); 527 c.drawCircle(x + half, y, half, mLegendFormPaint); 528 break; 529 530 case SQUARE: 531 mLegendFormPaint.setStyle(Paint.Style.FILL); 532 c.drawRect(x, y - half, x + formSize, y + half, mLegendFormPaint); 533 break; 534 535 case LINE: 536 { 537 final float formLineWidth = Utils.convertDpToPixel( 538 Float.isNaN(entry.formLineWidth) 539 ? legend.getFormLineWidth() 540 : entry.formLineWidth); 541 final DashPathEffect formLineDashEffect = entry.formLineDashEffect == null 542 ? legend.getFormLineDashEffect() 543 : entry.formLineDashEffect; 544 mLegendFormPaint.setStyle(Paint.Style.STROKE); 545 mLegendFormPaint.setStrokeWidth(formLineWidth); 546 mLegendFormPaint.setPathEffect(formLineDashEffect); 547 548 mLineFormPath.reset(); 549 mLineFormPath.moveTo(x, y); 550 mLineFormPath.lineTo(x + formSize, y); 551 c.drawPath(mLineFormPath, mLegendFormPaint); 552 } 553 break; 554 } 555 556 c.restoreToCount(restoreCount); 557 } 558 559 /** 560 * Draws the provided label at the given position. 561 * 562 * @param c canvas to draw with 563 * @param x 564 * @param y 565 * @param label the label to draw 566 */ 567 protected void drawLabel(Canvas c, float x, float y, String label) { 568 c.drawText(label, x, y, mLegendLabelPaint); 569 } 570 } 571