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