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