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    }