Source for org.jfree.chart.plot.SpiderWebPlot

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