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            LegendItemCollection result = new LegendItemCollection();
1080            if (getDataset() == null) {
1081                return result;
1082            }
1083    
1084            List keys = null;
1085            if (this.dataExtractOrder == TableOrder.BY_ROW) {
1086                keys = this.dataset.getRowKeys();
1087            }
1088            else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1089                keys = this.dataset.getColumnKeys();
1090            }
1091    
1092            if (keys != null) {
1093                int series = 0;
1094                Iterator iterator = keys.iterator();
1095                Shape shape = getLegendItemShape();
1096    
1097                while (iterator.hasNext()) {
1098                    String label = iterator.next().toString();
1099                    String description = label;
1100    
1101                    Paint paint = getSeriesPaint(series);
1102                    Paint outlinePaint = getSeriesOutlinePaint(series);
1103                    Stroke stroke = getSeriesOutlineStroke(series);
1104                    LegendItem item = new LegendItem(label, description,
1105                            null, null, shape, paint, stroke, outlinePaint);
1106                    item.setDataset(getDataset());
1107                    result.add(item);
1108                    series++;
1109                }
1110            }
1111    
1112            return result;
1113        }
1114    
1115        /**
1116         * Returns a cartesian point from a polar angle, length and bounding box
1117         *
1118         * @param bounds  the area inside which the point needs to be.
1119         * @param angle  the polar angle, in degrees.
1120         * @param length  the relative length. Given in percent of maximum extend.
1121         *
1122         * @return The cartesian point.
1123         */
1124        protected Point2D getWebPoint(Rectangle2D bounds,
1125                                      double angle, double length) {
1126    
1127            double angrad = Math.toRadians(angle);
1128            double x = Math.cos(angrad) * length * bounds.getWidth() / 2;
1129            double y = -Math.sin(angrad) * length * bounds.getHeight() / 2;
1130    
1131            return new Point2D.Double(bounds.getX() + x + bounds.getWidth() / 2,
1132                    bounds.getY() + y + bounds.getHeight() / 2);
1133        }
1134    
1135        /**
1136         * Draws the plot on a Java 2D graphics device (such as the screen or a
1137         * printer).
1138         *
1139         * @param g2  the graphics device.
1140         * @param area  the area within which the plot should be drawn.
1141         * @param anchor  the anchor point (<code>null</code> permitted).
1142         * @param parentState  the state from the parent plot, if there is one.
1143         * @param info  collects info about the drawing.
1144         */
1145        public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
1146                PlotState parentState, PlotRenderingInfo info) {
1147    
1148            // adjust for insets...
1149            RectangleInsets insets = getInsets();
1150            insets.trim(area);
1151    
1152            if (info != null) {
1153                info.setPlotArea(area);
1154                info.setDataArea(area);
1155            }
1156    
1157            drawBackground(g2, area);
1158            drawOutline(g2, area);
1159    
1160            Shape savedClip = g2.getClip();
1161    
1162            g2.clip(area);
1163            Composite originalComposite = g2.getComposite();
1164            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1165                    getForegroundAlpha()));
1166    
1167            if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
1168                int seriesCount = 0, catCount = 0;
1169    
1170                if (this.dataExtractOrder == TableOrder.BY_ROW) {
1171                    seriesCount = this.dataset.getRowCount();
1172                    catCount = this.dataset.getColumnCount();
1173                }
1174                else {
1175                    seriesCount = this.dataset.getColumnCount();
1176                    catCount = this.dataset.getRowCount();
1177                }
1178    
1179                // ensure we have a maximum value to use on the axes
1180                if (this.maxValue == DEFAULT_MAX_VALUE)
1181                    calculateMaxValue(seriesCount, catCount);
1182    
1183                // Next, setup the plot area
1184    
1185                // adjust the plot area by the interior spacing value
1186    
1187                double gapHorizontal = area.getWidth() * getInteriorGap();
1188                double gapVertical = area.getHeight() * getInteriorGap();
1189    
1190                double X = area.getX() + gapHorizontal / 2;
1191                double Y = area.getY() + gapVertical / 2;
1192                double W = area.getWidth() - gapHorizontal;
1193                double H = area.getHeight() - gapVertical;
1194    
1195                double headW = area.getWidth() * this.headPercent;
1196                double headH = area.getHeight() * this.headPercent;
1197    
1198                // make the chart area a square
1199                double min = Math.min(W, H) / 2;
1200                X = (X + X + W) / 2 - min;
1201                Y = (Y + Y + H) / 2 - min;
1202                W = 2 * min;
1203                H = 2 * min;
1204    
1205                Point2D  centre = new Point2D.Double(X + W / 2, Y + H / 2);
1206                Rectangle2D radarArea = new Rectangle2D.Double(X, Y, W, H);
1207    
1208                // draw the axis and category label
1209                for (int cat = 0; cat < catCount; cat++) {
1210                    double angle = getStartAngle()
1211                            + (getDirection().getFactor() * cat * 360 / catCount);
1212    
1213                    Point2D endPoint = getWebPoint(radarArea, angle, 1);
1214                                                         // 1 = end of axis
1215                    Line2D  line = new Line2D.Double(centre, endPoint);
1216                    g2.setPaint(this.axisLinePaint);
1217                    g2.setStroke(this.axisLineStroke);
1218                    g2.draw(line);
1219                    drawLabel(g2, radarArea, 0.0, cat, angle, 360.0 / catCount);
1220                }
1221    
1222                // Now actually plot each of the series polygons..
1223                for (int series = 0; series < seriesCount; series++) {
1224                    drawRadarPoly(g2, radarArea, centre, info, series, catCount,
1225                            headH, headW);
1226                }
1227            }
1228            else {
1229                drawNoDataMessage(g2, area);
1230            }
1231            g2.setClip(savedClip);
1232            g2.setComposite(originalComposite);
1233            drawOutline(g2, area);
1234        }
1235    
1236        /**
1237         * loop through each of the series to get the maximum value
1238         * on each category axis
1239         *
1240         * @param seriesCount  the number of series
1241         * @param catCount  the number of categories
1242         */
1243        private void calculateMaxValue(int seriesCount, int catCount) {
1244            double v = 0;
1245            Number nV = null;
1246    
1247            for (int seriesIndex = 0; seriesIndex < seriesCount; seriesIndex++) {
1248                for (int catIndex = 0; catIndex < catCount; catIndex++) {
1249                    nV = getPlotValue(seriesIndex, catIndex);
1250                    if (nV != null) {
1251                        v = nV.doubleValue();
1252                        if (v > this.maxValue) {
1253                            this.maxValue = v;
1254                        }
1255                    }
1256                }
1257            }
1258        }
1259    
1260        /**
1261         * Draws a radar plot polygon.
1262         *
1263         * @param g2 the graphics device.
1264         * @param plotArea the area we are plotting in (already adjusted).
1265         * @param centre the centre point of the radar axes
1266         * @param info chart rendering info.
1267         * @param series the series within the dataset we are plotting
1268         * @param catCount the number of categories per radar plot
1269         * @param headH the data point height
1270         * @param headW the data point width
1271         */
1272        protected void drawRadarPoly(Graphics2D g2,
1273                                     Rectangle2D plotArea,
1274                                     Point2D centre,
1275                                     PlotRenderingInfo info,
1276                                     int series, int catCount,
1277                                     double headH, double headW) {
1278    
1279            Polygon polygon = new Polygon();
1280    
1281            EntityCollection entities = null;
1282            if (info != null) {
1283                entities = info.getOwner().getEntityCollection();
1284            }
1285    
1286            // plot the data...
1287            for (int cat = 0; cat < catCount; cat++) {
1288    
1289                Number dataValue = getPlotValue(series, cat);
1290    
1291                if (dataValue != null) {
1292                    double value = dataValue.doubleValue();
1293    
1294                    if (value >= 0) { // draw the polygon series...
1295    
1296                        // Finds our starting angle from the centre for this axis
1297    
1298                        double angle = getStartAngle()
1299                            + (getDirection().getFactor() * cat * 360 / catCount);
1300    
1301                        // The following angle calc will ensure there isn't a top
1302                        // vertical axis - this may be useful if you don't want any
1303                        // given criteria to 'appear' move important than the
1304                        // others..
1305                        //  + (getDirection().getFactor()
1306                        //        * (cat + 0.5) * 360 / catCount);
1307    
1308                        // find the point at the appropriate distance end point
1309                        // along the axis/angle identified above and add it to the
1310                        // polygon
1311    
1312                        Point2D point = getWebPoint(plotArea, angle,
1313                                value / this.maxValue);
1314                        polygon.addPoint((int) point.getX(), (int) point.getY());
1315    
1316                        // put an elipse at the point being plotted..
1317    
1318                        Paint paint = getSeriesPaint(series);
1319                        Paint outlinePaint = getSeriesOutlinePaint(series);
1320                        Stroke outlineStroke = getSeriesOutlineStroke(series);
1321    
1322                        Ellipse2D head = new Ellipse2D.Double(point.getX()
1323                                - headW / 2, point.getY() - headH / 2, headW,
1324                                headH);
1325                        g2.setPaint(paint);
1326                        g2.fill(head);
1327                        g2.setStroke(outlineStroke);
1328                        g2.setPaint(outlinePaint);
1329                        g2.draw(head);
1330    
1331                        if (entities != null) {
1332                            int row = 0; int col = 0;
1333                            if (this.dataExtractOrder == TableOrder.BY_ROW) {
1334                                row = series;
1335                                col = cat;
1336                            }
1337                            else {
1338                                row = cat;
1339                                col = series;
1340                            }
1341                            String tip = null;
1342                            if (this.toolTipGenerator != null) {
1343                                tip = this.toolTipGenerator.generateToolTip(
1344                                        this.dataset, row, col);
1345                            }
1346    
1347                            String url = null;
1348                            if (this.urlGenerator != null) {
1349                                url = this.urlGenerator.generateURL(this.dataset,
1350                                       row, col);
1351                            }
1352    
1353                            Shape area = new Rectangle(
1354                                    (int) (point.getX() - headW),
1355                                    (int) (point.getY() - headH),
1356                                    (int) (headW * 2), (int) (headH * 2));
1357                            CategoryItemEntity entity = new CategoryItemEntity(
1358                                    area, tip, url, this.dataset,
1359                                    this.dataset.getRowKey(row),
1360                                    this.dataset.getColumnKey(col));
1361                            entities.add(entity);
1362                        }
1363    
1364                    }
1365                }
1366            }
1367            // Plot the polygon
1368    
1369            Paint paint = getSeriesPaint(series);
1370            g2.setPaint(paint);
1371            g2.setStroke(getSeriesOutlineStroke(series));
1372            g2.draw(polygon);
1373    
1374            // Lastly, fill the web polygon if this is required
1375    
1376            if (this.webFilled) {
1377                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1378                        0.1f));
1379                g2.fill(polygon);
1380                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1381                        getForegroundAlpha()));
1382            }
1383        }
1384    
1385        /**
1386         * Returns the value to be plotted at the interseries of the
1387         * series and the category.  This allows us to plot
1388         * <code>BY_ROW</code> or <code>BY_COLUMN</code> which basically is just
1389         * reversing the definition of the categories and data series being
1390         * plotted.
1391         *
1392         * @param series the series to be plotted.
1393         * @param cat the category within the series to be plotted.
1394         *
1395         * @return The value to be plotted (possibly <code>null</code>).
1396         *
1397         * @see #getDataExtractOrder()
1398         */
1399        protected Number getPlotValue(int series, int cat) {
1400            Number value = null;
1401            if (this.dataExtractOrder == TableOrder.BY_ROW) {
1402                value = this.dataset.getValue(series, cat);
1403            }
1404            else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1405                value = this.dataset.getValue(cat, series);
1406            }
1407            return value;
1408        }
1409    
1410        /**
1411         * Draws the label for one axis.
1412         *
1413         * @param g2  the graphics device.
1414         * @param plotArea  the plot area
1415         * @param value  the value of the label (ignored).
1416         * @param cat  the category (zero-based index).
1417         * @param startAngle  the starting angle.
1418         * @param extent  the extent of the arc.
1419         */
1420        protected void drawLabel(Graphics2D g2, Rectangle2D plotArea, double value,
1421                                 int cat, double startAngle, double extent) {
1422            FontRenderContext frc = g2.getFontRenderContext();
1423    
1424            String label = null;
1425            if (this.dataExtractOrder == TableOrder.BY_ROW) {
1426                // if series are in rows, then the categories are the column keys
1427                label = this.labelGenerator.generateColumnLabel(this.dataset, cat);
1428            }
1429            else {
1430                // if series are in columns, then the categories are the row keys
1431                label = this.labelGenerator.generateRowLabel(this.dataset, cat);
1432            }
1433    
1434            Rectangle2D labelBounds = getLabelFont().getStringBounds(label, frc);
1435            LineMetrics lm = getLabelFont().getLineMetrics(label, frc);
1436            double ascent = lm.getAscent();
1437    
1438            Point2D labelLocation = calculateLabelLocation(labelBounds, ascent,
1439                    plotArea, startAngle);
1440    
1441            Composite saveComposite = g2.getComposite();
1442    
1443            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1444                    1.0f));
1445            g2.setPaint(getLabelPaint());
1446            g2.setFont(getLabelFont());
1447            g2.drawString(label, (float) labelLocation.getX(),
1448                    (float) labelLocation.getY());
1449            g2.setComposite(saveComposite);
1450        }
1451    
1452        /**
1453         * Returns the location for a label
1454         *
1455         * @param labelBounds the label bounds.
1456         * @param ascent the ascent (height of font).
1457         * @param plotArea the plot area
1458         * @param startAngle the start angle for the pie series.
1459         *
1460         * @return The location for a label.
1461         */
1462        protected Point2D calculateLabelLocation(Rectangle2D labelBounds,
1463                                                 double ascent,
1464                                                 Rectangle2D plotArea,
1465                                                 double startAngle)
1466        {
1467            Arc2D arc1 = new Arc2D.Double(plotArea, startAngle, 0, Arc2D.OPEN);
1468            Point2D point1 = arc1.getEndPoint();
1469    
1470            double deltaX = -(point1.getX() - plotArea.getCenterX())
1471                            * this.axisLabelGap;
1472            double deltaY = -(point1.getY() - plotArea.getCenterY())
1473                            * this.axisLabelGap;
1474    
1475            double labelX = point1.getX() - deltaX;
1476            double labelY = point1.getY() - deltaY;
1477    
1478            if (labelX < plotArea.getCenterX()) {
1479                labelX -= labelBounds.getWidth();
1480            }
1481    
1482            if (labelX == plotArea.getCenterX()) {
1483                labelX -= labelBounds.getWidth() / 2;
1484            }
1485    
1486            if (labelY > plotArea.getCenterY()) {
1487                labelY += ascent;
1488            }
1489    
1490            return new Point2D.Double(labelX, labelY);
1491        }
1492    
1493        /**
1494         * Tests this plot for equality with an arbitrary object.
1495         *
1496         * @param obj  the object (<code>null</code> permitted).
1497         *
1498         * @return A boolean.
1499         */
1500        public boolean equals(Object obj) {
1501            if (obj == this) {
1502                return true;
1503            }
1504            if (!(obj instanceof SpiderWebPlot)) {
1505                return false;
1506            }
1507            if (!super.equals(obj)) {
1508                return false;
1509            }
1510            SpiderWebPlot that = (SpiderWebPlot) obj;
1511            if (!this.dataExtractOrder.equals(that.dataExtractOrder)) {
1512                return false;
1513            }
1514            if (this.headPercent != that.headPercent) {
1515                return false;
1516            }
1517            if (this.interiorGap != that.interiorGap) {
1518                return false;
1519            }
1520            if (this.startAngle != that.startAngle) {
1521                return false;
1522            }
1523            if (!this.direction.equals(that.direction)) {
1524                return false;
1525            }
1526            if (this.maxValue != that.maxValue) {
1527                return false;
1528            }
1529            if (this.webFilled != that.webFilled) {
1530                return false;
1531            }
1532            if (this.axisLabelGap != that.axisLabelGap) {
1533                return false;
1534            }
1535            if (!PaintUtilities.equal(this.axisLinePaint, that.axisLinePaint)) {
1536                return false;
1537            }
1538            if (!this.axisLineStroke.equals(that.axisLineStroke)) {
1539                return false;
1540            }
1541            if (!ShapeUtilities.equal(this.legendItemShape, that.legendItemShape)) {
1542                return false;
1543            }
1544            if (!PaintUtilities.equal(this.seriesPaint, that.seriesPaint)) {
1545                return false;
1546            }
1547            if (!this.seriesPaintList.equals(that.seriesPaintList)) {
1548                return false;
1549            }
1550            if (!PaintUtilities.equal(this.baseSeriesPaint, that.baseSeriesPaint)) {
1551                return false;
1552            }
1553            if (!PaintUtilities.equal(this.seriesOutlinePaint,
1554                    that.seriesOutlinePaint)) {
1555                return false;
1556            }
1557            if (!this.seriesOutlinePaintList.equals(that.seriesOutlinePaintList)) {
1558                return false;
1559            }
1560            if (!PaintUtilities.equal(this.baseSeriesOutlinePaint,
1561                    that.baseSeriesOutlinePaint)) {
1562                return false;
1563            }
1564            if (!ObjectUtilities.equal(this.seriesOutlineStroke,
1565                    that.seriesOutlineStroke)) {
1566                return false;
1567            }
1568            if (!this.seriesOutlineStrokeList.equals(
1569                    that.seriesOutlineStrokeList)) {
1570                return false;
1571            }
1572            if (!this.baseSeriesOutlineStroke.equals(
1573                    that.baseSeriesOutlineStroke)) {
1574                return false;
1575            }
1576            if (!this.labelFont.equals(that.labelFont)) {
1577                return false;
1578            }
1579            if (!PaintUtilities.equal(this.labelPaint, that.labelPaint)) {
1580                return false;
1581            }
1582            if (!this.labelGenerator.equals(that.labelGenerator)) {
1583                return false;
1584            }
1585            if (!ObjectUtilities.equal(this.toolTipGenerator,
1586                    that.toolTipGenerator)) {
1587                return false;
1588            }
1589            if (!ObjectUtilities.equal(this.urlGenerator,
1590                    that.urlGenerator)) {
1591                return false;
1592            }
1593            return true;
1594        }
1595    
1596        /**
1597         * Returns a clone of this plot.
1598         *
1599         * @return A clone of this plot.
1600         *
1601         * @throws CloneNotSupportedException if the plot cannot be cloned for
1602         *         any reason.
1603         */
1604        public Object clone() throws CloneNotSupportedException {
1605            SpiderWebPlot clone = (SpiderWebPlot) super.clone();
1606            clone.legendItemShape = ShapeUtilities.clone(this.legendItemShape);
1607            clone.seriesPaintList = (PaintList) this.seriesPaintList.clone();
1608            clone.seriesOutlinePaintList
1609                    = (PaintList) this.seriesOutlinePaintList.clone();
1610            clone.seriesOutlineStrokeList
1611                    = (StrokeList) this.seriesOutlineStrokeList.clone();
1612            return clone;
1613        }
1614    
1615        /**
1616         * Provides serialization support.
1617         *
1618         * @param stream  the output stream.
1619         *
1620         * @throws IOException  if there is an I/O error.
1621         */
1622        private void writeObject(ObjectOutputStream stream) throws IOException {
1623            stream.defaultWriteObject();
1624    
1625            SerialUtilities.writeShape(this.legendItemShape, stream);
1626            SerialUtilities.writePaint(this.seriesPaint, stream);
1627            SerialUtilities.writePaint(this.baseSeriesPaint, stream);
1628            SerialUtilities.writePaint(this.seriesOutlinePaint, stream);
1629            SerialUtilities.writePaint(this.baseSeriesOutlinePaint, stream);
1630            SerialUtilities.writeStroke(this.seriesOutlineStroke, stream);
1631            SerialUtilities.writeStroke(this.baseSeriesOutlineStroke, stream);
1632            SerialUtilities.writePaint(this.labelPaint, stream);
1633            SerialUtilities.writePaint(this.axisLinePaint, stream);
1634            SerialUtilities.writeStroke(this.axisLineStroke, stream);
1635        }
1636    
1637        /**
1638         * Provides serialization support.
1639         *
1640         * @param stream  the input stream.
1641         *
1642         * @throws IOException  if there is an I/O error.
1643         * @throws ClassNotFoundException  if there is a classpath problem.
1644         */
1645        private void readObject(ObjectInputStream stream) throws IOException,
1646                ClassNotFoundException {
1647            stream.defaultReadObject();
1648    
1649            this.legendItemShape = SerialUtilities.readShape(stream);
1650            this.seriesPaint = SerialUtilities.readPaint(stream);
1651            this.baseSeriesPaint = SerialUtilities.readPaint(stream);
1652            this.seriesOutlinePaint = SerialUtilities.readPaint(stream);
1653            this.baseSeriesOutlinePaint = SerialUtilities.readPaint(stream);
1654            this.seriesOutlineStroke = SerialUtilities.readStroke(stream);
1655            this.baseSeriesOutlineStroke = SerialUtilities.readStroke(stream);
1656            this.labelPaint = SerialUtilities.readPaint(stream);
1657            this.axisLinePaint = SerialUtilities.readPaint(stream);
1658            this.axisLineStroke = SerialUtilities.readStroke(stream);
1659            if (this.dataset != null) {
1660                this.dataset.addChangeListener(this);
1661            }
1662        }
1663    
1664    }