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