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