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