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}