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