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