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-2007, 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-2007, 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.14 2007/03/05 13:46:28 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.x ---------------------------------------------
  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:  * 05-Feb-2007 : Added attributes for axis stroke and paint, while fixing
  62:  *               bug 1651277, and implemented clone() properly (DG);
  63:  * 06-Feb-2007 : Changed getPlotValue() to protected, as suggested in bug 
  64:  *               1605202 (DG);
  65:  * 05-Mar-2007 : Restore clip region correctly (see bug 1667750) (DG);
  66:  *
  67:  */
  68: 
  69: package org.jfree.chart.plot;
  70: 
  71: import java.awt.AlphaComposite;
  72: import java.awt.BasicStroke;
  73: import java.awt.Color;
  74: import java.awt.Composite;
  75: import java.awt.Font;
  76: import java.awt.Graphics2D;
  77: import java.awt.Paint;
  78: import java.awt.Polygon;
  79: import java.awt.Rectangle;
  80: import java.awt.Shape;
  81: import java.awt.Stroke;
  82: import java.awt.font.FontRenderContext;
  83: import java.awt.font.LineMetrics;
  84: import java.awt.geom.Arc2D;
  85: import java.awt.geom.Ellipse2D;
  86: import java.awt.geom.Line2D;
  87: import java.awt.geom.Point2D;
  88: import java.awt.geom.Rectangle2D;
  89: import java.io.IOException;
  90: import java.io.ObjectInputStream;
  91: import java.io.ObjectOutputStream;
  92: import java.io.Serializable;
  93: import java.util.Iterator;
  94: import java.util.List;
  95: 
  96: import org.jfree.chart.LegendItem;
  97: import org.jfree.chart.LegendItemCollection;
  98: import org.jfree.chart.entity.CategoryItemEntity;
  99: 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: }