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