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