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