001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2021, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ---------------- 028 * LegendTitle.java 029 * ---------------- 030 * (C) Copyright 2002-2021, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Pierre-Marie Le Biot; 034 * 035 */ 036 037package org.jfree.chart.title; 038 039import java.awt.Color; 040import java.awt.Font; 041import java.awt.Graphics2D; 042import java.awt.Paint; 043import java.awt.geom.Rectangle2D; 044import java.io.IOException; 045import java.io.ObjectInputStream; 046import java.io.ObjectOutputStream; 047import java.io.Serializable; 048 049import org.jfree.chart.LegendItem; 050import org.jfree.chart.LegendItemCollection; 051import org.jfree.chart.LegendItemSource; 052import org.jfree.chart.block.Arrangement; 053import org.jfree.chart.block.Block; 054import org.jfree.chart.block.BlockContainer; 055import org.jfree.chart.block.BlockFrame; 056import org.jfree.chart.block.BlockResult; 057import org.jfree.chart.block.BorderArrangement; 058import org.jfree.chart.block.CenterArrangement; 059import org.jfree.chart.block.ColumnArrangement; 060import org.jfree.chart.block.EntityBlockParams; 061import org.jfree.chart.block.FlowArrangement; 062import org.jfree.chart.block.LabelBlock; 063import org.jfree.chart.block.RectangleConstraint; 064import org.jfree.chart.entity.EntityCollection; 065import org.jfree.chart.entity.StandardEntityCollection; 066import org.jfree.chart.entity.TitleEntity; 067import org.jfree.chart.event.TitleChangeEvent; 068import org.jfree.chart.ui.RectangleAnchor; 069import org.jfree.chart.ui.RectangleEdge; 070import org.jfree.chart.util.PublicCloneable; 071import org.jfree.chart.util.SerialUtils; 072import org.jfree.chart.ui.RectangleInsets; 073import org.jfree.chart.ui.Size2D; 074import org.jfree.chart.util.PaintUtils; 075import org.jfree.chart.util.Args; 076import org.jfree.chart.util.SortOrder; 077 078 079/** 080 * A chart title that displays a legend for the data in the chart. 081 * <P> 082 * The title can be populated with legend items manually, or you can assign a 083 * reference to the plot, in which case the legend items will be automatically 084 * created to match the dataset(s). 085 */ 086public class LegendTitle extends Title 087 implements Cloneable, PublicCloneable, Serializable { 088 089 /** For serialization. */ 090 private static final long serialVersionUID = 2644010518533854633L; 091 092 /** The default item font. */ 093 public static final Font DEFAULT_ITEM_FONT 094 = new Font("SansSerif", Font.PLAIN, 12); 095 096 /** The default item paint. */ 097 public static final Paint DEFAULT_ITEM_PAINT = Color.BLACK; 098 099 /** The sources for legend items. */ 100 private LegendItemSource[] sources; 101 102 /** The background paint (possibly {@code null}). */ 103 private transient Paint backgroundPaint; 104 105 /** The edge for the legend item graphic relative to the text. */ 106 private RectangleEdge legendItemGraphicEdge; 107 108 /** The anchor point for the legend item graphic. */ 109 private RectangleAnchor legendItemGraphicAnchor; 110 111 /** The legend item graphic location. */ 112 private RectangleAnchor legendItemGraphicLocation; 113 114 /** The padding for the legend item graphic. */ 115 private RectangleInsets legendItemGraphicPadding; 116 117 /** The item font. */ 118 private Font itemFont; 119 120 /** The item paint. */ 121 private transient Paint itemPaint; 122 123 /** The padding for the item labels. */ 124 private RectangleInsets itemLabelPadding; 125 126 /** 127 * A container that holds and displays the legend items. 128 */ 129 private BlockContainer items; 130 131 /** 132 * The layout for the legend when it is positioned at the top or bottom 133 * of the chart. 134 */ 135 private Arrangement hLayout; 136 137 /** 138 * The layout for the legend when it is positioned at the left or right 139 * of the chart. 140 */ 141 private Arrangement vLayout; 142 143 /** 144 * An optional container for wrapping the legend items (allows for adding 145 * a title or other text to the legend). 146 */ 147 private BlockContainer wrapper; 148 149 /** 150 * Whether to render legend items in ascending or descending order. 151 */ 152 private SortOrder sortOrder; 153 154 /** 155 * Constructs a new (empty) legend for the specified source. 156 * 157 * @param source the source. 158 */ 159 public LegendTitle(LegendItemSource source) { 160 this(source, new FlowArrangement(), new ColumnArrangement()); 161 } 162 163 /** 164 * Creates a new legend title with the specified arrangement. 165 * 166 * @param source the source. 167 * @param hLayout the horizontal item arrangement ({@code null} not 168 * permitted). 169 * @param vLayout the vertical item arrangement ({@code null} not 170 * permitted). 171 */ 172 public LegendTitle(LegendItemSource source, 173 Arrangement hLayout, Arrangement vLayout) { 174 this.sources = new LegendItemSource[] {source}; 175 this.items = new BlockContainer(hLayout); 176 this.hLayout = hLayout; 177 this.vLayout = vLayout; 178 this.backgroundPaint = null; 179 this.legendItemGraphicEdge = RectangleEdge.LEFT; 180 this.legendItemGraphicAnchor = RectangleAnchor.CENTER; 181 this.legendItemGraphicLocation = RectangleAnchor.CENTER; 182 this.legendItemGraphicPadding = new RectangleInsets(2.0, 2.0, 2.0, 2.0); 183 this.itemFont = DEFAULT_ITEM_FONT; 184 this.itemPaint = DEFAULT_ITEM_PAINT; 185 this.itemLabelPadding = new RectangleInsets(2.0, 2.0, 2.0, 2.0); 186 this.sortOrder = SortOrder.ASCENDING; 187 } 188 189 /** 190 * Returns the legend item sources. 191 * 192 * @return The sources. 193 */ 194 public LegendItemSource[] getSources() { 195 return this.sources; 196 } 197 198 /** 199 * Sets the legend item sources and sends a {@link TitleChangeEvent} to 200 * all registered listeners. 201 * 202 * @param sources the sources ({@code null} not permitted). 203 */ 204 public void setSources(LegendItemSource[] sources) { 205 Args.nullNotPermitted(sources, "sources"); 206 this.sources = sources; 207 notifyListeners(new TitleChangeEvent(this)); 208 } 209 210 /** 211 * Returns the background paint. 212 * 213 * @return The background paint (possibly {@code null}). 214 */ 215 public Paint getBackgroundPaint() { 216 return this.backgroundPaint; 217 } 218 219 /** 220 * Sets the background paint for the legend and sends a 221 * {@link TitleChangeEvent} to all registered listeners. 222 * 223 * @param paint the paint ({@code null} permitted). 224 */ 225 public void setBackgroundPaint(Paint paint) { 226 this.backgroundPaint = paint; 227 notifyListeners(new TitleChangeEvent(this)); 228 } 229 230 /** 231 * Returns the location of the shape within each legend item. 232 * 233 * @return The location (never {@code null}). 234 */ 235 public RectangleEdge getLegendItemGraphicEdge() { 236 return this.legendItemGraphicEdge; 237 } 238 239 /** 240 * Sets the location of the shape within each legend item. 241 * 242 * @param edge the edge ({@code null} not permitted). 243 */ 244 public void setLegendItemGraphicEdge(RectangleEdge edge) { 245 Args.nullNotPermitted(edge, "edge"); 246 this.legendItemGraphicEdge = edge; 247 notifyListeners(new TitleChangeEvent(this)); 248 } 249 250 /** 251 * Returns the legend item graphic anchor. 252 * 253 * @return The graphic anchor (never {@code null}). 254 */ 255 public RectangleAnchor getLegendItemGraphicAnchor() { 256 return this.legendItemGraphicAnchor; 257 } 258 259 /** 260 * Sets the anchor point used for the graphic in each legend item. 261 * 262 * @param anchor the anchor point ({@code null} not permitted). 263 */ 264 public void setLegendItemGraphicAnchor(RectangleAnchor anchor) { 265 Args.nullNotPermitted(anchor, "anchor"); 266 this.legendItemGraphicAnchor = anchor; 267 } 268 269 /** 270 * Returns the legend item graphic location. 271 * 272 * @return The location (never {@code null}). 273 */ 274 public RectangleAnchor getLegendItemGraphicLocation() { 275 return this.legendItemGraphicLocation; 276 } 277 278 /** 279 * Sets the legend item graphic location. 280 * 281 * @param anchor the anchor ({@code null} not permitted). 282 */ 283 public void setLegendItemGraphicLocation(RectangleAnchor anchor) { 284 this.legendItemGraphicLocation = anchor; 285 } 286 287 /** 288 * Returns the padding that will be applied to each item graphic. 289 * 290 * @return The padding (never {@code null}). 291 */ 292 public RectangleInsets getLegendItemGraphicPadding() { 293 return this.legendItemGraphicPadding; 294 } 295 296 /** 297 * Sets the padding that will be applied to each item graphic in the 298 * legend and sends a {@link TitleChangeEvent} to all registered listeners. 299 * 300 * @param padding the padding ({@code null} not permitted). 301 */ 302 public void setLegendItemGraphicPadding(RectangleInsets padding) { 303 Args.nullNotPermitted(padding, "padding"); 304 this.legendItemGraphicPadding = padding; 305 notifyListeners(new TitleChangeEvent(this)); 306 } 307 308 /** 309 * Returns the item font. 310 * 311 * @return The font (never {@code null}). 312 */ 313 public Font getItemFont() { 314 return this.itemFont; 315 } 316 317 /** 318 * Sets the item font and sends a {@link TitleChangeEvent} to 319 * all registered listeners. 320 * 321 * @param font the font ({@code null} not permitted). 322 */ 323 public void setItemFont(Font font) { 324 Args.nullNotPermitted(font, "font"); 325 this.itemFont = font; 326 notifyListeners(new TitleChangeEvent(this)); 327 } 328 329 /** 330 * Returns the item paint. 331 * 332 * @return The paint (never {@code null}). 333 */ 334 public Paint getItemPaint() { 335 return this.itemPaint; 336 } 337 338 /** 339 * Sets the item paint. 340 * 341 * @param paint the paint ({@code null} not permitted). 342 */ 343 public void setItemPaint(Paint paint) { 344 Args.nullNotPermitted(paint, "paint"); 345 this.itemPaint = paint; 346 notifyListeners(new TitleChangeEvent(this)); 347 } 348 349 /** 350 * Returns the padding used for the items labels. 351 * 352 * @return The padding (never {@code null}). 353 */ 354 public RectangleInsets getItemLabelPadding() { 355 return this.itemLabelPadding; 356 } 357 358 /** 359 * Sets the padding used for the item labels in the legend. 360 * 361 * @param padding the padding ({@code null} not permitted). 362 */ 363 public void setItemLabelPadding(RectangleInsets padding) { 364 Args.nullNotPermitted(padding, "padding"); 365 this.itemLabelPadding = padding; 366 notifyListeners(new TitleChangeEvent(this)); 367 } 368 369 /** 370 * Gets the order used to display legend items. 371 * 372 * @return The order (never {@code null}). 373 */ 374 public SortOrder getSortOrder() { 375 return this.sortOrder; 376 } 377 378 /** 379 * Sets the order used to display legend items. 380 * 381 * @param order Specifies ascending or descending order ({@code null} 382 * not permitted). 383 */ 384 public void setSortOrder(SortOrder order) { 385 Args.nullNotPermitted(order, "order"); 386 this.sortOrder = order; 387 notifyListeners(new TitleChangeEvent(this)); 388 } 389 390 /** 391 * Fetches the latest legend items. 392 */ 393 protected void fetchLegendItems() { 394 this.items.clear(); 395 RectangleEdge p = getPosition(); 396 if (RectangleEdge.isTopOrBottom(p)) { 397 this.items.setArrangement(this.hLayout); 398 } 399 else { 400 this.items.setArrangement(this.vLayout); 401 } 402 403 if (this.sortOrder.equals(SortOrder.ASCENDING)) { 404 for (int s = 0; s < this.sources.length; s++) { 405 LegendItemCollection legendItems = 406 this.sources[s].getLegendItems(); 407 if (legendItems != null) { 408 for (int i = 0; i < legendItems.getItemCount(); i++) { 409 addItemBlock(legendItems.get(i)); 410 } 411 } 412 } 413 } 414 else { 415 for (int s = this.sources.length - 1; s >= 0; s--) { 416 LegendItemCollection legendItems = 417 this.sources[s].getLegendItems(); 418 if (legendItems != null) { 419 for (int i = legendItems.getItemCount()-1; i >= 0; i--) { 420 addItemBlock(legendItems.get(i)); 421 } 422 } 423 } 424 } 425 } 426 427 private void addItemBlock(LegendItem item) { 428 Block block = createLegendItemBlock(item); 429 this.items.add(block); 430 } 431 432 /** 433 * Creates a legend item block. 434 * 435 * @param item the legend item. 436 * 437 * @return The block. 438 */ 439 protected Block createLegendItemBlock(LegendItem item) { 440 BlockContainer result; 441 LegendGraphic lg = new LegendGraphic(item.getShape(), 442 item.getFillPaint()); 443 lg.setFillPaintTransformer(item.getFillPaintTransformer()); 444 lg.setShapeFilled(item.isShapeFilled()); 445 lg.setLine(item.getLine()); 446 lg.setLineStroke(item.getLineStroke()); 447 lg.setLinePaint(item.getLinePaint()); 448 lg.setLineVisible(item.isLineVisible()); 449 lg.setShapeVisible(item.isShapeVisible()); 450 lg.setShapeOutlineVisible(item.isShapeOutlineVisible()); 451 lg.setOutlinePaint(item.getOutlinePaint()); 452 lg.setOutlineStroke(item.getOutlineStroke()); 453 lg.setPadding(this.legendItemGraphicPadding); 454 455 LegendItemBlockContainer legendItem = new LegendItemBlockContainer( 456 new BorderArrangement(), item.getDataset(), 457 item.getSeriesKey()); 458 lg.setShapeAnchor(getLegendItemGraphicAnchor()); 459 lg.setShapeLocation(getLegendItemGraphicLocation()); 460 legendItem.add(lg, this.legendItemGraphicEdge); 461 Font textFont = item.getLabelFont(); 462 if (textFont == null) { 463 textFont = this.itemFont; 464 } 465 Paint textPaint = item.getLabelPaint(); 466 if (textPaint == null) { 467 textPaint = this.itemPaint; 468 } 469 LabelBlock labelBlock = new LabelBlock(item.getLabel(), textFont, 470 textPaint); 471 labelBlock.setPadding(this.itemLabelPadding); 472 legendItem.add(labelBlock); 473 legendItem.setToolTipText(item.getToolTipText()); 474 legendItem.setURLText(item.getURLText()); 475 476 result = new BlockContainer(new CenterArrangement()); 477 result.add(legendItem); 478 479 return result; 480 } 481 482 /** 483 * Returns the container that holds the legend items. 484 * 485 * @return The container for the legend items. 486 */ 487 public BlockContainer getItemContainer() { 488 return this.items; 489 } 490 491 /** 492 * Arranges the contents of the block, within the given constraints, and 493 * returns the block size. 494 * 495 * @param g2 the graphics device. 496 * @param constraint the constraint ({@code null} not permitted). 497 * 498 * @return The block size (in Java2D units, never {@code null}). 499 */ 500 @Override 501 public Size2D arrange(Graphics2D g2, RectangleConstraint constraint) { 502 Size2D result = new Size2D(); 503 fetchLegendItems(); 504 if (this.items.isEmpty()) { 505 return result; 506 } 507 BlockContainer container = this.wrapper; 508 if (container == null) { 509 container = this.items; 510 } 511 RectangleConstraint c = toContentConstraint(constraint); 512 Size2D size = container.arrange(g2, c); 513 result.height = calculateTotalHeight(size.height); 514 result.width = calculateTotalWidth(size.width); 515 return result; 516 } 517 518 /** 519 * Draws the title on a Java 2D graphics device (such as the screen or a 520 * printer). 521 * 522 * @param g2 the graphics device. 523 * @param area the available area for the title. 524 */ 525 @Override 526 public void draw(Graphics2D g2, Rectangle2D area) { 527 draw(g2, area, null); 528 } 529 530 /** 531 * Draws the block within the specified area. 532 * 533 * @param g2 the graphics device. 534 * @param area the area. 535 * @param params ignored ({@code null} permitted). 536 * 537 * @return An {@link org.jfree.chart.block.EntityBlockResult} or 538 * {@code null}. 539 */ 540 @Override 541 public Object draw(Graphics2D g2, Rectangle2D area, Object params) { 542 Rectangle2D target = (Rectangle2D) area.clone(); 543 Rectangle2D hotspot = (Rectangle2D) area.clone(); 544 StandardEntityCollection sec = null; 545 if (params instanceof EntityBlockParams 546 && ((EntityBlockParams) params).getGenerateEntities()) { 547 sec = new StandardEntityCollection(); 548 sec.add(new TitleEntity(hotspot, this)); 549 } 550 target = trimMargin(target); 551 if (this.backgroundPaint != null) { 552 g2.setPaint(this.backgroundPaint); 553 g2.fill(target); 554 } 555 BlockFrame border = getFrame(); 556 border.draw(g2, target); 557 border.getInsets().trim(target); 558 BlockContainer container = this.wrapper; 559 if (container == null) { 560 container = this.items; 561 } 562 target = trimPadding(target); 563 Object val = container.draw(g2, target, params); 564 if (val instanceof BlockResult) { 565 EntityCollection ec = ((BlockResult) val).getEntityCollection(); 566 if (ec != null && sec != null) { 567 sec.addAll(ec); 568 ((BlockResult) val).setEntityCollection(sec); 569 } 570 } 571 return val; 572 } 573 574 /** 575 * Returns the wrapper container, if any. 576 * 577 * @return The wrapper container (possibly {@code null}). 578 */ 579 public BlockContainer getWrapper() { 580 return this.wrapper; 581 } 582 583 /** 584 * Sets the wrapper container for the legend. 585 * 586 * @param wrapper the wrapper container. 587 */ 588 public void setWrapper(BlockContainer wrapper) { 589 this.wrapper = wrapper; 590 } 591 592 /** 593 * Tests this title for equality with an arbitrary object. 594 * 595 * @param obj the object ({@code null} permitted). 596 * 597 * @return A boolean. 598 */ 599 @Override 600 public boolean equals(Object obj) { 601 if (obj == this) { 602 return true; 603 } 604 if (!(obj instanceof LegendTitle)) { 605 return false; 606 } 607 if (!super.equals(obj)) { 608 return false; 609 } 610 LegendTitle that = (LegendTitle) obj; 611 if (!PaintUtils.equal(this.backgroundPaint, that.backgroundPaint)) { 612 return false; 613 } 614 if (this.legendItemGraphicEdge != that.legendItemGraphicEdge) { 615 return false; 616 } 617 if (this.legendItemGraphicAnchor != that.legendItemGraphicAnchor) { 618 return false; 619 } 620 if (this.legendItemGraphicLocation != that.legendItemGraphicLocation) { 621 return false; 622 } 623 if (!this.itemFont.equals(that.itemFont)) { 624 return false; 625 } 626 if (!this.itemPaint.equals(that.itemPaint)) { 627 return false; 628 } 629 if (!this.hLayout.equals(that.hLayout)) { 630 return false; 631 } 632 if (!this.vLayout.equals(that.vLayout)) { 633 return false; 634 } 635 if (!this.sortOrder.equals(that.sortOrder)) { 636 return false; 637 } 638 return true; 639 } 640 641 /** 642 * Provides serialization support. 643 * 644 * @param stream the output stream. 645 * 646 * @throws IOException if there is an I/O error. 647 */ 648 private void writeObject(ObjectOutputStream stream) throws IOException { 649 stream.defaultWriteObject(); 650 SerialUtils.writePaint(this.backgroundPaint, stream); 651 SerialUtils.writePaint(this.itemPaint, stream); 652 } 653 654 /** 655 * Provides serialization support. 656 * 657 * @param stream the input stream. 658 * 659 * @throws IOException if there is an I/O error. 660 * @throws ClassNotFoundException if there is a classpath problem. 661 */ 662 private void readObject(ObjectInputStream stream) 663 throws IOException, ClassNotFoundException { 664 stream.defaultReadObject(); 665 this.backgroundPaint = SerialUtils.readPaint(stream); 666 this.itemPaint = SerialUtils.readPaint(stream); 667 } 668 669}