    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    }