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 LegendItemCollection result = new LegendItemCollection();
1080 if (getDataset() == null) {
1081 return result;
1082 }
1083
1084 List keys = null;
1085 if (this.dataExtractOrder == TableOrder.BY_ROW) {
1086 keys = this.dataset.getRowKeys();
1087 }
1088 else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1089 keys = this.dataset.getColumnKeys();
1090 }
1091
1092 if (keys != null) {
1093 int series = 0;
1094 Iterator iterator = keys.iterator();
1095 Shape shape = getLegendItemShape();
1096
1097 while (iterator.hasNext()) {
1098 String label = iterator.next().toString();
1099 String description = label;
1100
1101 Paint paint = getSeriesPaint(series);
1102 Paint outlinePaint = getSeriesOutlinePaint(series);
1103 Stroke stroke = getSeriesOutlineStroke(series);
1104 LegendItem item = new LegendItem(label, description,
1105 null, null, shape, paint, stroke, outlinePaint);
1106 item.setDataset(getDataset());
1107 result.add(item);
1108 series++;
1109 }
1110 }
1111
1112 return result;
1113 }
1114
1115 /**
1116 * Returns a cartesian point from a polar angle, length and bounding box
1117 *
1118 * @param bounds the area inside which the point needs to be.
1119 * @param angle the polar angle, in degrees.
1120 * @param length the relative length. Given in percent of maximum extend.
1121 *
1122 * @return The cartesian point.
1123 */
1124 protected Point2D getWebPoint(Rectangle2D bounds,
1125 double angle, double length) {
1126
1127 double angrad = Math.toRadians(angle);
1128 double x = Math.cos(angrad) * length * bounds.getWidth() / 2;
1129 double y = -Math.sin(angrad) * length * bounds.getHeight() / 2;
1130
1131 return new Point2D.Double(bounds.getX() + x + bounds.getWidth() / 2,
1132 bounds.getY() + y + bounds.getHeight() / 2);
1133 }
1134
1135 /**
1136 * Draws the plot on a Java 2D graphics device (such as the screen or a
1137 * printer).
1138 *
1139 * @param g2 the graphics device.
1140 * @param area the area within which the plot should be drawn.
1141 * @param anchor the anchor point (<code>null</code> permitted).
1142 * @param parentState the state from the parent plot, if there is one.
1143 * @param info collects info about the drawing.
1144 */
1145 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
1146 PlotState parentState, PlotRenderingInfo info) {
1147
1148 // adjust for insets...
1149 RectangleInsets insets = getInsets();
1150 insets.trim(area);
1151
1152 if (info != null) {
1153 info.setPlotArea(area);
1154 info.setDataArea(area);
1155 }
1156
1157 drawBackground(g2, area);
1158 drawOutline(g2, area);
1159
1160 Shape savedClip = g2.getClip();
1161
1162 g2.clip(area);
1163 Composite originalComposite = g2.getComposite();
1164 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1165 getForegroundAlpha()));
1166
1167 if (!DatasetUtilities.isEmptyOrNull(this.dataset)) {
1168 int seriesCount = 0, catCount = 0;
1169
1170 if (this.dataExtractOrder == TableOrder.BY_ROW) {
1171 seriesCount = this.dataset.getRowCount();
1172 catCount = this.dataset.getColumnCount();
1173 }
1174 else {
1175 seriesCount = this.dataset.getColumnCount();
1176 catCount = this.dataset.getRowCount();
1177 }
1178
1179 // ensure we have a maximum value to use on the axes
1180 if (this.maxValue == DEFAULT_MAX_VALUE)
1181 calculateMaxValue(seriesCount, catCount);
1182
1183 // Next, setup the plot area
1184
1185 // adjust the plot area by the interior spacing value
1186
1187 double gapHorizontal = area.getWidth() * getInteriorGap();
1188 double gapVertical = area.getHeight() * getInteriorGap();
1189
1190 double X = area.getX() + gapHorizontal / 2;
1191 double Y = area.getY() + gapVertical / 2;
1192 double W = area.getWidth() - gapHorizontal;
1193 double H = area.getHeight() - gapVertical;
1194
1195 double headW = area.getWidth() * this.headPercent;
1196 double headH = area.getHeight() * this.headPercent;
1197
1198 // make the chart area a square
1199 double min = Math.min(W, H) / 2;
1200 X = (X + X + W) / 2 - min;
1201 Y = (Y + Y + H) / 2 - min;
1202 W = 2 * min;
1203 H = 2 * min;
1204
1205 Point2D centre = new Point2D.Double(X + W / 2, Y + H / 2);
1206 Rectangle2D radarArea = new Rectangle2D.Double(X, Y, W, H);
1207
1208 // draw the axis and category label
1209 for (int cat = 0; cat < catCount; cat++) {
1210 double angle = getStartAngle()
1211 + (getDirection().getFactor() * cat * 360 / catCount);
1212
1213 Point2D endPoint = getWebPoint(radarArea, angle, 1);
1214 // 1 = end of axis
1215 Line2D line = new Line2D.Double(centre, endPoint);
1216 g2.setPaint(this.axisLinePaint);
1217 g2.setStroke(this.axisLineStroke);
1218 g2.draw(line);
1219 drawLabel(g2, radarArea, 0.0, cat, angle, 360.0 / catCount);
1220 }
1221
1222 // Now actually plot each of the series polygons..
1223 for (int series = 0; series < seriesCount; series++) {
1224 drawRadarPoly(g2, radarArea, centre, info, series, catCount,
1225 headH, headW);
1226 }
1227 }
1228 else {
1229 drawNoDataMessage(g2, area);
1230 }
1231 g2.setClip(savedClip);
1232 g2.setComposite(originalComposite);
1233 drawOutline(g2, area);
1234 }
1235
1236 /**
1237 * loop through each of the series to get the maximum value
1238 * on each category axis
1239 *
1240 * @param seriesCount the number of series
1241 * @param catCount the number of categories
1242 */
1243 private void calculateMaxValue(int seriesCount, int catCount) {
1244 double v = 0;
1245 Number nV = null;
1246
1247 for (int seriesIndex = 0; seriesIndex < seriesCount; seriesIndex++) {
1248 for (int catIndex = 0; catIndex < catCount; catIndex++) {
1249 nV = getPlotValue(seriesIndex, catIndex);
1250 if (nV != null) {
1251 v = nV.doubleValue();
1252 if (v > this.maxValue) {
1253 this.maxValue = v;
1254 }
1255 }
1256 }
1257 }
1258 }
1259
1260 /**
1261 * Draws a radar plot polygon.
1262 *
1263 * @param g2 the graphics device.
1264 * @param plotArea the area we are plotting in (already adjusted).
1265 * @param centre the centre point of the radar axes
1266 * @param info chart rendering info.
1267 * @param series the series within the dataset we are plotting
1268 * @param catCount the number of categories per radar plot
1269 * @param headH the data point height
1270 * @param headW the data point width
1271 */
1272 protected void drawRadarPoly(Graphics2D g2,
1273 Rectangle2D plotArea,
1274 Point2D centre,
1275 PlotRenderingInfo info,
1276 int series, int catCount,
1277 double headH, double headW) {
1278
1279 Polygon polygon = new Polygon();
1280
1281 EntityCollection entities = null;
1282 if (info != null) {
1283 entities = info.getOwner().getEntityCollection();
1284 }
1285
1286 // plot the data...
1287 for (int cat = 0; cat < catCount; cat++) {
1288
1289 Number dataValue = getPlotValue(series, cat);
1290
1291 if (dataValue != null) {
1292 double value = dataValue.doubleValue();
1293
1294 if (value >= 0) { // draw the polygon series...
1295
1296 // Finds our starting angle from the centre for this axis
1297
1298 double angle = getStartAngle()
1299 + (getDirection().getFactor() * cat * 360 / catCount);
1300
1301 // The following angle calc will ensure there isn't a top
1302 // vertical axis - this may be useful if you don't want any
1303 // given criteria to 'appear' move important than the
1304 // others..
1305 // + (getDirection().getFactor()
1306 // * (cat + 0.5) * 360 / catCount);
1307
1308 // find the point at the appropriate distance end point
1309 // along the axis/angle identified above and add it to the
1310 // polygon
1311
1312 Point2D point = getWebPoint(plotArea, angle,
1313 value / this.maxValue);
1314 polygon.addPoint((int) point.getX(), (int) point.getY());
1315
1316 // put an elipse at the point being plotted..
1317
1318 Paint paint = getSeriesPaint(series);
1319 Paint outlinePaint = getSeriesOutlinePaint(series);
1320 Stroke outlineStroke = getSeriesOutlineStroke(series);
1321
1322 Ellipse2D head = new Ellipse2D.Double(point.getX()
1323 - headW / 2, point.getY() - headH / 2, headW,
1324 headH);
1325 g2.setPaint(paint);
1326 g2.fill(head);
1327 g2.setStroke(outlineStroke);
1328 g2.setPaint(outlinePaint);
1329 g2.draw(head);
1330
1331 if (entities != null) {
1332 int row = 0; int col = 0;
1333 if (this.dataExtractOrder == TableOrder.BY_ROW) {
1334 row = series;
1335 col = cat;
1336 }
1337 else {
1338 row = cat;
1339 col = series;
1340 }
1341 String tip = null;
1342 if (this.toolTipGenerator != null) {
1343 tip = this.toolTipGenerator.generateToolTip(
1344 this.dataset, row, col);
1345 }
1346
1347 String url = null;
1348 if (this.urlGenerator != null) {
1349 url = this.urlGenerator.generateURL(this.dataset,
1350 row, col);
1351 }
1352
1353 Shape area = new Rectangle(
1354 (int) (point.getX() - headW),
1355 (int) (point.getY() - headH),
1356 (int) (headW * 2), (int) (headH * 2));
1357 CategoryItemEntity entity = new CategoryItemEntity(
1358 area, tip, url, this.dataset,
1359 this.dataset.getRowKey(row),
1360 this.dataset.getColumnKey(col));
1361 entities.add(entity);
1362 }
1363
1364 }
1365 }
1366 }
1367 // Plot the polygon
1368
1369 Paint paint = getSeriesPaint(series);
1370 g2.setPaint(paint);
1371 g2.setStroke(getSeriesOutlineStroke(series));
1372 g2.draw(polygon);
1373
1374 // Lastly, fill the web polygon if this is required
1375
1376 if (this.webFilled) {
1377 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1378 0.1f));
1379 g2.fill(polygon);
1380 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1381 getForegroundAlpha()));
1382 }
1383 }
1384
1385 /**
1386 * Returns the value to be plotted at the interseries of the
1387 * series and the category. This allows us to plot
1388 * <code>BY_ROW</code> or <code>BY_COLUMN</code> which basically is just
1389 * reversing the definition of the categories and data series being
1390 * plotted.
1391 *
1392 * @param series the series to be plotted.
1393 * @param cat the category within the series to be plotted.
1394 *
1395 * @return The value to be plotted (possibly <code>null</code>).
1396 *
1397 * @see #getDataExtractOrder()
1398 */
1399 protected Number getPlotValue(int series, int cat) {
1400 Number value = null;
1401 if (this.dataExtractOrder == TableOrder.BY_ROW) {
1402 value = this.dataset.getValue(series, cat);
1403 }
1404 else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1405 value = this.dataset.getValue(cat, series);
1406 }
1407 return value;
1408 }
1409
1410 /**
1411 * Draws the label for one axis.
1412 *
1413 * @param g2 the graphics device.
1414 * @param plotArea the plot area
1415 * @param value the value of the label (ignored).
1416 * @param cat the category (zero-based index).
1417 * @param startAngle the starting angle.
1418 * @param extent the extent of the arc.
1419 */
1420 protected void drawLabel(Graphics2D g2, Rectangle2D plotArea, double value,
1421 int cat, double startAngle, double extent) {
1422 FontRenderContext frc = g2.getFontRenderContext();
1423
1424 String label = null;
1425 if (this.dataExtractOrder == TableOrder.BY_ROW) {
1426 // if series are in rows, then the categories are the column keys
1427 label = this.labelGenerator.generateColumnLabel(this.dataset, cat);
1428 }
1429 else {
1430 // if series are in columns, then the categories are the row keys
1431 label = this.labelGenerator.generateRowLabel(this.dataset, cat);
1432 }
1433
1434 Rectangle2D labelBounds = getLabelFont().getStringBounds(label, frc);
1435 LineMetrics lm = getLabelFont().getLineMetrics(label, frc);
1436 double ascent = lm.getAscent();
1437
1438 Point2D labelLocation = calculateLabelLocation(labelBounds, ascent,
1439 plotArea, startAngle);
1440
1441 Composite saveComposite = g2.getComposite();
1442
1443 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1444 1.0f));
1445 g2.setPaint(getLabelPaint());
1446 g2.setFont(getLabelFont());
1447 g2.drawString(label, (float) labelLocation.getX(),
1448 (float) labelLocation.getY());
1449 g2.setComposite(saveComposite);
1450 }
1451
1452 /**
1453 * Returns the location for a label
1454 *
1455 * @param labelBounds the label bounds.
1456 * @param ascent the ascent (height of font).
1457 * @param plotArea the plot area
1458 * @param startAngle the start angle for the pie series.
1459 *
1460 * @return The location for a label.
1461 */
1462 protected Point2D calculateLabelLocation(Rectangle2D labelBounds,
1463 double ascent,
1464 Rectangle2D plotArea,
1465 double startAngle)
1466 {
1467 Arc2D arc1 = new Arc2D.Double(plotArea, startAngle, 0, Arc2D.OPEN);
1468 Point2D point1 = arc1.getEndPoint();
1469
1470 double deltaX = -(point1.getX() - plotArea.getCenterX())
1471 * this.axisLabelGap;
1472 double deltaY = -(point1.getY() - plotArea.getCenterY())
1473 * this.axisLabelGap;
1474
1475 double labelX = point1.getX() - deltaX;
1476 double labelY = point1.getY() - deltaY;
1477
1478 if (labelX < plotArea.getCenterX()) {
1479 labelX -= labelBounds.getWidth();
1480 }
1481
1482 if (labelX == plotArea.getCenterX()) {
1483 labelX -= labelBounds.getWidth() / 2;
1484 }
1485
1486 if (labelY > plotArea.getCenterY()) {
1487 labelY += ascent;
1488 }
1489
1490 return new Point2D.Double(labelX, labelY);
1491 }
1492
1493 /**
1494 * Tests this plot for equality with an arbitrary object.
1495 *
1496 * @param obj the object (<code>null</code> permitted).
1497 *
1498 * @return A boolean.
1499 */
1500 public boolean equals(Object obj) {
1501 if (obj == this) {
1502 return true;
1503 }
1504 if (!(obj instanceof SpiderWebPlot)) {
1505 return false;
1506 }
1507 if (!super.equals(obj)) {
1508 return false;
1509 }
1510 SpiderWebPlot that = (SpiderWebPlot) obj;
1511 if (!this.dataExtractOrder.equals(that.dataExtractOrder)) {
1512 return false;
1513 }
1514 if (this.headPercent != that.headPercent) {
1515 return false;
1516 }
1517 if (this.interiorGap != that.interiorGap) {
1518 return false;
1519 }
1520 if (this.startAngle != that.startAngle) {
1521 return false;
1522 }
1523 if (!this.direction.equals(that.direction)) {
1524 return false;
1525 }
1526 if (this.maxValue != that.maxValue) {
1527 return false;
1528 }
1529 if (this.webFilled != that.webFilled) {
1530 return false;
1531 }
1532 if (this.axisLabelGap != that.axisLabelGap) {
1533 return false;
1534 }
1535 if (!PaintUtilities.equal(this.axisLinePaint, that.axisLinePaint)) {
1536 return false;
1537 }
1538 if (!this.axisLineStroke.equals(that.axisLineStroke)) {
1539 return false;
1540 }
1541 if (!ShapeUtilities.equal(this.legendItemShape, that.legendItemShape)) {
1542 return false;
1543 }
1544 if (!PaintUtilities.equal(this.seriesPaint, that.seriesPaint)) {
1545 return false;
1546 }
1547 if (!this.seriesPaintList.equals(that.seriesPaintList)) {
1548 return false;
1549 }
1550 if (!PaintUtilities.equal(this.baseSeriesPaint, that.baseSeriesPaint)) {
1551 return false;
1552 }
1553 if (!PaintUtilities.equal(this.seriesOutlinePaint,
1554 that.seriesOutlinePaint)) {
1555 return false;
1556 }
1557 if (!this.seriesOutlinePaintList.equals(that.seriesOutlinePaintList)) {
1558 return false;
1559 }
1560 if (!PaintUtilities.equal(this.baseSeriesOutlinePaint,
1561 that.baseSeriesOutlinePaint)) {
1562 return false;
1563 }
1564 if (!ObjectUtilities.equal(this.seriesOutlineStroke,
1565 that.seriesOutlineStroke)) {
1566 return false;
1567 }
1568 if (!this.seriesOutlineStrokeList.equals(
1569 that.seriesOutlineStrokeList)) {
1570 return false;
1571 }
1572 if (!this.baseSeriesOutlineStroke.equals(
1573 that.baseSeriesOutlineStroke)) {
1574 return false;
1575 }
1576 if (!this.labelFont.equals(that.labelFont)) {
1577 return false;
1578 }
1579 if (!PaintUtilities.equal(this.labelPaint, that.labelPaint)) {
1580 return false;
1581 }
1582 if (!this.labelGenerator.equals(that.labelGenerator)) {
1583 return false;
1584 }
1585 if (!ObjectUtilities.equal(this.toolTipGenerator,
1586 that.toolTipGenerator)) {
1587 return false;
1588 }
1589 if (!ObjectUtilities.equal(this.urlGenerator,
1590 that.urlGenerator)) {
1591 return false;
1592 }
1593 return true;
1594 }
1595
1596 /**
1597 * Returns a clone of this plot.
1598 *
1599 * @return A clone of this plot.
1600 *
1601 * @throws CloneNotSupportedException if the plot cannot be cloned for
1602 * any reason.
1603 */
1604 public Object clone() throws CloneNotSupportedException {
1605 SpiderWebPlot clone = (SpiderWebPlot) super.clone();
1606 clone.legendItemShape = ShapeUtilities.clone(this.legendItemShape);
1607 clone.seriesPaintList = (PaintList) this.seriesPaintList.clone();
1608 clone.seriesOutlinePaintList
1609 = (PaintList) this.seriesOutlinePaintList.clone();
1610 clone.seriesOutlineStrokeList
1611 = (StrokeList) this.seriesOutlineStrokeList.clone();
1612 return clone;
1613 }
1614
1615 /**
1616 * Provides serialization support.
1617 *
1618 * @param stream the output stream.
1619 *
1620 * @throws IOException if there is an I/O error.
1621 */
1622 private void writeObject(ObjectOutputStream stream) throws IOException {
1623 stream.defaultWriteObject();
1624
1625 SerialUtilities.writeShape(this.legendItemShape, stream);
1626 SerialUtilities.writePaint(this.seriesPaint, stream);
1627 SerialUtilities.writePaint(this.baseSeriesPaint, stream);
1628 SerialUtilities.writePaint(this.seriesOutlinePaint, stream);
1629 SerialUtilities.writePaint(this.baseSeriesOutlinePaint, stream);
1630 SerialUtilities.writeStroke(this.seriesOutlineStroke, stream);
1631 SerialUtilities.writeStroke(this.baseSeriesOutlineStroke, stream);
1632 SerialUtilities.writePaint(this.labelPaint, stream);
1633 SerialUtilities.writePaint(this.axisLinePaint, stream);
1634 SerialUtilities.writeStroke(this.axisLineStroke, stream);
1635 }
1636
1637 /**
1638 * Provides serialization support.
1639 *
1640 * @param stream the input stream.
1641 *
1642 * @throws IOException if there is an I/O error.
1643 * @throws ClassNotFoundException if there is a classpath problem.
1644 */
1645 private void readObject(ObjectInputStream stream) throws IOException,
1646 ClassNotFoundException {
1647 stream.defaultReadObject();
1648
1649 this.legendItemShape = SerialUtilities.readShape(stream);
1650 this.seriesPaint = SerialUtilities.readPaint(stream);
1651 this.baseSeriesPaint = SerialUtilities.readPaint(stream);
1652 this.seriesOutlinePaint = SerialUtilities.readPaint(stream);
1653 this.baseSeriesOutlinePaint = SerialUtilities.readPaint(stream);
1654 this.seriesOutlineStroke = SerialUtilities.readStroke(stream);
1655 this.baseSeriesOutlineStroke = SerialUtilities.readStroke(stream);
1656 this.labelPaint = SerialUtilities.readPaint(stream);
1657 this.axisLinePaint = SerialUtilities.readPaint(stream);
1658 this.axisLineStroke = SerialUtilities.readStroke(stream);
1659 if (this.dataset != null) {
1660 this.dataset.addChangeListener(this);
1661 }
1662 }
1663
1664 }