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    
068    package org.jfree.chart.plot;
069    
070    import java.awt.AlphaComposite;
071    import java.awt.BasicStroke;
072    import java.awt.Color;
073    import java.awt.Composite;
074    import java.awt.Font;
075    import java.awt.Graphics2D;
076    import java.awt.Paint;
077    import java.awt.Polygon;
078    import java.awt.Rectangle;
079    import java.awt.Shape;
080    import java.awt.Stroke;
081    import java.awt.font.FontRenderContext;
082    import java.awt.font.LineMetrics;
083    import java.awt.geom.Arc2D;
084    import java.awt.geom.Ellipse2D;
085    import java.awt.geom.Line2D;
086    import java.awt.geom.Point2D;
087    import java.awt.geom.Rectangle2D;
088    import java.io.IOException;
089    import java.io.ObjectInputStream;
090    import java.io.ObjectOutputStream;
091    import java.io.Serializable;
092    import java.util.Iterator;
093    import java.util.List;
094    
095    import org.jfree.chart.LegendItem;
096    import org.jfree.chart.LegendItemCollection;
097    import org.jfree.chart.entity.CategoryItemEntity;
098    import org.jfree.chart.entity.EntityCollection;
099    import org.jfree.chart.event.PlotChangeEvent;
100    import org.jfree.chart.labels.CategoryItemLabelGenerator;
101    import org.jfree.chart.labels.CategoryToolTipGenerator;
102    import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
103    import org.jfree.chart.urls.CategoryURLGenerator;
104    import org.jfree.data.category.CategoryDataset;
105    import org.jfree.data.general.DatasetChangeEvent;
106    import org.jfree.data.general.DatasetUtilities;
107    import org.jfree.io.SerialUtilities;
108    import org.jfree.ui.RectangleInsets;
109    import org.jfree.util.ObjectUtilities;
110    import org.jfree.util.PaintList;
111    import org.jfree.util.PaintUtilities;
112    import org.jfree.util.Rotation;
113    import org.jfree.util.ShapeUtilities;
114    import org.jfree.util.StrokeList;
115    import 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     */
122    public 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    }