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