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