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