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    
070    package org.jfree.chart.plot;
071    
072    import java.awt.AlphaComposite;
073    import java.awt.BasicStroke;
074    import java.awt.Color;
075    import java.awt.Composite;
076    import java.awt.Font;
077    import java.awt.Graphics2D;
078    import java.awt.Paint;
079    import java.awt.Polygon;
080    import java.awt.Rectangle;
081    import java.awt.Shape;
082    import java.awt.Stroke;
083    import java.awt.font.FontRenderContext;
084    import java.awt.font.LineMetrics;
085    import java.awt.geom.Arc2D;
086    import java.awt.geom.Ellipse2D;
087    import java.awt.geom.Line2D;
088    import java.awt.geom.Point2D;
089    import java.awt.geom.Rectangle2D;
090    import java.io.IOException;
091    import java.io.ObjectInputStream;
092    import java.io.ObjectOutputStream;
093    import java.io.Serializable;
094    import java.util.Iterator;
095    import java.util.List;
096    
097    import org.jfree.chart.LegendItem;
098    import org.jfree.chart.LegendItemCollection;
099    import org.jfree.chart.entity.CategoryItemEntity;
100    import org.jfree.chart.entity.EntityCollection;
101    import org.jfree.chart.event.PlotChangeEvent;
102    import org.jfree.chart.labels.CategoryItemLabelGenerator;
103    import org.jfree.chart.labels.CategoryToolTipGenerator;
104    import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
105    import org.jfree.chart.urls.CategoryURLGenerator;
106    import org.jfree.data.category.CategoryDataset;
107    import org.jfree.data.general.DatasetChangeEvent;
108    import org.jfree.data.general.DatasetUtilities;
109    import org.jfree.io.SerialUtilities;
110    import org.jfree.ui.RectangleInsets;
111    import org.jfree.util.ObjectUtilities;
112    import org.jfree.util.PaintList;
113    import org.jfree.util.PaintUtilities;
114    import org.jfree.util.Rotation;
115    import org.jfree.util.ShapeUtilities;
116    import org.jfree.util.StrokeList;
117    import 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     */
124    public 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    
1080            LegendItemCollection result = new LegendItemCollection();
1081            if (getDataset() == null) {
1082                    return result;
1083            }
1084    
1085            List keys = null;
1086            if (this.dataExtractOrder == TableOrder.BY_ROW) {
1087                keys = this.dataset.getRowKeys();
1088            }
1089            else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1090                keys = this.dataset.getColumnKeys();
1091            }
1092    
1093            if (keys != null) {
1094                int series = 0;
1095                Iterator iterator = keys.iterator();
1096                Shape shape = getLegendItemShape();
1097    
1098                while (iterator.hasNext()) {
1099                    String label = iterator.next().toString();
1100                    String description = label;
1101    
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                    result.add(item);
1109                    series++;
1110                }
1111            }
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    }