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