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