001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, 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 * XYAreaRenderer.java 029 * ------------------- 030 * (C) Copyright 2002-2021, by Hari and Contributors. 031 * 032 * Original Author: Hari (ourhari@hotmail.com); 033 * Contributor(s): David Gilbert; 034 * Richard Atkinson; 035 * Christian W. Zuckschwerdt; 036 * Martin Krauskopf; 037 * Ulrich Voigt (patch #312); 038 */ 039 040package org.jfree.chart.renderer.xy; 041 042import java.awt.BasicStroke; 043import java.awt.GradientPaint; 044import java.awt.Graphics2D; 045import java.awt.Paint; 046import java.awt.Shape; 047import java.awt.Stroke; 048import java.awt.geom.Area; 049import java.awt.geom.GeneralPath; 050import java.awt.geom.Line2D; 051import java.awt.geom.Rectangle2D; 052import java.io.IOException; 053import java.io.ObjectInputStream; 054import java.io.ObjectOutputStream; 055 056import org.jfree.chart.internal.HashUtils; 057import org.jfree.chart.legend.LegendItem; 058import org.jfree.chart.axis.ValueAxis; 059import org.jfree.chart.entity.EntityCollection; 060import org.jfree.chart.event.RendererChangeEvent; 061import org.jfree.chart.labels.XYSeriesLabelGenerator; 062import org.jfree.chart.labels.XYToolTipGenerator; 063import org.jfree.chart.plot.CrosshairState; 064import org.jfree.chart.plot.PlotOrientation; 065import org.jfree.chart.plot.PlotRenderingInfo; 066import org.jfree.chart.plot.XYPlot; 067import org.jfree.chart.util.GradientPaintTransformer; 068import org.jfree.chart.util.StandardGradientPaintTransformer; 069import org.jfree.chart.urls.XYURLGenerator; 070import org.jfree.chart.internal.Args; 071import org.jfree.chart.api.PublicCloneable; 072import org.jfree.chart.internal.CloneUtils; 073import org.jfree.chart.internal.SerialUtils; 074import org.jfree.chart.internal.ShapeUtils; 075import org.jfree.data.xy.XYDataset; 076 077/** 078 * Area item renderer for an {@link XYPlot}. This class can draw (a) shapes at 079 * each point, or (b) lines between points, or (c) both shapes and lines, 080 * or (d) filled areas, or (e) filled areas and shapes. The example shown here 081 * is generated by the {@code XYAreaRendererDemo1.java} program included 082 * in the JFreeChart demo collection: 083 * <br><br> 084 * <img src="doc-files/XYAreaRendererSample.png" alt="XYAreaRendererSample.png"> 085 */ 086public class XYAreaRenderer extends AbstractXYItemRenderer 087 implements XYItemRenderer, PublicCloneable { 088 089 /** For serialization. */ 090 private static final long serialVersionUID = -4481971353973876747L; 091 092 /** 093 * A state object used by this renderer. 094 */ 095 static class XYAreaRendererState extends XYItemRendererState { 096 097 /** Working storage for the area under one series. */ 098 public GeneralPath area; 099 100 /** Working line that can be recycled. */ 101 public Line2D line; 102 103 /** 104 * Creates a new state. 105 * 106 * @param info the plot rendering info. 107 */ 108 public XYAreaRendererState(PlotRenderingInfo info) { 109 super(info); 110 this.area = new GeneralPath(); 111 this.line = new Line2D.Double(); 112 } 113 114 } 115 116 /** Useful constant for specifying the type of rendering (shapes only). */ 117 public static final int SHAPES = 1; 118 119 /** Useful constant for specifying the type of rendering (lines only). */ 120 public static final int LINES = 2; 121 122 /** 123 * Useful constant for specifying the type of rendering (shapes and lines). 124 */ 125 public static final int SHAPES_AND_LINES = 3; 126 127 /** Useful constant for specifying the type of rendering (area only). */ 128 public static final int AREA = 4; 129 130 /** 131 * Useful constant for specifying the type of rendering (area and shapes). 132 */ 133 public static final int AREA_AND_SHAPES = 5; 134 135 /** A flag indicating whether or not shapes are drawn at each XY point. */ 136 private boolean plotShapes; 137 138 /** A flag indicating whether or not lines are drawn between XY points. */ 139 private boolean plotLines; 140 141 /** A flag indicating whether or not Area are drawn at each XY point. */ 142 private boolean plotArea; 143 144 /** A flag that controls whether or not the outline is shown. */ 145 private boolean showOutline; 146 147 /** 148 * The shape used to represent an area in each legend item (this should 149 * never be {@code null}). 150 */ 151 private transient Shape legendArea; 152 153 /** 154 * A flag that can be set to specify that the fill paint should be used 155 * to fill the area under the renderer. 156 */ 157 private boolean useFillPaint; 158 159 /** 160 * A transformer that is applied to the paint used to fill under the 161 * area *if* it is an instance of GradientPaint. 162 */ 163 private GradientPaintTransformer gradientTransformer; 164 165 /** 166 * Constructs a new renderer. 167 */ 168 public XYAreaRenderer() { 169 this(AREA); 170 } 171 172 /** 173 * Constructs a new renderer. 174 * 175 * @param type the type of the renderer. 176 */ 177 public XYAreaRenderer(int type) { 178 this(type, null, null); 179 } 180 181 /** 182 * Constructs a new renderer. To specify the type of renderer, use one of 183 * the constants: {@code SHAPES}, {@code LINES}, {@code SHAPES_AND_LINES}, 184 * {@code AREA} or {@code AREA_AND_SHAPES}. 185 * 186 * @param type the type of renderer. 187 * @param toolTipGenerator the tool tip generator ({@code null} permitted). 188 * @param urlGenerator the URL generator ({@code null} permitted). 189 */ 190 public XYAreaRenderer(int type, XYToolTipGenerator toolTipGenerator, 191 XYURLGenerator urlGenerator) { 192 193 super(); 194 setDefaultToolTipGenerator(toolTipGenerator); 195 setURLGenerator(urlGenerator); 196 197 if (type == SHAPES) { 198 this.plotShapes = true; 199 } 200 if (type == LINES) { 201 this.plotLines = true; 202 } 203 if (type == SHAPES_AND_LINES) { 204 this.plotShapes = true; 205 this.plotLines = true; 206 } 207 if (type == AREA) { 208 this.plotArea = true; 209 } 210 if (type == AREA_AND_SHAPES) { 211 this.plotArea = true; 212 this.plotShapes = true; 213 } 214 this.showOutline = false; 215 GeneralPath area = new GeneralPath(); 216 area.moveTo(0.0f, -4.0f); 217 area.lineTo(3.0f, -2.0f); 218 area.lineTo(4.0f, 4.0f); 219 area.lineTo(-4.0f, 4.0f); 220 area.lineTo(-3.0f, -2.0f); 221 area.closePath(); 222 this.legendArea = area; 223 this.useFillPaint = false; 224 this.gradientTransformer = new StandardGradientPaintTransformer(); 225 } 226 227 /** 228 * Returns true if shapes are being plotted by the renderer. 229 * 230 * @return {@code true} if shapes are being plotted by the renderer. 231 */ 232 public boolean getPlotShapes() { 233 return this.plotShapes; 234 } 235 236 /** 237 * Returns true if lines are being plotted by the renderer. 238 * 239 * @return {@code true} if lines are being plotted by the renderer. 240 */ 241 public boolean getPlotLines() { 242 return this.plotLines; 243 } 244 245 /** 246 * Returns true if Area is being plotted by the renderer. 247 * 248 * @return {@code true} if Area is being plotted by the renderer. 249 */ 250 public boolean getPlotArea() { 251 return this.plotArea; 252 } 253 254 /** 255 * Returns a flag that controls whether or not outlines of the areas are 256 * drawn. 257 * 258 * @return The flag. 259 * 260 * @see #setOutline(boolean) 261 */ 262 public boolean isOutline() { 263 return this.showOutline; 264 } 265 266 /** 267 * Sets a flag that controls whether or not outlines of the areas are drawn 268 * and sends a {@link RendererChangeEvent} to all registered listeners. 269 * 270 * @param show the flag. 271 * 272 * @see #isOutline() 273 */ 274 public void setOutline(boolean show) { 275 this.showOutline = show; 276 fireChangeEvent(); 277 } 278 279 /** 280 * Returns the shape used to represent an area in the legend. 281 * 282 * @return The legend area (never {@code null}). 283 */ 284 public Shape getLegendArea() { 285 return this.legendArea; 286 } 287 288 /** 289 * Sets the shape used as an area in each legend item and sends a 290 * {@link RendererChangeEvent} to all registered listeners. 291 * 292 * @param area the area ({@code null} not permitted). 293 */ 294 public void setLegendArea(Shape area) { 295 Args.nullNotPermitted(area, "area"); 296 this.legendArea = area; 297 fireChangeEvent(); 298 } 299 300 /** 301 * Returns the flag that controls whether the series fill paint is used to 302 * fill the area under the line. 303 * 304 * @return A boolean. 305 */ 306 public boolean getUseFillPaint() { 307 return this.useFillPaint; 308 } 309 310 /** 311 * Sets the flag that controls whether or not the series fill paint is 312 * used to fill the area under the line and sends a 313 * {@link RendererChangeEvent} to all listeners. 314 * 315 * @param use the new flag value. 316 */ 317 public void setUseFillPaint(boolean use) { 318 this.useFillPaint = use; 319 fireChangeEvent(); 320 } 321 322 /** 323 * Returns the gradient paint transformer. 324 * 325 * @return The gradient paint transformer (never {@code null}). 326 */ 327 public GradientPaintTransformer getGradientTransformer() { 328 return this.gradientTransformer; 329 } 330 331 /** 332 * Sets the gradient paint transformer and sends a 333 * {@link RendererChangeEvent} to all registered listeners. 334 * 335 * @param transformer the transformer ({@code null} not permitted). 336 */ 337 public void setGradientTransformer(GradientPaintTransformer transformer) { 338 Args.nullNotPermitted(transformer, "transformer"); 339 this.gradientTransformer = transformer; 340 fireChangeEvent(); 341 } 342 343 /** 344 * Initialises the renderer and returns a state object that should be 345 * passed to all subsequent calls to the drawItem() method. 346 * 347 * @param g2 the graphics device. 348 * @param dataArea the area inside the axes. 349 * @param plot the plot. 350 * @param data the data. 351 * @param info an optional info collection object to return data back to 352 * the caller. 353 * 354 * @return A state object for use by the renderer. 355 */ 356 @Override 357 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 358 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 359 XYAreaRendererState state = new XYAreaRendererState(info); 360 361 // in the rendering process, there is special handling for item 362 // zero, so we can't support processing of visible data items only 363 state.setProcessVisibleItemsOnly(false); 364 return state; 365 } 366 367 /** 368 * Returns a default legend item for the specified series. Subclasses 369 * should override this method to generate customised items. 370 * 371 * @param datasetIndex the dataset index (zero-based). 372 * @param series the series index (zero-based). 373 * 374 * @return A legend item for the series. 375 */ 376 @Override 377 public LegendItem getLegendItem(int datasetIndex, int series) { 378 LegendItem result = null; 379 XYPlot xyplot = getPlot(); 380 if (xyplot != null) { 381 XYDataset dataset = xyplot.getDataset(datasetIndex); 382 if (dataset != null) { 383 XYSeriesLabelGenerator lg = getLegendItemLabelGenerator(); 384 String label = lg.generateLabel(dataset, series); 385 String description = label; 386 String toolTipText = null; 387 if (getLegendItemToolTipGenerator() != null) { 388 toolTipText = getLegendItemToolTipGenerator().generateLabel( 389 dataset, series); 390 } 391 String urlText = null; 392 if (getLegendItemURLGenerator() != null) { 393 urlText = getLegendItemURLGenerator().generateLabel( 394 dataset, series); 395 } 396 Paint paint = lookupSeriesPaint(series); 397 result = new LegendItem(label, description, toolTipText, 398 urlText, this.legendArea, paint); 399 result.setLabelFont(lookupLegendTextFont(series)); 400 Paint labelPaint = lookupLegendTextPaint(series); 401 if (labelPaint != null) { 402 result.setLabelPaint(labelPaint); 403 } 404 result.setDataset(dataset); 405 result.setDatasetIndex(datasetIndex); 406 result.setSeriesKey(dataset.getSeriesKey(series)); 407 result.setSeriesIndex(series); 408 } 409 } 410 return result; 411 } 412 413 /** 414 * Draws the visual representation of a single data item. 415 * 416 * @param g2 the graphics device. 417 * @param state the renderer state. 418 * @param dataArea the area within which the data is being drawn. 419 * @param info collects information about the drawing. 420 * @param plot the plot (can be used to obtain standard color information 421 * etc). 422 * @param domainAxis the domain axis. 423 * @param rangeAxis the range axis. 424 * @param dataset the dataset. 425 * @param series the series index (zero-based). 426 * @param item the item index (zero-based). 427 * @param crosshairState crosshair information for the plot 428 * ({@code null} permitted). 429 * @param pass the pass index. 430 */ 431 @Override 432 public void drawItem(Graphics2D g2, XYItemRendererState state, 433 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 434 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 435 int series, int item, CrosshairState crosshairState, int pass) { 436 437 if (!getItemVisible(series, item)) { 438 return; 439 } 440 XYAreaRendererState areaState = (XYAreaRendererState) state; 441 442 // get the data point... 443 double x1 = dataset.getXValue(series, item); 444 double y1 = dataset.getYValue(series, item); 445 if (Double.isNaN(y1)) { 446 y1 = 0.0; 447 } 448 double transX1 = domainAxis.valueToJava2D(x1, dataArea, 449 plot.getDomainAxisEdge()); 450 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, 451 plot.getRangeAxisEdge()); 452 453 // get the previous point and the next point so we can calculate a 454 // "hot spot" for the area (used by the chart entity)... 455 int itemCount = dataset.getItemCount(series); 456 double x0 = dataset.getXValue(series, Math.max(item - 1, 0)); 457 double y0 = dataset.getYValue(series, Math.max(item - 1, 0)); 458 if (Double.isNaN(y0)) { 459 y0 = 0.0; 460 } 461 double transX0 = domainAxis.valueToJava2D(x0, dataArea, 462 plot.getDomainAxisEdge()); 463 double transY0 = rangeAxis.valueToJava2D(y0, dataArea, 464 plot.getRangeAxisEdge()); 465 466 double x2 = dataset.getXValue(series, Math.min(item + 1, 467 itemCount - 1)); 468 double y2 = dataset.getYValue(series, Math.min(item + 1, 469 itemCount - 1)); 470 if (Double.isNaN(y2)) { 471 y2 = 0.0; 472 } 473 double transX2 = domainAxis.valueToJava2D(x2, dataArea, 474 plot.getDomainAxisEdge()); 475 double transY2 = rangeAxis.valueToJava2D(y2, dataArea, 476 plot.getRangeAxisEdge()); 477 478 double transZero = rangeAxis.valueToJava2D(0.0, dataArea, 479 plot.getRangeAxisEdge()); 480 481 if (item == 0) { // create a new area polygon for the series 482 areaState.area = new GeneralPath(); 483 // the first point is (x, 0) 484 double zero = rangeAxis.valueToJava2D(0.0, dataArea, 485 plot.getRangeAxisEdge()); 486 if (plot.getOrientation().isVertical()) { 487 moveTo(areaState.area, transX1, zero); 488 } else if (plot.getOrientation().isHorizontal()) { 489 moveTo(areaState.area, zero, transX1); 490 } 491 } 492 493 // Add each point to Area (x, y) 494 if (plot.getOrientation().isVertical()) { 495 lineTo(areaState.area, transX1, transY1); 496 } else if (plot.getOrientation().isHorizontal()) { 497 lineTo(areaState.area, transY1, transX1); 498 } 499 500 PlotOrientation orientation = plot.getOrientation(); 501 Paint paint = getItemPaint(series, item); 502 Stroke stroke = getItemStroke(series, item); 503 g2.setPaint(paint); 504 g2.setStroke(stroke); 505 506 Shape shape; 507 if (getPlotShapes()) { 508 shape = getItemShape(series, item); 509 if (orientation == PlotOrientation.VERTICAL) { 510 shape = ShapeUtils.createTranslatedShape(shape, transX1, 511 transY1); 512 } else if (orientation == PlotOrientation.HORIZONTAL) { 513 shape = ShapeUtils.createTranslatedShape(shape, transY1, 514 transX1); 515 } 516 g2.draw(shape); 517 } 518 519 if (getPlotLines()) { 520 if (item > 0) { 521 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 522 areaState.line.setLine(transX0, transY0, transX1, transY1); 523 } else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 524 areaState.line.setLine(transY0, transX0, transY1, transX1); 525 } 526 g2.draw(areaState.line); 527 } 528 } 529 530 // Check if the item is the last item for the series. 531 // and number of items > 0. We can't draw an area for a single point. 532 if (getPlotArea() && item > 0 && item == (itemCount - 1)) { 533 534 if (orientation == PlotOrientation.VERTICAL) { 535 // Add the last point (x,0) 536 lineTo(areaState.area, transX1, transZero); 537 areaState.area.closePath(); 538 } else if (orientation == PlotOrientation.HORIZONTAL) { 539 // Add the last point (x,0) 540 lineTo(areaState.area, transZero, transX1); 541 areaState.area.closePath(); 542 } 543 544 if (this.useFillPaint) { 545 paint = lookupSeriesFillPaint(series); 546 g2.setPaint(paint); 547 } 548 if (paint instanceof GradientPaint) { 549 GradientPaint gp = (GradientPaint) paint; 550 GradientPaint adjGP = this.gradientTransformer.transform(gp, 551 dataArea); 552 g2.setPaint(adjGP); 553 } 554 g2.fill(areaState.area); 555 556 // draw an outline around the Area. 557 if (isOutline()) { 558 Shape area = areaState.area; 559 560 // Java2D has some issues drawing dashed lines around "large" 561 // geometrical shapes - for example, see bug 6620013 in the 562 // Java bug database. So, we'll check if the outline is 563 // dashed and, if it is, do our own clipping before drawing 564 // the outline... 565 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 566 if (outlineStroke instanceof BasicStroke) { 567 BasicStroke bs = (BasicStroke) outlineStroke; 568 if (bs.getDashArray() != null) { 569 Area poly = new Area(areaState.area); 570 // we make the clip region slightly larger than the 571 // dataArea so that the clipped edges don't show lines 572 // on the chart 573 Area clip = new Area(new Rectangle2D.Double( 574 dataArea.getX() - 5.0, dataArea.getY() - 5.0, 575 dataArea.getWidth() + 10.0, 576 dataArea.getHeight() + 10.0)); 577 poly.intersect(clip); 578 area = poly; 579 } 580 } // end of workaround 581 582 g2.setStroke(outlineStroke); 583 g2.setPaint(lookupSeriesOutlinePaint(series)); 584 g2.draw(area); 585 } 586 } 587 588 int datasetIndex = plot.indexOf(dataset); 589 updateCrosshairValues(crosshairState, x1, y1, datasetIndex, 590 transX1, transY1, orientation); 591 592 // collect entity and tool tip information... 593 EntityCollection entities = state.getEntityCollection(); 594 if (entities != null) { 595 GeneralPath hotspot = new GeneralPath(); 596 if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 597 moveTo(hotspot, transZero, ((transX0 + transX1) / 2.0)); 598 lineTo(hotspot, ((transY0 + transY1) / 2.0), ((transX0 + transX1) / 2.0)); 599 lineTo(hotspot, transY1, transX1); 600 lineTo(hotspot, ((transY1 + transY2) / 2.0), ((transX1 + transX2) / 2.0)); 601 lineTo(hotspot, transZero, ((transX1 + transX2) / 2.0)); 602 } else { // vertical orientation 603 moveTo(hotspot, ((transX0 + transX1) / 2.0), transZero); 604 lineTo(hotspot, ((transX0 + transX1) / 2.0), ((transY0 + transY1) / 2.0)); 605 lineTo(hotspot, transX1, transY1); 606 lineTo(hotspot, ((transX1 + transX2) / 2.0), ((transY1 + transY2) / 2.0)); 607 lineTo(hotspot, ((transX1 + transX2) / 2.0), transZero); 608 } 609 hotspot.closePath(); 610 611 // limit the entity hotspot area to the data area 612 Area dataAreaHotspot = new Area(hotspot); 613 dataAreaHotspot.intersect(new Area(dataArea)); 614 615 if (dataAreaHotspot.isEmpty() == false) { 616 addEntity(entities, dataAreaHotspot, dataset, series, item, 617 0.0, 0.0); 618 } 619 } 620 621 } 622 623 /** 624 * Returns a clone of the renderer. 625 * 626 * @return A clone. 627 * 628 * @throws CloneNotSupportedException if the renderer cannot be cloned. 629 */ 630 @Override 631 public Object clone() throws CloneNotSupportedException { 632 XYAreaRenderer clone = (XYAreaRenderer) super.clone(); 633 clone.legendArea = CloneUtils.clone(this.legendArea); 634 return clone; 635 } 636 637 /** 638 * Tests this renderer for equality with an arbitrary object. 639 * 640 * @param obj the object ({@code null} permitted). 641 * 642 * @return A boolean. 643 */ 644 @Override 645 public boolean equals(Object obj) { 646 if (obj == this) { 647 return true; 648 } 649 if (!(obj instanceof XYAreaRenderer)) { 650 return false; 651 } 652 XYAreaRenderer that = (XYAreaRenderer) obj; 653 if (this.plotArea != that.plotArea) { 654 return false; 655 } 656 if (this.plotLines != that.plotLines) { 657 return false; 658 } 659 if (this.plotShapes != that.plotShapes) { 660 return false; 661 } 662 if (this.showOutline != that.showOutline) { 663 return false; 664 } 665 if (this.useFillPaint != that.useFillPaint) { 666 return false; 667 } 668 if (!this.gradientTransformer.equals(that.gradientTransformer)) { 669 return false; 670 } 671 if (!ShapeUtils.equal(this.legendArea, that.legendArea)) { 672 return false; 673 } 674 return true; 675 } 676 677 /** 678 * Returns a hash code for this instance. 679 * 680 * @return A hash code. 681 */ 682 @Override 683 public int hashCode() { 684 int result = super.hashCode(); 685 result = HashUtils.hashCode(result, this.plotArea); 686 result = HashUtils.hashCode(result, this.plotLines); 687 result = HashUtils.hashCode(result, this.plotShapes); 688 result = HashUtils.hashCode(result, this.useFillPaint); 689 return result; 690 } 691 692 /** 693 * Provides serialization support. 694 * 695 * @param stream the input stream. 696 * 697 * @throws IOException if there is an I/O error. 698 * @throws ClassNotFoundException if there is a classpath problem. 699 */ 700 private void readObject(ObjectInputStream stream) 701 throws IOException, ClassNotFoundException { 702 stream.defaultReadObject(); 703 this.legendArea = SerialUtils.readShape(stream); 704 } 705 706 /** 707 * Provides serialization support. 708 * 709 * @param stream the output stream. 710 * 711 * @throws IOException if there is an I/O error. 712 */ 713 private void writeObject(ObjectOutputStream stream) throws IOException { 714 stream.defaultWriteObject(); 715 SerialUtils.writeShape(this.legendArea, stream); 716 } 717}