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