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