    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     * 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     *
    066     */
    067
    068    package org.jfree.chart.plot;
    069
    070    import java.awt.AlphaComposite;
    071    import java.awt.BasicStroke;
    072    import java.awt.Color;
    073    import java.awt.Composite;
    074    import java.awt.Font;
    075    import java.awt.Graphics2D;
    076    import java.awt.Paint;
    077    import java.awt.Polygon;
    078    import java.awt.Rectangle;
    079    import java.awt.Shape;
    080    import java.awt.Stroke;
    081    import java.awt.font.FontRenderContext;
    082    import java.awt.font.LineMetrics;
    083    import java.awt.geom.Arc2D;
    084    import java.awt.geom.Ellipse2D;
    085    import java.awt.geom.Line2D;
    086    import java.awt.geom.Point2D;
    087    import java.awt.geom.Rectangle2D;
    088    import java.io.IOException;
    089    import java.io.ObjectInputStream;
    090    import java.io.ObjectOutputStream;
    091    import java.io.Serializable;
    092    import java.util.Iterator;
    093    import java.util.List;
    094
    095    import org.jfree.chart.LegendItem;
    096    import org.jfree.chart.LegendItemCollection;
    097    import org.jfree.chart.entity.CategoryItemEntity;
    098    import org.jfree.chart.entity.EntityCollection;
    099    import org.jfree.chart.event.PlotChangeEvent;
    100    import org.jfree.chart.labels.CategoryItemLabelGenerator;
    101    import org.jfree.chart.labels.CategoryToolTipGenerator;
    102    import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
    103    import org.jfree.chart.urls.CategoryURLGenerator;
    104    import org.jfree.data.category.CategoryDataset;
    105    import org.jfree.data.general.DatasetChangeEvent;
    106    import org.jfree.data.general.DatasetUtilities;
    107    import org.jfree.io.SerialUtilities;
    108    import org.jfree.ui.RectangleInsets;
    109    import org.jfree.util.ObjectUtilities;
    110    import org.jfree.util.PaintList;
    111    import org.jfree.util.PaintUtilities;
    112    import org.jfree.util.Rotation;
    113    import org.jfree.util.ShapeUtilities;
    114    import org.jfree.util.StrokeList;
    115    import org.jfree.util.TableOrder;
    116
    117    /**
    118     * A plot that displays data from a {@link CategoryDataset} in the form of a
    119     * "spider web".  Multiple series can be plotted on the same axis to allow
    120     * easy comparison.  This plot doesn't support negative values at present.
    121     */
    122    public class SpiderWebPlot extends Plot implements Cloneable, Serializable {
    123
    124        /** For serialization. */
    125        private static final long serialVersionUID = -5376340422031599463L;
    126
    127        /** The default head radius percent (currently 1%). */
    128        public static final double DEFAULT_HEAD = 0.01;
    129
    130        /** The default axis label gap (currently 10%). */
    131        public static final double DEFAULT_AXIS_LABEL_GAP = 0.10;
    132
    133        /** The default interior gap. */
    134        public static final double DEFAULT_INTERIOR_GAP = 0.25;
    135
    136        /** The maximum interior gap (currently 40%). */
    137        public static final double MAX_INTERIOR_GAP = 0.40;
    138
    139        /** The default starting angle for the radar chart axes. */
    140        public static final double DEFAULT_START_ANGLE = 90.0;
    141
    142        /** The default series label font. */
    143        public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif",
    144                Font.PLAIN, 10);
    145
    146        /** The default series label paint. */
    147        public static final Paint  DEFAULT_LABEL_PAINT = Color.black;
    148
    149        /** The default series label background paint. */
    150        public static final Paint  DEFAULT_LABEL_BACKGROUND_PAINT
    151                = new Color(255, 255, 192);
    152
    153        /** The default series label outline paint. */
    154        public static final Paint  DEFAULT_LABEL_OUTLINE_PAINT = Color.black;
    155
    156        /** The default series label outline stroke. */
    157        public static final Stroke DEFAULT_LABEL_OUTLINE_STROKE
    158                = new BasicStroke(0.5f);
    159
    160        /** The default series label shadow paint. */
    161        public static final Paint  DEFAULT_LABEL_SHADOW_PAINT = Color.lightGray;
    162
    163        /**
    164         * The default maximum value plotted - forces the plot to evaluate
    165         *  the maximum from the data passed in
    166         */
    167        public static final double DEFAULT_MAX_VALUE = -1.0;
    168
    169        /** The head radius as a percentage of the available drawing area. */
    170        protected double headPercent;
    171
    172        /** The space left around the outside of the plot as a percentage. */
    173        private double interiorGap;
    174
    175        /** The gap between the labels and the axes as a %age of the radius. */
    176        private double axisLabelGap;
    177
    178        /**
    179         * The paint used to draw the axis lines.
    180         *
    181         * @since 1.0.4
    182         */
    183        private transient Paint axisLinePaint;
    184
    185        /**
    186         * The stroke used to draw the axis lines.
    187         *
    188         * @since 1.0.4
    189         */
    190        private transient Stroke axisLineStroke;
    191
    192        /** The dataset. */
    193        private CategoryDataset dataset;
    194
    195        /** The maximum value we are plotting against on each category axis */
    196        private double maxValue;
    197
    198        /**
    199         * The data extract order (BY_ROW or BY_COLUMN). This denotes whether
    200         * the data series are stored in rows (in which case the category names are
    201         * derived from the column keys) or in columns (in which case the category
    202         * names are derived from the row keys).
    203         */
    204        private TableOrder dataExtractOrder;
    205
    206        /** The starting angle. */
    207        private double startAngle;
    208
    209        /** The direction for drawing the radar axis & plots. */
    210        private Rotation direction;
    211
    212        /** The legend item shape. */
    213        private transient Shape legendItemShape;
    214
    215        /** The paint for ALL series (overrides list). */
    216        private transient Paint seriesPaint;
    217
    218        /** The series paint list. */
    219        private PaintList seriesPaintList;
    220
    221        /** The base series paint (fallback). */
    222        private transient Paint baseSeriesPaint;
    223
    224        /** The outline paint for ALL series (overrides list). */
    225        private transient Paint seriesOutlinePaint;
    226
    227        /** The series outline paint list. */
    228        private PaintList seriesOutlinePaintList;
    229
    230        /** The base series outline paint (fallback). */
    231        private transient Paint baseSeriesOutlinePaint;
    232
    233        /** The outline stroke for ALL series (overrides list). */
    234        private transient Stroke seriesOutlineStroke;
    235
    236        /** The series outline stroke list. */
    237        private StrokeList seriesOutlineStrokeList;
    238
    239        /** The base series outline stroke (fallback). */
    240        private transient Stroke baseSeriesOutlineStroke;
    241
    242        /** The font used to display the category labels. */
    243        private Font labelFont;
    244
    245        /** The color used to draw the category labels. */
    246        private transient Paint labelPaint;
    247
    248        /** The label generator. */
    249        private CategoryItemLabelGenerator labelGenerator;
    250
    251        /** controls if the web polygons are filled or not */
    252        private boolean webFilled = true;
    253
    254        /** A tooltip generator for the plot (<code>null</code> permitted). */
    255        private CategoryToolTipGenerator toolTipGenerator;
    256
    257        /** A URL generator for the plot (<code>null</code> permitted). */
    258        private CategoryURLGenerator urlGenerator;
    259
    260        /**
    261         * Creates a default plot with no dataset.
    262         */
    263        public SpiderWebPlot() {
    264            this(null);
    265        }
    266
    267        /**
    268         * Creates a new spider web plot with the given dataset, with each row
    269         * representing a series.
    270         *
    271         * @param dataset  the dataset (<code>null</code> permitted).
    272         */
    273        public SpiderWebPlot(CategoryDataset dataset) {
    274            this(dataset, TableOrder.BY_ROW);
    275        }
    276
    277        /**
    278         * Creates a new spider web plot with the given dataset.
    279         *
    280         * @param dataset  the dataset.
    281         * @param extract  controls how data is extracted ({@link TableOrder#BY_ROW}
    282         *                 or {@link TableOrder#BY_COLUMN}).
    283         */
    284        public SpiderWebPlot(CategoryDataset dataset, TableOrder extract) {
    285            super();
    286            if (extract == null) {
    287                throw new IllegalArgumentException("Null 'extract' argument.");
    288            }
    289            this.dataset = dataset;
    290            if (dataset != null) {
    291                dataset.addChangeListener(this);
    292            }
    293
    294            this.dataExtractOrder = extract;
    295            this.headPercent = DEFAULT_HEAD;
    296            this.axisLabelGap = DEFAULT_AXIS_LABEL_GAP;
    297            this.axisLinePaint = Color.black;
    298            this.axisLineStroke = new BasicStroke(1.0f);
    299
    300            this.interiorGap = DEFAULT_INTERIOR_GAP;
    301            this.startAngle = DEFAULT_START_ANGLE;
    302            this.direction = Rotation.CLOCKWISE;
    303            this.maxValue = DEFAULT_MAX_VALUE;
    304
    305            this.seriesPaint = null;
    306            this.seriesPaintList = new PaintList();
    307            this.baseSeriesPaint = null;
    308
    309            this.seriesOutlinePaint = null;
    310            this.seriesOutlinePaintList = new PaintList();
    311            this.baseSeriesOutlinePaint = DEFAULT_OUTLINE_PAINT;
    312
    313            this.seriesOutlineStroke = null;
    314            this.seriesOutlineStrokeList = new StrokeList();
    315            this.baseSeriesOutlineStroke = DEFAULT_OUTLINE_STROKE;
    316
    317            this.labelFont = DEFAULT_LABEL_FONT;
    318            this.labelPaint = DEFAULT_LABEL_PAINT;
    319            this.labelGenerator = new StandardCategoryItemLabelGenerator();
    320
    321            this.legendItemShape = DEFAULT_LEGEND_ITEM_CIRCLE;
    322        }
    323
    324        /**
    325         * Returns a short string describing the type of plot.
    326         *
    327         * @return The plot type.
    328         */
    329        public String getPlotType() {
    330            // return localizationResources.getString("Radar_Plot");
    331            return ("Spider Web Plot");
    332        }
    333
    334        /**
    335         * Returns the dataset.
    336         *
    337         * @return The dataset (possibly <code>null</code>).
    338         *
    339         * @see #setDataset(CategoryDataset)
    340         */
    341        public CategoryDataset getDataset() {
    342            return this.dataset;
    343        }
    344
    345        /**
    346         * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
    347         * to all registered listeners.
    348         *
    349         * @param dataset  the dataset (<code>null</code> permitted).
    350         *
    351         * @see #getDataset()
    352         */
    353        public void setDataset(CategoryDataset dataset) {
    354            // if there is an existing dataset, remove the plot from the list of
    355            // change listeners...
    356            if (this.dataset != null) {
    357                this.dataset.removeChangeListener(this);
    358            }
    359
    360            // set the new dataset, and register the chart as a change listener...
    361            this.dataset = dataset;
    362            if (dataset != null) {
    363                setDatasetGroup(dataset.getGroup());
    364                dataset.addChangeListener(this);
    365            }
    366
    367            // send a dataset change event to self to trigger plot change event
    368            datasetChanged(new DatasetChangeEvent(this, dataset));
    369        }
    370
    371        /**
    372         * Method to determine if the web chart is to be filled.
    373         *
    374         * @return A boolean.
    375         *
    376         * @see #setWebFilled(boolean)
    377         */
    378        public boolean isWebFilled() {
    379            return this.webFilled;
    380        }
    381
    382        /**
    383         * Sets the webFilled flag and sends a {@link PlotChangeEvent} to all
    384         * registered listeners.
    385         *
    386         * @param flag  the flag.
    387         *
    388         * @see #isWebFilled()
    389         */
    390        public void setWebFilled(boolean flag) {
    391            this.webFilled = flag;
    392            notifyListeners(new PlotChangeEvent(this));
    393        }
    394
    395        /**
    396         * Returns the data extract order (by row or by column).
    397         *
    398         * @return The data extract order (never <code>null</code>).
    399         *
    400         * @see #setDataExtractOrder(TableOrder)
    401         */
    402        public TableOrder getDataExtractOrder() {
    403            return this.dataExtractOrder;
    404        }
    405
    406        /**
    407         * Sets the data extract order (by row or by column) and sends a
    408         * {@link PlotChangeEvent}to all registered listeners.
    409         *
    410         * @param order the order (<code>null</code> not permitted).
    411         *
    412         * @throws IllegalArgumentException if <code>order</code> is
    413         *     <code>null</code>.
    414         *
    415         * @see #getDataExtractOrder()
    416         */
    417        public void setDataExtractOrder(TableOrder order) {
    418            if (order == null) {
    419                throw new IllegalArgumentException("Null 'order' argument");
    420            }
    421            this.dataExtractOrder = order;
    422            notifyListeners(new PlotChangeEvent(this));
    423        }
    424
    425        /**
    426         * Returns the head percent.
    427         *
    428         * @return The head percent.
    429         *
    430         * @see #setHeadPercent(double)
    431         */
    432        public double getHeadPercent() {
    433            return this.headPercent;
    434        }
    435
    436        /**
    437         * Sets the head percent and sends a {@link PlotChangeEvent} to all
    438         * registered listeners.
    439         *
    440         * @param percent  the percent.
    441         *
    442         * @see #getHeadPercent()
    443         */
    444        public void setHeadPercent(double percent) {
    445            this.headPercent = percent;
    446            notifyListeners(new PlotChangeEvent(this));
    447        }
    448
    449        /**
    450         * Returns the start angle for the first radar axis.
    451         * <BR>
    452         * This is measured in degrees starting from 3 o'clock (Java Arc2D default)
    453         * and measuring anti-clockwise.
    454         *
    455         * @return The start angle.
    456         *
    457         * @see #setStartAngle(double)
    458         */
    459        public double getStartAngle() {
    460            return this.startAngle;
    461        }
    462
    463        /**
    464         * Sets the starting angle and sends a {@link PlotChangeEvent} to all
    465         * registered listeners.
    466         * <P>
    467         * The initial default value is 90 degrees, which corresponds to 12 o'clock.
    468         * A value of zero corresponds to 3 o'clock... this is the encoding used by
    469         * Java's Arc2D class.
    470         *
    471         * @param angle  the angle (in degrees).
    472         *
    473         * @see #getStartAngle()
    474         */
    475        public void setStartAngle(double angle) {
    476            this.startAngle = angle;
    477            notifyListeners(new PlotChangeEvent(this));
    478        }
    479
    480        /**
    481         * Returns the maximum value any category axis can take.
    482         *
    483         * @return The maximum value.
    484         *
    485         * @see #setMaxValue(double)
    486         */
    487        public double getMaxValue() {
    488            return this.maxValue;
    489        }
    490
    491        /**
    492         * Sets the maximum value any category axis can take and sends
    493         * a {@link PlotChangeEvent} to all registered listeners.
    494         *
    495         * @param value  the maximum value.
    496         *
    497         * @see #getMaxValue()
    498         */
    499        public void setMaxValue(double value) {
    500            this.maxValue = value;
    501            notifyListeners(new PlotChangeEvent(this));
    502        }
    503
    504        /**
    505         * Returns the direction in which the radar axes are drawn
    506         * (clockwise or anti-clockwise).
    507         *
    508         * @return The direction (never <code>null</code>).
    509         *
    510         * @see #setDirection(Rotation)
    511         */
    512        public Rotation getDirection() {
    513            return this.direction;
    514        }
    515
    516        /**
    517         * Sets the direction in which the radar axes are drawn and sends a
    518         * {@link PlotChangeEvent} to all registered listeners.
    519         *
    520         * @param direction  the direction (<code>null</code> not permitted).
    521         *
    522         * @see #getDirection()
    523         */
    524        public void setDirection(Rotation direction) {
    525            if (direction == null) {
    526                throw new IllegalArgumentException("Null 'direction' argument.");
    527            }
    528            this.direction = direction;
    529            notifyListeners(new PlotChangeEvent(this));
    530        }
    531
    532        /**
    533         * Returns the interior gap, measured as a percentage of the available
    534         * drawing space.
    535         *
    536         * @return The gap (as a percentage of the available drawing space).
    537         *
    538         * @see #setInteriorGap(double)
    539         */
    540        public double getInteriorGap() {
    541            return this.interiorGap;
    542        }
    543
    544        /**
    545         * Sets the interior gap and sends a {@link PlotChangeEvent} to all
    546         * registered listeners. This controls the space between the edges of the
    547         * plot and the plot area itself (the region where the axis labels appear).
    548         *
    549         * @param percent  the gap (as a percentage of the available drawing space).
    550         *
    551         * @see #getInteriorGap()
    552         */
    553        public void setInteriorGap(double percent) {
    554            if ((percent < 0.0) || (percent > MAX_INTERIOR_GAP)) {
    555                throw new IllegalArgumentException(
    556                        "Percentage outside valid range.");
    557            }
    558            if (this.interiorGap != percent) {
    559                this.interiorGap = percent;
    560                notifyListeners(new PlotChangeEvent(this));
    561            }
    562        }
    563
    564        /**
    565         * Returns the axis label gap.
    566         *
    567         * @return The axis label gap.
    568         *
    569         * @see #setAxisLabelGap(double)
    570         */
    571        public double getAxisLabelGap() {
    572            return this.axisLabelGap;
    573        }
    574
    575        /**
    576         * Sets the axis label gap and sends a {@link PlotChangeEvent} to all
    577         * registered listeners.
    578         *
    579         * @param gap  the gap.
    580         *
    581         * @see #getAxisLabelGap()
    582         */
    583        public void setAxisLabelGap(double gap) {
    584            this.axisLabelGap = gap;
    585            notifyListeners(new PlotChangeEvent(this));
    586        }
    587
    588        /**
    589         * Returns the paint used to draw the axis lines.
    590         *
    591         * @return The paint used to draw the axis lines (never <code>null</code>).
    592         *
    593         * @see #setAxisLinePaint(Paint)
    594         * @see #getAxisLineStroke()
    595         * @since 1.0.4
    596         */
    597        public Paint getAxisLinePaint() {
    598            return this.axisLinePaint;
    599        }
    600
    601        /**
    602         * Sets the paint used to draw the axis lines and sends a
    603         * {@link PlotChangeEvent} to all registered listeners.
    604         *
    605         * @param paint  the paint (<code>null</code> not permitted).
    606         *
    607         * @see #getAxisLinePaint()
    608         * @since 1.0.4
    609         */
    610        public void setAxisLinePaint(Paint paint) {
    611            if (paint == null) {
    612                throw new IllegalArgumentException("Null 'paint' argument.");
    613            }
    614            this.axisLinePaint = paint;
    615            notifyListeners(new PlotChangeEvent(this));
    616        }
    617
    618        /**
    619         * Returns the stroke used to draw the axis lines.
    620         *
    621         * @return The stroke used to draw the axis lines (never <code>null</code>).
    622         *
    623         * @see #setAxisLineStroke(Stroke)
    624         * @see #getAxisLinePaint()
    625         * @since 1.0.4
    626         */
    627        public Stroke getAxisLineStroke() {
    628            return this.axisLineStroke;
    629        }
    630
    631        /**
    632         * Sets the stroke used to draw the axis lines and sends a
    633         * {@link PlotChangeEvent} to all registered listeners.
    634         *
    635         * @param stroke  the stroke (<code>null</code> not permitted).
    636         *
    637         * @see #getAxisLineStroke()
    638         * @since 1.0.4
    639         */
    640        public void setAxisLineStroke(Stroke stroke) {
    641            if (stroke == null) {
    642                throw new IllegalArgumentException("Null 'stroke' argument.");
    643            }
    644            this.axisLineStroke = stroke;
    645            notifyListeners(new PlotChangeEvent(this));
    646        }
    647
    648        //// SERIES PAINT /////////////////////////
    649
    650        /**
    651         * Returns the paint for ALL series in the plot.
    652         *
    653         * @return The paint (possibly <code>null</code>).
    654         *
    655         * @see #setSeriesPaint(Paint)
    656         */
    657        public Paint getSeriesPaint() {
    658            return this.seriesPaint;
    659        }
    660
    661        /**
    662         * Sets the paint for ALL series in the plot. If this is set to</code> null
    663         * </code>, then a list of paints is used instead (to allow different colors
    664         * to be used for each series of the radar group).
    665         *
    666         * @param paint the paint (<code>null</code> permitted).
    667         *
    668         * @see #getSeriesPaint()
    669         */
    670        public void setSeriesPaint(Paint paint) {
    671            this.seriesPaint = paint;
    672            notifyListeners(new PlotChangeEvent(this));
    673        }
    674
    675        /**
    676         * Returns the paint for the specified series.
    677         *
    678         * @param series  the series index (zero-based).
    679         *
    680         * @return The paint (never <code>null</code>).
    681         *
    682         * @see #setSeriesPaint(int, Paint)
    683         */
    684        public Paint getSeriesPaint(int series) {
    685
    686            // return the override, if there is one...
    687            if (this.seriesPaint != null) {
    688                return this.seriesPaint;
    689            }
    690
    691            // otherwise look up the paint list
    692            Paint result = this.seriesPaintList.getPaint(series);
    693            if (result == null) {
    694                DrawingSupplier supplier = getDrawingSupplier();
    695                if (supplier != null) {
    696                    Paint p = supplier.getNextPaint();
    697                    this.seriesPaintList.setPaint(series, p);
    698                    result = p;
    699                }
    700                else {
    701                    result = this.baseSeriesPaint;
    702                }
    703            }
    704            return result;
    705
    706        }
    707
    708        /**
    709         * Sets the paint used to fill a series of the radar and sends a
    710         * {@link PlotChangeEvent} to all registered listeners.
    711         *
    712         * @param series  the series index (zero-based).
    713         * @param paint  the paint (<code>null</code> permitted).
    714         *
    715         * @see #getSeriesPaint(int)
    716         */
    717        public void setSeriesPaint(int series, Paint paint) {
    718            this.seriesPaintList.setPaint(series, paint);
    719            notifyListeners(new PlotChangeEvent(this));
    720        }
    721
    722        /**
    723         * Returns the base series paint. This is used when no other paint is
    724         * available.
    725         *
    726         * @return The paint (never <code>null</code>).
    727         *
    728         * @see #setBaseSeriesPaint(Paint)
    729         */
    730        public Paint getBaseSeriesPaint() {
    731          return this.baseSeriesPaint;
    732        }
    733
    734        /**
    735         * Sets the base series paint.
    736         *
    737         * @param paint  the paint (<code>null</code> not permitted).
    738         *
    739         * @see #getBaseSeriesPaint()
    740         */
    741        public void setBaseSeriesPaint(Paint paint) {
    742            if (paint == null) {
    743                throw new IllegalArgumentException("Null 'paint' argument.");
    744            }
    745            this.baseSeriesPaint = paint;
    746            notifyListeners(new PlotChangeEvent(this));
    747        }
    748
    749        //// SERIES OUTLINE PAINT ////////////////////////////
    750
    751        /**
    752         * Returns the outline paint for ALL series in the plot.
    753         *
    754         * @return The paint (possibly <code>null</code>).
    755         */
    756        public Paint getSeriesOutlinePaint() {
    757            return this.seriesOutlinePaint;
    758        }
    759
    760        /**
    761         * Sets the outline paint for ALL series in the plot. If this is set to
    762         * </code> null</code>, then a list of paints is used instead (to allow
    763         * different colors to be used for each series).
    764         *
    765         * @param paint  the paint (<code>null</code> permitted).
    766         */
    767        public void setSeriesOutlinePaint(Paint paint) {
    768            this.seriesOutlinePaint = paint;
    769            notifyListeners(new PlotChangeEvent(this));
    770        }
    771
    772        /**
    773         * Returns the paint for the specified series.
    774         *
    775         * @param series  the series index (zero-based).
    776         *
    777         * @return The paint (never <code>null</code>).
    778         */
    779        public Paint getSeriesOutlinePaint(int series) {
    780            // return the override, if there is one...
    781            if (this.seriesOutlinePaint != null) {
    782                return this.seriesOutlinePaint;
    783            }
    784            // otherwise look up the paint list
    785            Paint result = this.seriesOutlinePaintList.getPaint(series);
    786            if (result == null) {
    787                result = this.baseSeriesOutlinePaint;
    788            }
    789            return result;
    790        }
    791
    792        /**
    793         * Sets the paint used to fill a series of the radar and sends a
    794         * {@link PlotChangeEvent} to all registered listeners.
    795         *
    796         * @param series  the series index (zero-based).
    797         * @param paint  the paint (<code>null</code> permitted).
    798         */
    799        public void setSeriesOutlinePaint(int series, Paint paint) {
    800            this.seriesOutlinePaintList.setPaint(series, paint);
    801            notifyListeners(new PlotChangeEvent(this));
    802        }
    803
    804        /**
    805         * Returns the base series paint. This is used when no other paint is
    806         * available.
    807         *
    808         * @return The paint (never <code>null</code>).
    809         */
    810        public Paint getBaseSeriesOutlinePaint() {
    811            return this.baseSeriesOutlinePaint;
    812        }
    813
    814        /**
    815         * Sets the base series paint.
    816         *
    817         * @param paint  the paint (<code>null</code> not permitted).
    818         */
    819        public void setBaseSeriesOutlinePaint(Paint paint) {
    820            if (paint == null) {
    821                throw new IllegalArgumentException("Null 'paint' argument.");
    822            }
    823            this.baseSeriesOutlinePaint = paint;
    824            notifyListeners(new PlotChangeEvent(this));
    825        }
    826
    827        //// SERIES OUTLINE STROKE /////////////////////
    828
    829        /**
    830         * Returns the outline stroke for ALL series in the plot.
    831         *
    832         * @return The stroke (possibly <code>null</code>).
    833         */
    834        public Stroke getSeriesOutlineStroke() {
    835            return this.seriesOutlineStroke;
    836        }
    837
    838        /**
    839         * Sets the outline stroke for ALL series in the plot. If this is set to
    840         * </code> null</code>, then a list of paints is used instead (to allow
    841         * different colors to be used for each series).
    842         *
    843         * @param stroke  the stroke (<code>null</code> permitted).
    844         */
    845        public void setSeriesOutlineStroke(Stroke stroke) {
    846            this.seriesOutlineStroke = stroke;
    847            notifyListeners(new PlotChangeEvent(this));
    848        }
    849
    850        /**
    851         * Returns the stroke for the specified series.
    852         *
    853         * @param series  the series index (zero-based).
    854         *
    855         * @return The stroke (never <code>null</code>).
    856         */
    857        public Stroke getSeriesOutlineStroke(int series) {
    858
    859            // return the override, if there is one...
    860            if (this.seriesOutlineStroke != null) {
    861                return this.seriesOutlineStroke;
    862            }
    863
    864            // otherwise look up the paint list
    865            Stroke result = this.seriesOutlineStrokeList.getStroke(series);
    866            if (result == null) {
    867                result = this.baseSeriesOutlineStroke;
    868            }
    869            return result;
    870
    871        }
    872
    873        /**
    874         * Sets the stroke used to fill a series of the radar and sends a
    875         * {@link PlotChangeEvent} to all registered listeners.
    876         *
    877         * @param series  the series index (zero-based).
    878         * @param stroke  the stroke (<code>null</code> permitted).
    879         */
    880        public void setSeriesOutlineStroke(int series, Stroke stroke) {
    881            this.seriesOutlineStrokeList.setStroke(series, stroke);
    882            notifyListeners(new PlotChangeEvent(this));
    883        }
    884
    885        /**
    886         * Returns the base series stroke. This is used when no other stroke is
    887         * available.
    888         *
    889         * @return The stroke (never <code>null</code>).
    890         */
    891        public Stroke getBaseSeriesOutlineStroke() {
    892            return this.baseSeriesOutlineStroke;
    893        }
    894
    895        /**
    896         * Sets the base series stroke.
    897         *
    898         * @param stroke  the stroke (<code>null</code> not permitted).
    899         */
    900        public void setBaseSeriesOutlineStroke(Stroke stroke) {
    901            if (stroke == null) {
    902                throw new IllegalArgumentException("Null 'stroke' argument.");
    903            }
    904            this.baseSeriesOutlineStroke = stroke;
    905            notifyListeners(new PlotChangeEvent(this));
    906        }
    907
    908        /**
    909         * Returns the shape used for legend items.
    910         *
    911         * @return The shape (never <code>null</code>).
    912         *
    913         * @see #setLegendItemShape(Shape)
    914         */
    915        public Shape getLegendItemShape() {
    916            return this.legendItemShape;
    917        }
    918
    919        /**
    920         * Sets the shape used for legend items and sends a {@link PlotChangeEvent}
    921         * to all registered listeners.
    922         *
    923         * @param shape  the shape (<code>null</code> not permitted).
    924         *
    925         * @see #getLegendItemShape()
    926         */
    927        public void setLegendItemShape(Shape shape) {
    928            if (shape == null) {
    929                throw new IllegalArgumentException("Null 'shape' argument.");
    930            }
    931            this.legendItemShape = shape;
    932            notifyListeners(new PlotChangeEvent(this));
    933        }
    934
    935        /**
    936         * Returns the series label font.
    937         *
    938         * @return The font (never <code>null</code>).
    939         *
    940         * @see #setLabelFont(Font)
    941         */
    942        public Font getLabelFont() {
    943            return this.labelFont;
    944        }
    945
    946        /**
    947         * Sets the series label font and sends a {@link PlotChangeEvent} to all
    948         * registered listeners.
    949         *
    950         * @param font  the font (<code>null</code> not permitted).
    951         *
    952         * @see #getLabelFont()
    953         */
    954        public void setLabelFont(Font font) {
    955            if (font == null) {
    956                throw new IllegalArgumentException("Null 'font' argument.");
    957            }
    958            this.labelFont = font;
    959            notifyListeners(new PlotChangeEvent(this));
    960        }
    961
    962        /**
    963         * Returns the series label paint.
    964         *
    965         * @return The paint (never <code>null</code>).
    966         *
    967         * @see #setLabelPaint(Paint)
    968         */
    969        public Paint getLabelPaint() {
    970            return this.labelPaint;
    971        }
    972
    973        /**
    974         * Sets the series label paint and sends a {@link PlotChangeEvent} to all
    975         * registered listeners.
    976         *
    977         * @param paint  the paint (<code>null</code> not permitted).
    978         *
    979         * @see #getLabelPaint()
    980         */
    981        public void setLabelPaint(Paint paint) {
    982            if (paint == null) {
    983                throw new IllegalArgumentException("Null 'paint' argument.");
    984            }
    985            this.labelPaint = paint;
    986            notifyListeners(new PlotChangeEvent(this));
    987        }
    988
    989        /**
    990         * Returns the label generator.
    991         *
    992         * @return The label generator (never <code>null</code>).
    993         *
    994         * @see #setLabelGenerator(CategoryItemLabelGenerator)
    995         */
    996        public CategoryItemLabelGenerator getLabelGenerator() {
    997            return this.labelGenerator;
    998        }
    999
    1000        /**
    1001         * Sets the label generator and sends a {@link PlotChangeEvent} to all
    1002         * registered listeners.
    1003         *
    1004         * @param generator  the generator (<code>null</code> not permitted).
    1005         *
    1006         * @see #getLabelGenerator()
    1007         */
    1008        public void setLabelGenerator(CategoryItemLabelGenerator generator) {
    1009            if (generator == null) {
    1010                throw new IllegalArgumentException("Null 'generator' argument.");
    1011            }
    1012            this.labelGenerator = generator;
    1013        }
    1014
    1015        /**
    1016         * Returns the tool tip generator for the plot.
    1017         *
    1018         * @return The tool tip generator (possibly <code>null</code>).
    1019         *
    1020         * @see #setToolTipGenerator(CategoryToolTipGenerator)
    1021         *
    1022         * @since 1.0.2
    1023         */
    1024        public CategoryToolTipGenerator getToolTipGenerator() {
    1025            return this.toolTipGenerator;
    1026        }
    1027
    1028        /**
    1029         * Sets the tool tip generator for the plot and sends a
    1030         * {@link PlotChangeEvent} to all registered listeners.
    1031         *
    1032         * @param generator  the generator (<code>null</code> permitted).
    1033         *
    1034         * @see #getToolTipGenerator()
    1035         *
    1036         * @since 1.0.2
    1037         */
    1038        public void setToolTipGenerator(CategoryToolTipGenerator generator) {
    1039            this.toolTipGenerator = generator;
    1040            this.notifyListeners(new PlotChangeEvent(this));
    1041        }
    1042
    1043        /**
    1044         * Returns the URL generator for the plot.
    1045         *
    1046         * @return The URL generator (possibly <code>null</code>).
    1047         *
    1048         * @see #setURLGenerator(CategoryURLGenerator)
    1049         *
    1050         * @since 1.0.2
    1051         */
    1052        public CategoryURLGenerator getURLGenerator() {
    1053            return this.urlGenerator;
    1054        }
    1055
    1056        /**
    1057         * Sets the URL generator for the plot and sends a
    1058         * {@link PlotChangeEvent} to all registered listeners.
    1059         *
    1060         * @param generator  the generator (<code>null</code> permitted).
    1061         *
    1062         * @see #getURLGenerator()
    1063         *
    1064         * @since 1.0.2
    1065         */
    1066        public void setURLGenerator(CategoryURLGenerator generator) {
    1067            this.urlGenerator = generator;
    1068            this.notifyListeners(new PlotChangeEvent(this));
    1069        }
    1070
    1071        /**
    1072         * Returns a collection of legend items for the radar chart.
    1073         *
    1074         * @return The legend items.
    1075         */
    1076        public LegendItemCollection getLegendItems() {
    1077            LegendItemCollection result = new LegendItemCollection();
    1078
    1079            List keys = null;
    1080
    1081            if (this.dataExtractOrder == TableOrder.BY_ROW) {
    1082                keys = this.dataset.getRowKeys();
    1083            }
    1084            else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
    1085                keys = this.dataset.getColumnKeys();
    1086            }
    1087
    1088            if (keys != null) {
    1089                int series = 0;
    1090                Iterator iterator = keys.iterator();
    1091                Shape shape = getLegendItemShape();
    1092
    1093                while (iterator.hasNext()) {
    1094                    String label = iterator.next().toString();
    1095                    String description = label;
    1096
    1097                    Paint paint = getSeriesPaint(series);
    1098                    Paint outlinePaint = getSeriesOutlinePaint(series);
    1099                    Stroke stroke = getSeriesOutlineStroke(series);
    1100                    LegendItem item = new LegendItem(label, description,
    1101                            null, null, shape, paint, stroke, outlinePaint);
    1102                    item.setDataset(getDataset());
    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(
    1342                                    (int) (point.getX() - headW),
    1343                                    (int) (point.getY() - headH),
    1344                                    (int) (headW * 2), (int) (headH * 2));
    1345                            CategoryItemEntity entity = new CategoryItemEntity(
    1346                                    area, tip, url, this.dataset,
    1347                                    this.dataset.getRowKey(series),
    1348                                    this.dataset.getColumnKey(cat));
    1349                            entities.add(entity);
    1350                        }
    1351
    1352                    }
    1353                }
    1354            }
    1355            // Plot the polygon
    1356
    1357            Paint paint = getSeriesPaint(series);
    1358            g2.setPaint(paint);
    1359            g2.setStroke(getSeriesOutlineStroke(series));
    1360            g2.draw(polygon);
    1361
    1362            // Lastly, fill the web polygon if this is required
    1363
    1364            if (this.webFilled) {
    1365                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
    1366                        0.1f));
    1367                g2.fill(polygon);
    1368                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
    1369                        getForegroundAlpha()));
    1370            }
    1371        }
    1372
    1373        /**
    1374         * Returns the value to be plotted at the interseries of the
    1375         * series and the category.  This allows us to plot
    1376         * <code>BY_ROW</code> or <code>BY_COLUMN</code> which basically is just
    1377         * reversing the definition of the categories and data series being
    1378         * plotted.
    1379         *
    1380         * @param series the series to be plotted.
    1381         * @param cat the category within the series to be plotted.
    1382         *
    1383         * @return The value to be plotted (possibly <code>null</code>).
    1384         *
    1385         * @see #getDataExtractOrder()
    1386         */
    1387        protected Number getPlotValue(int series, int cat) {
    1388            Number value = null;
    1389            if (this.dataExtractOrder == TableOrder.BY_ROW) {
    1390                value = this.dataset.getValue(series, cat);
    1391            }
    1392            else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
    1393                value = this.dataset.getValue(cat, series);
    1394            }
    1395            return value;
    1396        }
    1397
    1398        /**
    1399         * Draws the label for one axis.
    1400         *
    1401         * @param g2  the graphics device.
    1402         * @param plotArea  the plot area
    1403         * @param value  the value of the label (ignored).
    1404         * @param cat  the category (zero-based index).
    1405         * @param startAngle  the starting angle.
    1406         * @param extent  the extent of the arc.
    1407         */
    1408        protected void drawLabel(Graphics2D g2, Rectangle2D plotArea, double value,
    1409                                 int cat, double startAngle, double extent) {
    1410            FontRenderContext frc = g2.getFontRenderContext();
    1411
    1412            String label = null;
    1413            if (this.dataExtractOrder == TableOrder.BY_ROW) {
    1414                // if series are in rows, then the categories are the column keys
    1415                label = this.labelGenerator.generateColumnLabel(this.dataset, cat);
    1416            }
    1417            else {
    1418                // if series are in columns, then the categories are the row keys
    1419                label = this.labelGenerator.generateRowLabel(this.dataset, cat);
    1420            }
    1421
    1422            Rectangle2D labelBounds = getLabelFont().getStringBounds(label, frc);
    1423            LineMetrics lm = getLabelFont().getLineMetrics(label, frc);
    1424            double ascent = lm.getAscent();
    1425
    1426            Point2D labelLocation = calculateLabelLocation(labelBounds, ascent,
    1427                    plotArea, startAngle);
    1428
    1429            Composite saveComposite = g2.getComposite();
    1430
    1431            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
    1432                    1.0f));
    1433            g2.setPaint(getLabelPaint());
    1434            g2.setFont(getLabelFont());
    1435            g2.drawString(label, (float) labelLocation.getX(),
    1436                    (float) labelLocation.getY());
    1437            g2.setComposite(saveComposite);
    1438        }
    1439
    1440        /**
    1441         * Returns the location for a label
    1442         *
    1443         * @param labelBounds the label bounds.
    1444         * @param ascent the ascent (height of font).
    1445         * @param plotArea the plot area
    1446         * @param startAngle the start angle for the pie series.
    1447         *
    1448         * @return The location for a label.
    1449         */
    1450        protected Point2D calculateLabelLocation(Rectangle2D labelBounds,
    1451                                                 double ascent,
    1452                                                 Rectangle2D plotArea,
    1453                                                 double startAngle)
    1454        {
    1455            Arc2D arc1 = new Arc2D.Double(plotArea, startAngle, 0, Arc2D.OPEN);
    1456            Point2D point1 = arc1.getEndPoint();
    1457
    1458            double deltaX = -(point1.getX() - plotArea.getCenterX())
    1459                            * this.axisLabelGap;
    1460            double deltaY = -(point1.getY() - plotArea.getCenterY())
    1461                            * this.axisLabelGap;
    1462
    1463            double labelX = point1.getX() - deltaX;
    1464            double labelY = point1.getY() - deltaY;
    1465
    1466            if (labelX < plotArea.getCenterX()) {
    1467                labelX -= labelBounds.getWidth();
    1468            }
    1469
    1470            if (labelX == plotArea.getCenterX()) {
    1471                labelX -= labelBounds.getWidth() / 2;
    1472            }
    1473
    1474            if (labelY > plotArea.getCenterY()) {
    1475                labelY += ascent;
    1476            }
    1477
    1478            return new Point2D.Double(labelX, labelY);
    1479        }
    1480
    1481        /**
    1482         * Tests this plot for equality with an arbitrary object.
    1483         *
    1484         * @param obj  the object (<code>null</code> permitted).
    1485         *
    1486         * @return A boolean.
    1487         */
    1488        public boolean equals(Object obj) {
    1489            if (obj == this) {
    1490                return true;
    1491            }
    1492            if (!(obj instanceof SpiderWebPlot)) {
    1493                return false;
    1494            }
    1495            if (!super.equals(obj)) {
    1496                return false;
    1497            }
    1498            SpiderWebPlot that = (SpiderWebPlot) obj;
    1499            if (!this.dataExtractOrder.equals(that.dataExtractOrder)) {
    1500                return false;
    1501            }
    1502            if (this.headPercent != that.headPercent) {
    1503                return false;
    1504            }
    1505            if (this.interiorGap != that.interiorGap) {
    1506                return false;
    1507            }
    1508            if (this.startAngle != that.startAngle) {
    1509                return false;
    1510            }
    1511            if (!this.direction.equals(that.direction)) {
    1512                return false;
    1513            }
    1514            if (this.maxValue != that.maxValue) {
    1515                return false;
    1516            }
    1517            if (this.webFilled != that.webFilled) {
    1518                return false;
    1519            }
    1520            if (this.axisLabelGap != that.axisLabelGap) {
    1521                return false;
    1522            }
    1523            if (!PaintUtilities.equal(this.axisLinePaint, that.axisLinePaint)) {
    1524                return false;
    1525            }
    1526            if (!this.axisLineStroke.equals(that.axisLineStroke)) {
    1527                return false;
    1528            }
    1529            if (!ShapeUtilities.equal(this.legendItemShape, that.legendItemShape)) {
    1530                return false;
    1531            }
    1532            if (!PaintUtilities.equal(this.seriesPaint, that.seriesPaint)) {
    1533                return false;
    1534            }
    1535            if (!this.seriesPaintList.equals(that.seriesPaintList)) {
    1536                return false;
    1537            }
    1538            if (!PaintUtilities.equal(this.baseSeriesPaint, that.baseSeriesPaint)) {
    1539                return false;
    1540            }
    1541            if (!PaintUtilities.equal(this.seriesOutlinePaint,
    1542                    that.seriesOutlinePaint)) {
    1543                return false;
    1544            }
    1545            if (!this.seriesOutlinePaintList.equals(that.seriesOutlinePaintList)) {
    1546                return false;
    1547            }
    1548            if (!PaintUtilities.equal(this.baseSeriesOutlinePaint,
    1549                    that.baseSeriesOutlinePaint)) {
    1550                return false;
    1551            }
    1552            if (!ObjectUtilities.equal(this.seriesOutlineStroke,
    1553                    that.seriesOutlineStroke)) {
    1554                return false;
    1555            }
    1556            if (!this.seriesOutlineStrokeList.equals(
    1557                    that.seriesOutlineStrokeList)) {
    1558                return false;
    1559            }
    1560            if (!this.baseSeriesOutlineStroke.equals(
    1561                    that.baseSeriesOutlineStroke)) {
    1562                return false;
    1563            }
    1564            if (!this.labelFont.equals(that.labelFont)) {
    1565                return false;
    1566            }
    1567            if (!PaintUtilities.equal(this.labelPaint, that.labelPaint)) {
    1568                return false;
    1569            }
    1570            if (!this.labelGenerator.equals(that.labelGenerator)) {
    1571                return false;
    1572            }
    1573            if (!ObjectUtilities.equal(this.toolTipGenerator,
    1574                    that.toolTipGenerator)) {
    1575                return false;
    1576            }
    1577            if (!ObjectUtilities.equal(this.urlGenerator,
    1578                    that.urlGenerator)) {
    1579                return false;
    1580            }
    1581            return true;
    1582        }
    1583
    1584        /**
    1585         * Returns a clone of this plot.
    1586         *
    1587         * @return A clone of this plot.
    1588         *
    1589         * @throws CloneNotSupportedException if the plot cannot be cloned for
    1590         *         any reason.
    1591         */
    1592        public Object clone() throws CloneNotSupportedException {
    1593            SpiderWebPlot clone = (SpiderWebPlot) super.clone();
    1594            clone.legendItemShape = ShapeUtilities.clone(this.legendItemShape);
    1595            clone.seriesPaintList = (PaintList) this.seriesPaintList.clone();
    1596            clone.seriesOutlinePaintList
    1597                    = (PaintList) this.seriesOutlinePaintList.clone();
    1598            clone.seriesOutlineStrokeList
    1599                    = (StrokeList) this.seriesOutlineStrokeList.clone();
    1600            return clone;
    1601        }
    1602
    1603        /**
    1604         * Provides serialization support.
    1605         *
    1606         * @param stream  the output stream.
    1607         *
    1608         * @throws IOException  if there is an I/O error.
    1609         */
    1610        private void writeObject(ObjectOutputStream stream) throws IOException {
    1611            stream.defaultWriteObject();
    1612
    1613            SerialUtilities.writeShape(this.legendItemShape, stream);
    1614            SerialUtilities.writePaint(this.seriesPaint, stream);
    1615            SerialUtilities.writePaint(this.baseSeriesPaint, stream);
    1616            SerialUtilities.writePaint(this.seriesOutlinePaint, stream);
    1617            SerialUtilities.writePaint(this.baseSeriesOutlinePaint, stream);
    1618            SerialUtilities.writeStroke(this.seriesOutlineStroke, stream);
    1619            SerialUtilities.writeStroke(this.baseSeriesOutlineStroke, stream);
    1620            SerialUtilities.writePaint(this.labelPaint, stream);
    1621            SerialUtilities.writePaint(this.axisLinePaint, stream);
    1622            SerialUtilities.writeStroke(this.axisLineStroke, stream);
    1623        }
    1624
    1625        /**
    1626         * Provides serialization support.
    1627         *
    1628         * @param stream  the input stream.
    1629         *
    1630         * @throws IOException  if there is an I/O error.
    1631         * @throws ClassNotFoundException  if there is a classpath problem.
    1632         */
    1633        private void readObject(ObjectInputStream stream) throws IOException,
    1634                ClassNotFoundException {
    1635            stream.defaultReadObject();
    1636
    1637            this.legendItemShape = SerialUtilities.readShape(stream);
    1638            this.seriesPaint = SerialUtilities.readPaint(stream);
    1639            this.baseSeriesPaint = SerialUtilities.readPaint(stream);
    1640            this.seriesOutlinePaint = SerialUtilities.readPaint(stream);
    1641            this.baseSeriesOutlinePaint = SerialUtilities.readPaint(stream);
    1642            this.seriesOutlineStroke = SerialUtilities.readStroke(stream);
    1643            this.baseSeriesOutlineStroke = SerialUtilities.readStroke(stream);
    1644            this.labelPaint = SerialUtilities.readPaint(stream);
    1645            this.axisLinePaint = SerialUtilities.readPaint(stream);
    1646            this.axisLineStroke = SerialUtilities.readStroke(stream);
    1647            if (this.dataset != null) {
    1648                this.dataset.addChangeListener(this);
    1649            }
    1650        }
    1651
    1652    }