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 * SpiderWebPlot.java
029 * ------------------
030 * (C) Copyright 2005-present, by Heaps of Flavour Pty Ltd and Contributors.
031 *
032 * Company Info:  http://www.i4-talent.com
033 *
034 * Original Author:  Don Elliott;
035 * Contributor(s):   David Gilbert;
036 *                   Nina Jeliazkova;
037 *
038 */
039
040package org.jfree.chart.plot;
041
042import java.awt.AlphaComposite;
043import java.awt.BasicStroke;
044import java.awt.Color;
045import java.awt.Composite;
046import java.awt.Font;
047import java.awt.Graphics2D;
048import java.awt.Paint;
049import java.awt.Polygon;
050import java.awt.Rectangle;
051import java.awt.Shape;
052import java.awt.Stroke;
053import java.awt.font.FontRenderContext;
054import java.awt.font.LineMetrics;
055import java.awt.geom.Arc2D;
056import java.awt.geom.Ellipse2D;
057import java.awt.geom.Line2D;
058import java.awt.geom.Point2D;
059import java.awt.geom.Rectangle2D;
060import java.io.IOException;
061import java.io.ObjectInputStream;
062import java.io.ObjectOutputStream;
063import java.io.Serializable;
064import java.util.Iterator;
065import java.util.List;
066import java.util.Objects;
067
068import org.jfree.chart.LegendItem;
069import org.jfree.chart.LegendItemCollection;
070import org.jfree.chart.entity.CategoryItemEntity;
071import org.jfree.chart.entity.EntityCollection;
072import org.jfree.chart.event.PlotChangeEvent;
073import org.jfree.chart.labels.CategoryItemLabelGenerator;
074import org.jfree.chart.labels.CategoryToolTipGenerator;
075import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
076import org.jfree.chart.ui.RectangleInsets;
077import org.jfree.chart.urls.CategoryURLGenerator;
078import org.jfree.chart.util.PaintList;
079import org.jfree.chart.util.PaintUtils;
080import org.jfree.chart.util.Args;
081import org.jfree.chart.util.Rotation;
082import org.jfree.chart.util.SerialUtils;
083import org.jfree.chart.util.ShapeUtils;
084import org.jfree.chart.util.StrokeList;
085import org.jfree.chart.util.TableOrder;
086import org.jfree.data.category.CategoryDataset;
087import org.jfree.data.general.DatasetChangeEvent;
088import org.jfree.data.general.DatasetUtils;
089
090/**
091 * A plot that displays data from a {@link CategoryDataset} in the form of a
092 * "spider web".  Multiple series can be plotted on the same axis to allow
093 * easy comparison.  This plot doesn't support negative values at present.
094 */
095public class SpiderWebPlot extends Plot implements Cloneable, Serializable {
096
097    /** For serialization. */
098    private static final long serialVersionUID = -5376340422031599463L;
099
100    /** The default head radius percent (currently 1%). */
101    public static final double DEFAULT_HEAD = 0.01;
102
103    /** The default axis label gap (currently 10%). */
104    public static final double DEFAULT_AXIS_LABEL_GAP = 0.10;
105
106    /** The default interior gap. */
107    public static final double DEFAULT_INTERIOR_GAP = 0.25;
108
109    /** The maximum interior gap (currently 40%). */
110    public static final double MAX_INTERIOR_GAP = 0.40;
111
112    /** The default starting angle for the radar chart axes. */
113    public static final double DEFAULT_START_ANGLE = 90.0;
114
115    /** The default series label font. */
116    public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif",
117            Font.PLAIN, 10);
118
119    /** The default series label paint. */
120    public static final Paint  DEFAULT_LABEL_PAINT = Color.BLACK;
121
122    /** The default series label background paint. */
123    public static final Paint  DEFAULT_LABEL_BACKGROUND_PAINT
124            = new Color(255, 255, 192);
125
126    /** The default series label outline paint. */
127    public static final Paint  DEFAULT_LABEL_OUTLINE_PAINT = Color.BLACK;
128
129    /** The default series label outline stroke. */
130    public static final Stroke DEFAULT_LABEL_OUTLINE_STROKE
131            = new BasicStroke(0.5f);
132
133    /** The default series label shadow paint. */
134    public static final Paint  DEFAULT_LABEL_SHADOW_PAINT = Color.LIGHT_GRAY;
135
136    /**
137     * The default maximum value plotted - forces the plot to evaluate
138     *  the maximum from the data passed in
139     */
140    public static final double DEFAULT_MAX_VALUE = -1.0;
141
142    /** The head radius as a percentage of the available drawing area. */
143    protected double headPercent;
144
145    /** The space left around the outside of the plot as a percentage. */
146    private double interiorGap;
147
148    /** The gap between the labels and the axes as a %age of the radius. */
149    private double axisLabelGap;
150
151    /**
152     * The paint used to draw the axis lines.
153     */
154    private transient Paint axisLinePaint;
155
156    /**
157     * The stroke used to draw the axis lines.
158     */
159    private transient Stroke axisLineStroke;
160
161    /** The dataset. */
162    private CategoryDataset dataset;
163
164    /** The maximum value we are plotting against on each category axis */
165    private double maxValue;
166
167    /**
168     * The data extract order (BY_ROW or BY_COLUMN). This denotes whether
169     * the data series are stored in rows (in which case the category names are
170     * derived from the column keys) or in columns (in which case the category
171     * names are derived from the row keys).
172     */
173    private TableOrder dataExtractOrder;
174
175    /** The starting angle. */
176    private double startAngle;
177
178    /** The direction for drawing the radar axis and plots. */
179    private Rotation direction;
180
181    /** The legend item shape. */
182    private transient Shape legendItemShape;
183
184    /** The paint for ALL series (overrides list). */
185    private transient Paint seriesPaint;
186
187    /** The series paint list. */
188    private PaintList seriesPaintList;
189
190    /** The base series paint (fallback). */
191    private transient Paint baseSeriesPaint;
192
193    /** The outline paint for ALL series (overrides list). */
194    private transient Paint seriesOutlinePaint;
195
196    /** The series outline paint list. */
197    private PaintList seriesOutlinePaintList;
198
199    /** The base series outline paint (fallback). */
200    private transient Paint baseSeriesOutlinePaint;
201
202    /** The outline stroke for ALL series (overrides list). */
203    private transient Stroke seriesOutlineStroke;
204
205    /** The series outline stroke list. */
206    private StrokeList seriesOutlineStrokeList;
207
208    /** The base series outline stroke (fallback). */
209    private transient Stroke baseSeriesOutlineStroke;
210
211    /** The font used to display the category labels. */
212    private Font labelFont;
213
214    /** The color used to draw the category labels. */
215    private transient Paint labelPaint;
216
217    /** The label generator. */
218    private CategoryItemLabelGenerator labelGenerator;
219
220    /** controls if the web polygons are filled or not */
221    private boolean webFilled = true;
222
223    /** The alpha value of the fill portion of a polygon. */
224    private float webFillAlpha = 0.1F;
225
226    /** A tooltip generator for the plot ({@code null} permitted). */
227    private CategoryToolTipGenerator toolTipGenerator;
228
229    /** A URL generator for the plot ({@code null} permitted). */
230    private CategoryURLGenerator urlGenerator;
231
232    /**
233     * Creates a default plot with no dataset.
234     */
235    public SpiderWebPlot() {
236        this(null);
237    }
238
239    /**
240     * Creates a new spider web plot with the given dataset, with each row
241     * representing a series.
242     *
243     * @param dataset  the dataset ({@code null} permitted).
244     */
245    public SpiderWebPlot(CategoryDataset dataset) {
246        this(dataset, TableOrder.BY_ROW);
247    }
248
249    /**
250     * Creates a new spider web plot with the given dataset.
251     *
252     * @param dataset  the dataset.
253     * @param extract  controls how data is extracted ({@link TableOrder#BY_ROW}
254     *                 or {@link TableOrder#BY_COLUMN}).
255     */
256    public SpiderWebPlot(CategoryDataset dataset, TableOrder extract) {
257        super();
258        Args.nullNotPermitted(extract, "extract");
259        this.dataset = dataset;
260        if (dataset != null) {
261            dataset.addChangeListener(this);
262        }
263
264        this.dataExtractOrder = extract;
265        this.headPercent = DEFAULT_HEAD;
266        this.axisLabelGap = DEFAULT_AXIS_LABEL_GAP;
267        this.axisLinePaint = Color.BLACK;
268        this.axisLineStroke = new BasicStroke(1.0f);
269
270        this.interiorGap = DEFAULT_INTERIOR_GAP;
271        this.startAngle = DEFAULT_START_ANGLE;
272        this.direction = Rotation.CLOCKWISE;
273        this.maxValue = DEFAULT_MAX_VALUE;
274
275        this.seriesPaint = null;
276        this.seriesPaintList = new PaintList();
277        this.baseSeriesPaint = null;
278
279        this.seriesOutlinePaint = null;
280        this.seriesOutlinePaintList = new PaintList();
281        this.baseSeriesOutlinePaint = DEFAULT_OUTLINE_PAINT;
282
283        this.seriesOutlineStroke = null;
284        this.seriesOutlineStrokeList = new StrokeList();
285        this.baseSeriesOutlineStroke = DEFAULT_OUTLINE_STROKE;
286
287        this.labelFont = DEFAULT_LABEL_FONT;
288        this.labelPaint = DEFAULT_LABEL_PAINT;
289        this.labelGenerator = new StandardCategoryItemLabelGenerator();
290
291        this.legendItemShape = DEFAULT_LEGEND_ITEM_CIRCLE;
292    }
293
294    /**
295     * Returns a short string describing the type of plot.
296     *
297     * @return The plot type.
298     */
299    @Override
300    public String getPlotType() {
301        // return localizationResources.getString("Radar_Plot");
302        return ("Spider Web Plot");
303    }
304
305    /**
306     * Returns the dataset.
307     *
308     * @return The dataset (possibly {@code null}).
309     *
310     * @see #setDataset(CategoryDataset)
311     */
312    public CategoryDataset getDataset() {
313        return this.dataset;
314    }
315
316    /**
317     * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
318     * to all registered listeners.
319     *
320     * @param dataset  the dataset ({@code null} permitted).
321     *
322     * @see #getDataset()
323     */
324    public void setDataset(CategoryDataset dataset) {
325        // if there is an existing dataset, remove the plot from the list of
326        // change listeners...
327        if (this.dataset != null) {
328            this.dataset.removeChangeListener(this);
329        }
330
331        // set the new dataset, and register the chart as a change listener...
332        this.dataset = dataset;
333        if (dataset != null) {
334            setDatasetGroup(dataset.getGroup());
335            dataset.addChangeListener(this);
336        }
337
338        // send a dataset change event to self to trigger plot change event
339        datasetChanged(new DatasetChangeEvent(this, dataset));
340    }
341
342    /**
343     * Method to determine if the web chart is to be filled.
344     *
345     * @return A boolean.
346     *
347     * @see #setWebFilled(boolean)
348     */
349    public boolean isWebFilled() {
350        return this.webFilled;
351    }
352
353    /**
354     * Sets the webFilled flag and sends a {@link PlotChangeEvent} to all
355     * registered listeners.
356     *
357     * @param flag  the flag.
358     *
359     * @see #isWebFilled()
360     */
361    public void setWebFilled(boolean flag) {
362        this.webFilled = flag;
363        fireChangeEvent();
364    }
365
366    /**
367     * Returns the alpha value for filling a graph (in the range 0.0 to 1.0).
368     *
369     * @return The alpha value for filling a spider plot polygon.
370     *
371     * @see #setWebFillAlpha(float)
372     */
373    public float getWebFillAlpha() {
374        return webFillAlpha;
375    }
376
377    /**
378     * Sets the alpha value for the fill of a plot polygon and sends a {@link PlotChangeEvent} to all
379     * registered listeners.
380     *
381     * @param alpha the new alpha value. If it is outside [0,1] it will be corrected to fit the range.
382     * @see #getWebFillAlpha()
383     */
384    public void setWebFillAlpha(float alpha) {
385        this.webFillAlpha = alpha;
386        if (webFillAlpha < 0f) {
387            webFillAlpha = 0f;
388        } else if (webFillAlpha > 1f) {
389            webFillAlpha = 1f;
390        }
391        fireChangeEvent();
392    }
393
394    /**
395     * Returns the data extract order (by row or by column).
396     *
397     * @return The data extract order (never {@code null}).
398     *
399     * @see #setDataExtractOrder(TableOrder)
400     */
401    public TableOrder getDataExtractOrder() {
402        return this.dataExtractOrder;
403    }
404
405    /**
406     * Sets the data extract order (by row or by column) and sends a
407     * {@link PlotChangeEvent}to all registered listeners.
408     *
409     * @param order the order ({@code null} not permitted).
410     *
411     * @throws IllegalArgumentException if {@code order} is
412     *     {@code null}.
413     *
414     * @see #getDataExtractOrder()
415     */
416    public void setDataExtractOrder(TableOrder order) {
417        Args.nullNotPermitted(order, "order");
418        this.dataExtractOrder = order;
419        fireChangeEvent();
420    }
421
422    /**
423     * Returns the head percent (the default value is 0.01).
424     *
425     * @return The head percent (always &gt; 0).
426     *
427     * @see #setHeadPercent(double)
428     */
429    public double getHeadPercent() {
430        return this.headPercent;
431    }
432
433    /**
434     * Sets the head percent and sends a {@link PlotChangeEvent} to all
435     * registered listeners.  Note that 0.10 is 10 percent.
436     *
437     * @param percent  the percent (must be greater than zero).
438     *
439     * @see #getHeadPercent()
440     */
441    public void setHeadPercent(double percent) {
442        Args.requireNonNegative(percent, "percent");
443        this.headPercent = percent;
444        fireChangeEvent();
445    }
446
447    /**
448     * Returns the start angle for the first radar axis.
449     * <BR>
450     * This is measured in degrees starting from 3 o'clock (Java Arc2D default)
451     * and measuring anti-clockwise.
452     *
453     * @return The start angle.
454     *
455     * @see #setStartAngle(double)
456     */
457    public double getStartAngle() {
458        return this.startAngle;
459    }
460
461    /**
462     * Sets the starting angle and sends a {@link PlotChangeEvent} to all
463     * registered listeners.
464     * <P>
465     * The initial default value is 90 degrees, which corresponds to 12 o'clock.
466     * A value of zero corresponds to 3 o'clock... this is the encoding used by
467     * Java's Arc2D class.
468     *
469     * @param angle  the angle (in degrees).
470     *
471     * @see #getStartAngle()
472     */
473    public void setStartAngle(double angle) {
474        this.startAngle = angle;
475        fireChangeEvent();
476    }
477
478    /**
479     * Returns the maximum value any category axis can take.
480     *
481     * @return The maximum value.
482     *
483     * @see #setMaxValue(double)
484     */
485    public double getMaxValue() {
486        return this.maxValue;
487    }
488
489    /**
490     * Sets the maximum value any category axis can take and sends
491     * a {@link PlotChangeEvent} to all registered listeners.
492     *
493     * @param value  the maximum value.
494     *
495     * @see #getMaxValue()
496     */
497    public void setMaxValue(double value) {
498        this.maxValue = value;
499        fireChangeEvent();
500    }
501
502    /**
503     * Returns the direction in which the radar axes are drawn
504     * (clockwise or anti-clockwise).
505     *
506     * @return The direction (never {@code null}).
507     *
508     * @see #setDirection(Rotation)
509     */
510    public Rotation getDirection() {
511        return this.direction;
512    }
513
514    /**
515     * Sets the direction in which the radar axes are drawn and sends a
516     * {@link PlotChangeEvent} to all registered listeners.
517     *
518     * @param direction  the direction ({@code null} not permitted).
519     *
520     * @see #getDirection()
521     */
522    public void setDirection(Rotation direction) {
523        Args.nullNotPermitted(direction, "direction");
524        this.direction = direction;
525        fireChangeEvent();
526    }
527
528    /**
529     * Returns the interior gap, measured as a percentage of the available
530     * drawing space.
531     *
532     * @return The gap (as a percentage of the available drawing space).
533     *
534     * @see #setInteriorGap(double)
535     */
536    public double getInteriorGap() {
537        return this.interiorGap;
538    }
539
540    /**
541     * Sets the interior gap and sends a {@link PlotChangeEvent} to all
542     * registered listeners. This controls the space between the edges of the
543     * plot and the plot area itself (the region where the axis labels appear).
544     *
545     * @param percent  the gap (as a percentage of the available drawing space).
546     *
547     * @see #getInteriorGap()
548     */
549    public void setInteriorGap(double percent) {
550        if ((percent < 0.0) || (percent > MAX_INTERIOR_GAP)) {
551            throw new IllegalArgumentException(
552                    "Percentage outside valid range.");
553        }
554        if (this.interiorGap != percent) {
555            this.interiorGap = percent;
556            fireChangeEvent();
557        }
558    }
559
560    /**
561     * Returns the axis label gap.
562     *
563     * @return The axis label gap.
564     *
565     * @see #setAxisLabelGap(double)
566     */
567    public double getAxisLabelGap() {
568        return this.axisLabelGap;
569    }
570
571    /**
572     * Sets the axis label gap and sends a {@link PlotChangeEvent} to all
573     * registered listeners.
574     *
575     * @param gap  the gap.
576     *
577     * @see #getAxisLabelGap()
578     */
579    public void setAxisLabelGap(double gap) {
580        this.axisLabelGap = gap;
581        fireChangeEvent();
582    }
583
584    /**
585     * Returns the paint used to draw the axis lines.
586     *
587     * @return The paint used to draw the axis lines (never {@code null}).
588     *
589     * @see #setAxisLinePaint(Paint)
590     * @see #getAxisLineStroke()
591     */
592    public Paint getAxisLinePaint() {
593        return this.axisLinePaint;
594    }
595
596    /**
597     * Sets the paint used to draw the axis lines and sends a
598     * {@link PlotChangeEvent} to all registered listeners.
599     *
600     * @param paint  the paint ({@code null} not permitted).
601     *
602     * @see #getAxisLinePaint()
603     */
604    public void setAxisLinePaint(Paint paint) {
605        Args.nullNotPermitted(paint, "paint");
606        this.axisLinePaint = paint;
607        fireChangeEvent();
608    }
609
610    /**
611     * Returns the stroke used to draw the axis lines.
612     *
613     * @return The stroke used to draw the axis lines (never {@code null}).
614     *
615     * @see #setAxisLineStroke(Stroke)
616     * @see #getAxisLinePaint()
617     */
618    public Stroke getAxisLineStroke() {
619        return this.axisLineStroke;
620    }
621
622    /**
623     * Sets the stroke used to draw the axis lines and sends a
624     * {@link PlotChangeEvent} to all registered listeners.
625     *
626     * @param stroke  the stroke ({@code null} not permitted).
627     *
628     * @see #getAxisLineStroke()
629     */
630    public void setAxisLineStroke(Stroke stroke) {
631        Args.nullNotPermitted(stroke, "stroke");
632        this.axisLineStroke = stroke;
633        fireChangeEvent();
634    }
635
636    //// SERIES PAINT /////////////////////////
637
638    /**
639     * Returns the paint for ALL series in the plot.
640     *
641     * @return The paint (possibly {@code null}).
642     *
643     * @see #setSeriesPaint(Paint)
644     */
645    public Paint getSeriesPaint() {
646        return this.seriesPaint;
647    }
648
649    /**
650     * Sets the paint for ALL series in the plot.  If this is set to
651     * {@code null}, then a list of paints is used instead (to allow different
652     * colors to be used for each series of the radar group).
653     *
654     * @param paint the paint ({@code null} permitted).
655     *
656     * @see #getSeriesPaint()
657     */
658    public void setSeriesPaint(Paint paint) {
659        this.seriesPaint = paint;
660        fireChangeEvent();
661    }
662
663    /**
664     * Returns the paint for the specified series.
665     *
666     * @param series  the series index (zero-based).
667     *
668     * @return The paint (never {@code null}).
669     *
670     * @see #setSeriesPaint(int, Paint)
671     */
672    public Paint getSeriesPaint(int series) {
673
674        // return the override, if there is one...
675        if (this.seriesPaint != null) {
676            return this.seriesPaint;
677        }
678
679        // otherwise look up the paint list
680        Paint result = this.seriesPaintList.getPaint(series);
681        if (result == null) {
682            DrawingSupplier supplier = getDrawingSupplier();
683            if (supplier != null) {
684                Paint p = supplier.getNextPaint();
685                this.seriesPaintList.setPaint(series, p);
686                result = p;
687            }
688            else {
689                result = this.baseSeriesPaint;
690            }
691        }
692        return result;
693
694    }
695
696    /**
697     * Sets the paint used to fill a series of the radar and sends a
698     * {@link PlotChangeEvent} to all registered listeners.
699     *
700     * @param series  the series index (zero-based).
701     * @param paint  the paint ({@code null} permitted).
702     *
703     * @see #getSeriesPaint(int)
704     */
705    public void setSeriesPaint(int series, Paint paint) {
706        this.seriesPaintList.setPaint(series, paint);
707        fireChangeEvent();
708    }
709
710    /**
711     * Returns the base series paint. This is used when no other paint is
712     * available.
713     *
714     * @return The paint (never {@code null}).
715     *
716     * @see #setBaseSeriesPaint(Paint)
717     */
718    public Paint getBaseSeriesPaint() {
719      return this.baseSeriesPaint;
720    }
721
722    /**
723     * Sets the base series paint.
724     *
725     * @param paint  the paint ({@code null} not permitted).
726     *
727     * @see #getBaseSeriesPaint()
728     */
729    public void setBaseSeriesPaint(Paint paint) {
730        Args.nullNotPermitted(paint, "paint");
731        this.baseSeriesPaint = paint;
732        fireChangeEvent();
733    }
734
735    //// SERIES OUTLINE PAINT ////////////////////////////
736
737    /**
738     * Returns the outline paint for ALL series in the plot.
739     *
740     * @return The paint (possibly {@code null}).
741     */
742    public Paint getSeriesOutlinePaint() {
743        return this.seriesOutlinePaint;
744    }
745
746    /**
747     * Sets the outline paint for ALL series in the plot. If this is set to
748     * {@code null}, then a list of paints is used instead (to allow
749     * different colors to be used for each series).
750     *
751     * @param paint  the paint ({@code null} permitted).
752     */
753    public void setSeriesOutlinePaint(Paint paint) {
754        this.seriesOutlinePaint = paint;
755        fireChangeEvent();
756    }
757
758    /**
759     * Returns the paint for the specified series.
760     *
761     * @param series  the series index (zero-based).
762     *
763     * @return The paint (never {@code null}).
764     */
765    public Paint getSeriesOutlinePaint(int series) {
766        // return the override, if there is one...
767        if (this.seriesOutlinePaint != null) {
768            return this.seriesOutlinePaint;
769        }
770        // otherwise look up the paint list
771        Paint result = this.seriesOutlinePaintList.getPaint(series);
772        if (result == null) {
773            result = this.baseSeriesOutlinePaint;
774        }
775        return result;
776    }
777
778    /**
779     * Sets the paint used to fill a series of the radar and sends a
780     * {@link PlotChangeEvent} to all registered listeners.
781     *
782     * @param series  the series index (zero-based).
783     * @param paint  the paint ({@code null} permitted).
784     */
785    public void setSeriesOutlinePaint(int series, Paint paint) {
786        this.seriesOutlinePaintList.setPaint(series, paint);
787        fireChangeEvent();
788    }
789
790    /**
791     * Returns the base series paint. This is used when no other paint is
792     * available.
793     *
794     * @return The paint (never {@code null}).
795     */
796    public Paint getBaseSeriesOutlinePaint() {
797        return this.baseSeriesOutlinePaint;
798    }
799
800    /**
801     * Sets the base series paint.
802     *
803     * @param paint  the paint ({@code null} not permitted).
804     */
805    public void setBaseSeriesOutlinePaint(Paint paint) {
806        Args.nullNotPermitted(paint, "paint");
807        this.baseSeriesOutlinePaint = paint;
808        fireChangeEvent();
809    }
810
811    //// SERIES OUTLINE STROKE /////////////////////
812
813    /**
814     * Returns the outline stroke for ALL series in the plot.
815     *
816     * @return The stroke (possibly {@code null}).
817     */
818    public Stroke getSeriesOutlineStroke() {
819        return this.seriesOutlineStroke;
820    }
821
822    /**
823     * Sets the outline stroke for ALL series in the plot. If this is set to
824     * {@code null}, then a list of paints is used instead (to allow
825     * different colors to be used for each series).
826     *
827     * @param stroke  the stroke ({@code null} permitted).
828     */
829    public void setSeriesOutlineStroke(Stroke stroke) {
830        this.seriesOutlineStroke = stroke;
831        fireChangeEvent();
832    }
833
834    /**
835     * Returns the stroke for the specified series.
836     *
837     * @param series  the series index (zero-based).
838     *
839     * @return The stroke (never {@code null}).
840     */
841    public Stroke getSeriesOutlineStroke(int series) {
842
843        // return the override, if there is one...
844        if (this.seriesOutlineStroke != null) {
845            return this.seriesOutlineStroke;
846        }
847
848        // otherwise look up the paint list
849        Stroke result = this.seriesOutlineStrokeList.getStroke(series);
850        if (result == null) {
851            result = this.baseSeriesOutlineStroke;
852        }
853        return result;
854
855    }
856
857    /**
858     * Sets the stroke used to fill a series of the radar and sends a
859     * {@link PlotChangeEvent} to all registered listeners.
860     *
861     * @param series  the series index (zero-based).
862     * @param stroke  the stroke ({@code null} permitted).
863     */
864    public void setSeriesOutlineStroke(int series, Stroke stroke) {
865        this.seriesOutlineStrokeList.setStroke(series, stroke);
866        fireChangeEvent();
867    }
868
869    /**
870     * Returns the base series stroke. This is used when no other stroke is
871     * available.
872     *
873     * @return The stroke (never {@code null}).
874     */
875    public Stroke getBaseSeriesOutlineStroke() {
876        return this.baseSeriesOutlineStroke;
877    }
878
879    /**
880     * Sets the base series stroke.
881     *
882     * @param stroke  the stroke ({@code null} not permitted).
883     */
884    public void setBaseSeriesOutlineStroke(Stroke stroke) {
885        Args.nullNotPermitted(stroke, "stroke");
886        this.baseSeriesOutlineStroke = stroke;
887        fireChangeEvent();
888    }
889
890    /**
891     * Returns the shape used for legend items.
892     *
893     * @return The shape (never {@code null}).
894     *
895     * @see #setLegendItemShape(Shape)
896     */
897    public Shape getLegendItemShape() {
898        return this.legendItemShape;
899    }
900
901    /**
902     * Sets the shape used for legend items and sends a {@link PlotChangeEvent}
903     * to all registered listeners.
904     *
905     * @param shape  the shape ({@code null} not permitted).
906     *
907     * @see #getLegendItemShape()
908     */
909    public void setLegendItemShape(Shape shape) {
910        Args.nullNotPermitted(shape, "shape");
911        this.legendItemShape = shape;
912        fireChangeEvent();
913    }
914
915    /**
916     * Returns the series label font.
917     *
918     * @return The font (never {@code null}).
919     *
920     * @see #setLabelFont(Font)
921     */
922    public Font getLabelFont() {
923        return this.labelFont;
924    }
925
926    /**
927     * Sets the series label font and sends a {@link PlotChangeEvent} to all
928     * registered listeners.
929     *
930     * @param font  the font ({@code null} not permitted).
931     *
932     * @see #getLabelFont()
933     */
934    public void setLabelFont(Font font) {
935        Args.nullNotPermitted(font, "font");
936        this.labelFont = font;
937        fireChangeEvent();
938    }
939
940    /**
941     * Returns the series label paint.
942     *
943     * @return The paint (never {@code null}).
944     *
945     * @see #setLabelPaint(Paint)
946     */
947    public Paint getLabelPaint() {
948        return this.labelPaint;
949    }
950
951    /**
952     * Sets the series label paint and sends a {@link PlotChangeEvent} to all
953     * registered listeners.
954     *
955     * @param paint  the paint ({@code null} not permitted).
956     *
957     * @see #getLabelPaint()
958     */
959    public void setLabelPaint(Paint paint) {
960        Args.nullNotPermitted(paint, "paint");
961        this.labelPaint = paint;
962        fireChangeEvent();
963    }
964
965    /**
966     * Returns the label generator.
967     *
968     * @return The label generator (never {@code null}).
969     *
970     * @see #setLabelGenerator(CategoryItemLabelGenerator)
971     */
972    public CategoryItemLabelGenerator getLabelGenerator() {
973        return this.labelGenerator;
974    }
975
976    /**
977     * Sets the label generator and sends a {@link PlotChangeEvent} to all
978     * registered listeners.
979     *
980     * @param generator  the generator ({@code null} not permitted).
981     *
982     * @see #getLabelGenerator()
983     */
984    public void setLabelGenerator(CategoryItemLabelGenerator generator) {
985        Args.nullNotPermitted(generator, "generator");
986        this.labelGenerator = generator;
987    }
988
989    /**
990     * Returns the tool tip generator for the plot.
991     *
992     * @return The tool tip generator (possibly {@code null}).
993     *
994     * @see #setToolTipGenerator(CategoryToolTipGenerator)
995     */
996    public CategoryToolTipGenerator getToolTipGenerator() {
997        return this.toolTipGenerator;
998    }
999
1000    /**
1001     * Sets the tool tip generator for the plot and sends a
1002     * {@link PlotChangeEvent} to all registered listeners.
1003     *
1004     * @param generator  the generator ({@code null} permitted).
1005     *
1006     * @see #getToolTipGenerator()
1007     */
1008    public void setToolTipGenerator(CategoryToolTipGenerator generator) {
1009        this.toolTipGenerator = generator;
1010        fireChangeEvent();
1011    }
1012
1013    /**
1014     * Returns the URL generator for the plot.
1015     *
1016     * @return The URL generator (possibly {@code null}).
1017     *
1018     * @see #setURLGenerator(CategoryURLGenerator)
1019     */
1020    public CategoryURLGenerator getURLGenerator() {
1021        return this.urlGenerator;
1022    }
1023
1024    /**
1025     * Sets the URL generator for the plot and sends a
1026     * {@link PlotChangeEvent} to all registered listeners.
1027     *
1028     * @param generator  the generator ({@code null} permitted).
1029     *
1030     * @see #getURLGenerator()
1031     */
1032    public void setURLGenerator(CategoryURLGenerator generator) {
1033        this.urlGenerator = generator;
1034        fireChangeEvent();
1035    }
1036
1037    /**
1038     * Returns a collection of legend items for the spider web chart.
1039     *
1040     * @return The legend items (never {@code null}).
1041     */
1042    @Override
1043    public LegendItemCollection getLegendItems() {
1044        LegendItemCollection result = new LegendItemCollection();
1045        if (getDataset() == null) {
1046            return result;
1047        }
1048        List keys = null;
1049        if (this.dataExtractOrder == TableOrder.BY_ROW) {
1050            keys = this.dataset.getRowKeys();
1051        }
1052        else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1053            keys = this.dataset.getColumnKeys();
1054        }
1055        if (keys == null) {
1056            return result;
1057        }
1058
1059        int series = 0;
1060        Iterator iterator = keys.iterator();
1061        Shape shape = getLegendItemShape();
1062        while (iterator.hasNext()) {
1063            Comparable key = (Comparable) iterator.next();
1064            String label = key.toString();
1065            String description = label;
1066            Paint paint = getSeriesPaint(series);
1067            Paint outlinePaint = getSeriesOutlinePaint(series);
1068            Stroke stroke = getSeriesOutlineStroke(series);
1069            LegendItem item = new LegendItem(label, description,
1070                    null, null, shape, paint, stroke, outlinePaint);
1071            item.setDataset(getDataset());
1072            item.setSeriesKey(key);
1073            item.setSeriesIndex(series);
1074            result.add(item);
1075            series++;
1076        }
1077        return result;
1078    }
1079
1080    /**
1081     * Returns a cartesian point from a polar angle, length and bounding box
1082     *
1083     * @param bounds  the area inside which the point needs to be.
1084     * @param angle  the polar angle, in degrees.
1085     * @param length  the relative length. Given in percent of maximum extend.
1086     *
1087     * @return The cartesian point.
1088     */
1089    protected Point2D getWebPoint(Rectangle2D bounds,
1090                                  double angle, double length) {
1091
1092        double angrad = Math.toRadians(angle);
1093        double x = Math.cos(angrad) * length * bounds.getWidth() / 2;
1094        double y = -Math.sin(angrad) * length * bounds.getHeight() / 2;
1095
1096        return new Point2D.Double(bounds.getX() + x + bounds.getWidth() / 2,
1097                bounds.getY() + y + bounds.getHeight() / 2);
1098    }
1099
1100    /**
1101     * Draws the plot on a Java 2D graphics device (such as the screen or a
1102     * printer).
1103     *
1104     * @param g2  the graphics device.
1105     * @param area  the area within which the plot should be drawn.
1106     * @param anchor  the anchor point ({@code null} permitted).
1107     * @param parentState  the state from the parent plot, if there is one.
1108     * @param info  collects info about the drawing.
1109     */
1110    @Override
1111    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
1112            PlotState parentState, PlotRenderingInfo info) {
1113
1114        // adjust for insets...
1115        RectangleInsets insets = getInsets();
1116        insets.trim(area);
1117
1118        if (info != null) {
1119            info.setPlotArea(area);
1120            info.setDataArea(area);
1121        }
1122
1123        drawBackground(g2, area);
1124        drawOutline(g2, area);
1125
1126        Shape savedClip = g2.getClip();
1127
1128        g2.clip(area);
1129        Composite originalComposite = g2.getComposite();
1130        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1131                getForegroundAlpha()));
1132
1133        if (!DatasetUtils.isEmptyOrNull(this.dataset)) {
1134            int seriesCount, catCount;
1135
1136            if (this.dataExtractOrder == TableOrder.BY_ROW) {
1137                seriesCount = this.dataset.getRowCount();
1138                catCount = this.dataset.getColumnCount();
1139            }
1140            else {
1141                seriesCount = this.dataset.getColumnCount();
1142                catCount = this.dataset.getRowCount();
1143            }
1144
1145            // ensure we have a maximum value to use on the axes
1146            if (this.maxValue == DEFAULT_MAX_VALUE) {
1147                calculateMaxValue(seriesCount, catCount);
1148            }
1149
1150            // Next, setup the plot area
1151
1152            // adjust the plot area by the interior spacing value
1153
1154            double gapHorizontal = area.getWidth() * getInteriorGap();
1155            double gapVertical = area.getHeight() * getInteriorGap();
1156
1157            double X = area.getX() + gapHorizontal / 2;
1158            double Y = area.getY() + gapVertical / 2;
1159            double W = area.getWidth() - gapHorizontal;
1160            double H = area.getHeight() - gapVertical;
1161
1162            double headW = area.getWidth() * this.headPercent;
1163            double headH = area.getHeight() * this.headPercent;
1164
1165            // make the chart area a square
1166            double min = Math.min(W, H) / 2;
1167            X = (X + X + W) / 2 - min;
1168            Y = (Y + Y + H) / 2 - min;
1169            W = 2 * min;
1170            H = 2 * min;
1171
1172            Point2D  centre = new Point2D.Double(X + W / 2, Y + H / 2);
1173            Rectangle2D radarArea = new Rectangle2D.Double(X, Y, W, H);
1174
1175            // draw the axis and category label
1176            for (int cat = 0; cat < catCount; cat++) {
1177                double angle = getStartAngle()
1178                        + (getDirection().getFactor() * cat * 360 / catCount);
1179
1180                Point2D endPoint = getWebPoint(radarArea, angle, 1);
1181                                                     // 1 = end of axis
1182                Line2D  line = new Line2D.Double(centre, endPoint);
1183                g2.setPaint(this.axisLinePaint);
1184                g2.setStroke(this.axisLineStroke);
1185                g2.draw(line);
1186                drawLabel(g2, radarArea, 0.0, cat, angle, 360.0 / catCount);
1187            }
1188
1189            // Now actually plot each of the series polygons..
1190            for (int series = 0; series < seriesCount; series++) {
1191                drawRadarPoly(g2, radarArea, centre, info, series, catCount,
1192                        headH, headW);
1193            }
1194        }
1195        else {
1196            drawNoDataMessage(g2, area);
1197        }
1198        g2.setClip(savedClip);
1199        g2.setComposite(originalComposite);
1200        drawOutline(g2, area);
1201    }
1202
1203    /**
1204     * loop through each of the series to get the maximum value
1205     * on each category axis
1206     *
1207     * @param seriesCount  the number of series
1208     * @param catCount  the number of categories
1209     */
1210    private void calculateMaxValue(int seriesCount, int catCount) {
1211        double v;
1212        Number nV;
1213
1214        for (int seriesIndex = 0; seriesIndex < seriesCount; seriesIndex++) {
1215            for (int catIndex = 0; catIndex < catCount; catIndex++) {
1216                nV = getPlotValue(seriesIndex, catIndex);
1217                if (nV != null) {
1218                    v = nV.doubleValue();
1219                    if (v > this.maxValue) {
1220                        this.maxValue = v;
1221                    }
1222                }
1223            }
1224        }
1225    }
1226
1227    /**
1228     * Draws a radar plot polygon.
1229     *
1230     * @param g2 the graphics device.
1231     * @param plotArea the area we are plotting in (already adjusted).
1232     * @param centre the centre point of the radar axes
1233     * @param info chart rendering info.
1234     * @param series the series within the dataset we are plotting
1235     * @param catCount the number of categories per radar plot
1236     * @param headH the data point height
1237     * @param headW the data point width
1238     */
1239    protected void drawRadarPoly(Graphics2D g2,
1240                                 Rectangle2D plotArea,
1241                                 Point2D centre,
1242                                 PlotRenderingInfo info,
1243                                 int series, int catCount,
1244                                 double headH, double headW) {
1245
1246        Polygon polygon = new Polygon();
1247
1248        EntityCollection entities = null;
1249        if (info != null) {
1250            entities = info.getOwner().getEntityCollection();
1251        }
1252
1253        // plot the data...
1254        for (int cat = 0; cat < catCount; cat++) {
1255
1256            Number dataValue = getPlotValue(series, cat);
1257
1258            if (dataValue != null) {
1259                double value = dataValue.doubleValue();
1260
1261                if (value >= 0) { // draw the polygon series...
1262
1263                    // Finds our starting angle from the centre for this axis
1264
1265                    double angle = getStartAngle()
1266                        + (getDirection().getFactor() * cat * 360 / catCount);
1267
1268                    // The following angle calc will ensure there isn't a top
1269                    // vertical axis - this may be useful if you don't want any
1270                    // given criteria to 'appear' move important than the
1271                    // others..
1272                    //  + (getDirection().getFactor()
1273                    //        * (cat + 0.5) * 360 / catCount);
1274
1275                    // find the point at the appropriate distance end point
1276                    // along the axis/angle identified above and add it to the
1277                    // polygon
1278
1279                    Point2D point = getWebPoint(plotArea, angle,
1280                            value / this.maxValue);
1281                    polygon.addPoint((int) point.getX(), (int) point.getY());
1282
1283                    // put an elipse at the point being plotted..
1284
1285                    Paint paint = getSeriesPaint(series);
1286                    Paint outlinePaint = getSeriesOutlinePaint(series);
1287                    Stroke outlineStroke = getSeriesOutlineStroke(series);
1288
1289                    Ellipse2D head = new Ellipse2D.Double(point.getX()
1290                            - headW / 2, point.getY() - headH / 2, headW,
1291                            headH);
1292                    g2.setPaint(paint);
1293                    g2.fill(head);
1294                    g2.setStroke(outlineStroke);
1295                    g2.setPaint(outlinePaint);
1296                    g2.draw(head);
1297
1298                    if (entities != null) {
1299                        int row, col;
1300                        if (this.dataExtractOrder == TableOrder.BY_ROW) {
1301                            row = series;
1302                            col = cat;
1303                        }
1304                        else {
1305                            row = cat;
1306                            col = series;
1307                        }
1308                        String tip = null;
1309                        if (this.toolTipGenerator != null) {
1310                            tip = this.toolTipGenerator.generateToolTip(
1311                                    this.dataset, row, col);
1312                        }
1313
1314                        String url = null;
1315                        if (this.urlGenerator != null) {
1316                            url = this.urlGenerator.generateURL(this.dataset,
1317                                   row, col);
1318                        }
1319
1320                        Shape area = new Rectangle(
1321                                (int) (point.getX() - headW),
1322                                (int) (point.getY() - headH),
1323                                (int) (headW * 2), (int) (headH * 2));
1324                        CategoryItemEntity entity = new CategoryItemEntity(
1325                                area, tip, url, this.dataset,
1326                                this.dataset.getRowKey(row),
1327                                this.dataset.getColumnKey(col));
1328                        entities.add(entity);
1329                    }
1330
1331                }
1332            }
1333        }
1334        // Plot the polygon
1335
1336        Paint paint = getSeriesPaint(series);
1337        g2.setPaint(paint);
1338        g2.setStroke(getSeriesOutlineStroke(series));
1339        g2.draw(polygon);
1340
1341        // Lastly, fill the web polygon if this is required
1342
1343        if (this.webFilled) {
1344            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1345                    webFillAlpha));
1346            g2.fill(polygon);
1347            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1348                    getForegroundAlpha()));
1349        }
1350    }
1351
1352    /**
1353     * Returns the value to be plotted at the intersection of the
1354     * series and the category.  This allows us to plot
1355     * {@code BY_ROW} or {@code BY_COLUMN} which basically is just
1356     * reversing the definition of the categories and data series being
1357     * plotted.
1358     *
1359     * @param series the series to be plotted.
1360     * @param cat the category within the series to be plotted.
1361     *
1362     * @return The value to be plotted (possibly {@code null}).
1363     *
1364     * @see #getDataExtractOrder()
1365     */
1366    protected Number getPlotValue(int series, int cat) {
1367        Number value = null;
1368        if (this.dataExtractOrder == TableOrder.BY_ROW) {
1369            value = this.dataset.getValue(series, cat);
1370        }
1371        else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1372            value = this.dataset.getValue(cat, series);
1373        }
1374        return value;
1375    }
1376
1377    /**
1378     * Draws the label for one axis.
1379     *
1380     * @param g2  the graphics device.
1381     * @param plotArea  the plot area
1382     * @param value  the value of the label (ignored).
1383     * @param cat  the category (zero-based index).
1384     * @param startAngle  the starting angle.
1385     * @param extent  the extent of the arc.
1386     */
1387    protected void drawLabel(Graphics2D g2, Rectangle2D plotArea, double value,
1388                             int cat, double startAngle, double extent) {
1389        FontRenderContext frc = g2.getFontRenderContext();
1390
1391        String label;
1392        if (this.dataExtractOrder == TableOrder.BY_ROW) {
1393            // if series are in rows, then the categories are the column keys
1394            label = this.labelGenerator.generateColumnLabel(this.dataset, cat);
1395        }
1396        else {
1397            // if series are in columns, then the categories are the row keys
1398            label = this.labelGenerator.generateRowLabel(this.dataset, cat);
1399        }
1400
1401        Rectangle2D labelBounds = getLabelFont().getStringBounds(label, frc);
1402        LineMetrics lm = getLabelFont().getLineMetrics(label, frc);
1403        double ascent = lm.getAscent();
1404
1405        Point2D labelLocation = calculateLabelLocation(labelBounds, ascent,
1406                plotArea, startAngle);
1407
1408        Composite saveComposite = g2.getComposite();
1409
1410        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1411                1.0f));
1412        g2.setPaint(getLabelPaint());
1413        g2.setFont(getLabelFont());
1414        g2.drawString(label, (float) labelLocation.getX(),
1415                (float) labelLocation.getY());
1416        g2.setComposite(saveComposite);
1417    }
1418
1419    /**
1420     * Returns the location for a label
1421     *
1422     * @param labelBounds the label bounds.
1423     * @param ascent the ascent (height of font).
1424     * @param plotArea the plot area
1425     * @param startAngle the start angle for the pie series.
1426     *
1427     * @return The location for a label.
1428     */
1429    protected Point2D calculateLabelLocation(Rectangle2D labelBounds,
1430                                             double ascent,
1431                                             Rectangle2D plotArea,
1432                                             double startAngle)
1433    {
1434        Arc2D arc1 = new Arc2D.Double(plotArea, startAngle, 0, Arc2D.OPEN);
1435        Point2D point1 = arc1.getEndPoint();
1436
1437        double deltaX = -(point1.getX() - plotArea.getCenterX())
1438                        * this.axisLabelGap;
1439        double deltaY = -(point1.getY() - plotArea.getCenterY())
1440                        * this.axisLabelGap;
1441
1442        double labelX = point1.getX() - deltaX;
1443        double labelY = point1.getY() - deltaY;
1444
1445        if (labelX < plotArea.getCenterX()) {
1446            labelX -= labelBounds.getWidth();
1447        }
1448
1449        if (labelX == plotArea.getCenterX()) {
1450            labelX -= labelBounds.getWidth() / 2;
1451        }
1452
1453        if (labelY > plotArea.getCenterY()) {
1454            labelY += ascent;
1455        }
1456
1457        return new Point2D.Double(labelX, labelY);
1458    }
1459
1460    /**
1461     * Tests this plot for equality with an arbitrary object.
1462     *
1463     * @param obj  the object ({@code null} permitted).
1464     *
1465     * @return A boolean.
1466     */
1467    @Override
1468    public boolean equals(Object obj) {
1469        if (obj == this) {
1470            return true;
1471        }
1472        if (!(obj instanceof SpiderWebPlot)) {
1473            return false;
1474        }
1475        if (!super.equals(obj)) {
1476            return false;
1477        }
1478        SpiderWebPlot that = (SpiderWebPlot) obj;
1479        if (!this.dataExtractOrder.equals(that.dataExtractOrder)) {
1480            return false;
1481        }
1482        if (this.headPercent != that.headPercent) {
1483            return false;
1484        }
1485        if (this.interiorGap != that.interiorGap) {
1486            return false;
1487        }
1488        if (this.startAngle != that.startAngle) {
1489            return false;
1490        }
1491        if (!this.direction.equals(that.direction)) {
1492            return false;
1493        }
1494        if (this.maxValue != that.maxValue) {
1495            return false;
1496        }
1497        if (this.webFilled != that.webFilled) {
1498            return false;
1499        }
1500        if (this.webFillAlpha != that.webFillAlpha) {
1501            return false;
1502        }
1503        if (this.axisLabelGap != that.axisLabelGap) {
1504            return false;
1505        }
1506        if (!PaintUtils.equal(this.axisLinePaint, that.axisLinePaint)) {
1507            return false;
1508        }
1509        if (!this.axisLineStroke.equals(that.axisLineStroke)) {
1510            return false;
1511        }
1512        if (!ShapeUtils.equal(this.legendItemShape, that.legendItemShape)) {
1513            return false;
1514        }
1515        if (!PaintUtils.equal(this.seriesPaint, that.seriesPaint)) {
1516            return false;
1517        }
1518        if (!this.seriesPaintList.equals(that.seriesPaintList)) {
1519            return false;
1520        }
1521        if (!PaintUtils.equal(this.baseSeriesPaint, that.baseSeriesPaint)) {
1522            return false;
1523        }
1524        if (!PaintUtils.equal(this.seriesOutlinePaint,
1525                that.seriesOutlinePaint)) {
1526            return false;
1527        }
1528        if (!this.seriesOutlinePaintList.equals(that.seriesOutlinePaintList)) {
1529            return false;
1530        }
1531        if (!PaintUtils.equal(this.baseSeriesOutlinePaint,
1532                that.baseSeriesOutlinePaint)) {
1533            return false;
1534        }
1535        if (!Objects.equals(this.seriesOutlineStroke,
1536                that.seriesOutlineStroke)) {
1537            return false;
1538        }
1539        if (!this.seriesOutlineStrokeList.equals(
1540                that.seriesOutlineStrokeList)) {
1541            return false;
1542        }
1543        if (!this.baseSeriesOutlineStroke.equals(
1544                that.baseSeriesOutlineStroke)) {
1545            return false;
1546        }
1547        if (!this.labelFont.equals(that.labelFont)) {
1548            return false;
1549        }
1550        if (!PaintUtils.equal(this.labelPaint, that.labelPaint)) {
1551            return false;
1552        }
1553        if (!this.labelGenerator.equals(that.labelGenerator)) {
1554            return false;
1555        }
1556        if (!Objects.equals(this.toolTipGenerator,
1557                that.toolTipGenerator)) {
1558            return false;
1559        }
1560        if (!Objects.equals(this.urlGenerator,
1561                that.urlGenerator)) {
1562            return false;
1563        }
1564        return true;
1565    }
1566
1567    /**
1568     * Returns a clone of this plot.
1569     *
1570     * @return A clone of this plot.
1571     *
1572     * @throws CloneNotSupportedException if the plot cannot be cloned for
1573     *         any reason.
1574     */
1575    @Override
1576    public Object clone() throws CloneNotSupportedException {
1577        SpiderWebPlot clone = (SpiderWebPlot) super.clone();
1578        clone.legendItemShape = ShapeUtils.clone(this.legendItemShape);
1579        clone.seriesPaintList = (PaintList) this.seriesPaintList.clone();
1580        clone.seriesOutlinePaintList
1581                = (PaintList) this.seriesOutlinePaintList.clone();
1582        clone.seriesOutlineStrokeList
1583                = (StrokeList) this.seriesOutlineStrokeList.clone();
1584        return clone;
1585    }
1586
1587    /**
1588     * Provides serialization support.
1589     *
1590     * @param stream  the output stream.
1591     *
1592     * @throws IOException  if there is an I/O error.
1593     */
1594    private void writeObject(ObjectOutputStream stream) throws IOException {
1595        stream.defaultWriteObject();
1596
1597        SerialUtils.writeShape(this.legendItemShape, stream);
1598        SerialUtils.writePaint(this.seriesPaint, stream);
1599        SerialUtils.writePaint(this.baseSeriesPaint, stream);
1600        SerialUtils.writePaint(this.seriesOutlinePaint, stream);
1601        SerialUtils.writePaint(this.baseSeriesOutlinePaint, stream);
1602        SerialUtils.writeStroke(this.seriesOutlineStroke, stream);
1603        SerialUtils.writeStroke(this.baseSeriesOutlineStroke, stream);
1604        SerialUtils.writePaint(this.labelPaint, stream);
1605        SerialUtils.writePaint(this.axisLinePaint, stream);
1606        SerialUtils.writeStroke(this.axisLineStroke, stream);
1607    }
1608
1609    /**
1610     * Provides serialization support.
1611     *
1612     * @param stream  the input stream.
1613     *
1614     * @throws IOException  if there is an I/O error.
1615     * @throws ClassNotFoundException  if there is a classpath problem.
1616     */
1617    private void readObject(ObjectInputStream stream) throws IOException,
1618            ClassNotFoundException {
1619        stream.defaultReadObject();
1620
1621        this.legendItemShape = SerialUtils.readShape(stream);
1622        this.seriesPaint = SerialUtils.readPaint(stream);
1623        this.baseSeriesPaint = SerialUtils.readPaint(stream);
1624        this.seriesOutlinePaint = SerialUtils.readPaint(stream);
1625        this.baseSeriesOutlinePaint = SerialUtils.readPaint(stream);
1626        this.seriesOutlineStroke = SerialUtils.readStroke(stream);
1627        this.baseSeriesOutlineStroke = SerialUtils.readStroke(stream);
1628        this.labelPaint = SerialUtils.readPaint(stream);
1629        this.axisLinePaint = SerialUtils.readPaint(stream);
1630        this.axisLineStroke = SerialUtils.readStroke(stream);
1631        if (this.dataset != null) {
1632            this.dataset.addChangeListener(this);
1633        }
1634    }
1635
1636}