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