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