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}