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