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