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:  * Changes
  39:  * -------
  40:  * 28-Jan-2005 : First cut - missing a few features - still to do:
  41:  *                           - needs tooltips/URL/label generator functions
  42:  *                           - ticks on axes / background grid?
  43:  * 31-Jan-2005 : Renamed SpiderWebPlot, added label generator support, and 
  44:  *               reformatted for consistency with other source files in 
  45:  *               JFreeChart (DG);
  46:  * 20-Apr-2005 : Renamed CategoryLabelGenerator 
  47:  *               --> CategoryItemLabelGenerator (DG);
  48:  * 05-May-2005 : Updated draw() method parameters (DG);
  49:  * 10-Jun-2005 : Added equals() method and fixed serialization (DG);
  50:  * 16-Jun-2005 : Added default constructor and get/setDataset() 
  51:  *               methods (DG);
  52:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  53:  * 05-Apr-2006 : Fixed bug preventing the display of zero values - see patch
  54:  *               1462727 (DG);
  55:  * 05-Apr-2006 : Added support for mouse clicks, tool tips and URLs - see patch
  56:  *               1463455 (DG);
  57:  * 01-Jun-2006 : Fix bug 1493199, NullPointerException when drawing with null
  58:  *               info (DG);
  59:  * 05-Feb-2007 : Added attributes for axis stroke and paint, while fixing
  60:  *               bug 1651277, and implemented clone() properly (DG);
  61:  * 06-Feb-2007 : Changed getPlotValue() to protected, as suggested in bug 
  62:  *               1605202 (DG);
  63:  * 05-Mar-2007 : Restore clip region correctly (see bug 1667750) (DG);
  64:  * 18-May-2007 : Set dataset for LegendItem (DG);
  65:  *
  66:  */
  67: 
  68: package org.jfree.chart.plot;
  69: 
  70: import java.awt.AlphaComposite;
  71: import java.awt.BasicStroke;
  72: import java.awt.Color;
  73: import java.awt.Composite;
  74: import java.awt.Font;
  75: import java.awt.Graphics2D;
  76: import java.awt.Paint;
  77: import java.awt.Polygon;
  78: import java.awt.Rectangle;
  79: import java.awt.Shape;
  80: import java.awt.Stroke;
  81: import java.awt.font.FontRenderContext;
  82: import java.awt.font.LineMetrics;
  83: import java.awt.geom.Arc2D;
  84: import java.awt.geom.Ellipse2D;
  85: import java.awt.geom.Line2D;
  86: import java.awt.geom.Point2D;
  87: import java.awt.geom.Rectangle2D;
  88: import java.io.IOException;
  89: import java.io.ObjectInputStream;
  90: import java.io.ObjectOutputStream;
  91: import java.io.Serializable;
  92: import java.util.Iterator;
  93: import java.util.List;
  94: 
  95: import org.jfree.chart.LegendItem;
  96: import org.jfree.chart.LegendItemCollection;
  97: import org.jfree.chart.entity.CategoryItemEntity;
  98: import org.jfree.chart.entity.EntityCollection;
  99: import org.jfree.chart.event.PlotChangeEvent;
 100: import org.jfree.chart.labels.CategoryItemLabelGenerator;
 101: import org.jfree.chart.labels.CategoryToolTipGenerator;
 102: import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
 103: import org.jfree.chart.urls.CategoryURLGenerator;
 104: import org.jfree.data.category.CategoryDataset;
 105: import org.jfree.data.general.DatasetChangeEvent;
 106: import org.jfree.data.general.DatasetUtilities;
 107: import org.jfree.io.SerialUtilities;
 108: import org.jfree.ui.RectangleInsets;
 109: import org.jfree.util.ObjectUtilities;
 110: import org.jfree.util.PaintList;
 111: import org.jfree.util.PaintUtilities;
 112: import org.jfree.util.Rotation;
 113: import org.jfree.util.ShapeUtilities;
 114: import org.jfree.util.StrokeList;
 115: import org.jfree.util.TableOrder;
 116: 
 117: /**
 118:  * A plot that displays data from a {@link CategoryDataset} in the form of a 
 119:  * "spider web".  Multiple series can be plotted on the same axis to allow 
 120:  * easy comparison.  This plot doesn't support negative values at present.
 121:  */
 122: public class SpiderWebPlot extends Plot implements Cloneable, Serializable {
 123:     
 124:     /** For serialization. */
 125:     private static final long serialVersionUID = -5376340422031599463L;
 126:     
 127:     /** The default head radius percent (currently 1%). */
 128:     public static final double DEFAULT_HEAD = 0.01;
 129: 
 130:     /** The default axis label gap (currently 10%). */
 131:     public static final double DEFAULT_AXIS_LABEL_GAP = 0.10;
 132:  
 133:     /** The default interior gap. */
 134:     public static final double DEFAULT_INTERIOR_GAP = 0.25;
 135: 
 136:     /** The maximum interior gap (currently 40%). */
 137:     public static final double MAX_INTERIOR_GAP = 0.40;
 138: 
 139:     /** The default starting angle for the radar chart axes. */
 140:     public static final double DEFAULT_START_ANGLE = 90.0;
 141: 
 142:     /** The default series label font. */
 143:     public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif", 
 144:             Font.PLAIN, 10);
 145:     
 146:     /** The default series label paint. */
 147:     public static final Paint  DEFAULT_LABEL_PAINT = Color.black;
 148: 
 149:     /** The default series label background paint. */
 150:     public static final Paint  DEFAULT_LABEL_BACKGROUND_PAINT 
 151:             = new Color(255, 255, 192);
 152: 
 153:     /** The default series label outline paint. */
 154:     public static final Paint  DEFAULT_LABEL_OUTLINE_PAINT = Color.black;
 155: 
 156:     /** The default series label outline stroke. */
 157:     public static final Stroke DEFAULT_LABEL_OUTLINE_STROKE 
 158:             = new BasicStroke(0.5f);
 159: 
 160:     /** The default series label shadow paint. */
 161:     public static final Paint  DEFAULT_LABEL_SHADOW_PAINT = Color.lightGray;
 162: 
 163:     /** 
 164:      * The default maximum value plotted - forces the plot to evaluate
 165:      *  the maximum from the data passed in
 166:      */
 167:     public static final double DEFAULT_MAX_VALUE = -1.0;
 168: 
 169:     /** The head radius as a percentage of the available drawing area. */
 170:     protected double headPercent;
 171: 
 172:     /** The space left around the outside of the plot as a percentage. */
 173:     private double interiorGap;
 174: 
 175:     /** The gap between the labels and the axes as a %age of the radius. */
 176:     private double axisLabelGap;
 177:     
 178:     /**
 179:      * The paint used to draw the axis lines.
 180:      * 
 181:      * @since 1.0.4
 182:      */
 183:     private transient Paint axisLinePaint;
 184:     
 185:     /**
 186:      * The stroke used to draw the axis lines.
 187:      * 
 188:      * @since 1.0.4
 189:      */
 190:     private transient Stroke axisLineStroke;
 191: 
 192:     /** The dataset. */
 193:     private CategoryDataset dataset;
 194: 
 195:     /** The maximum value we are plotting against on each category axis */
 196:     private double maxValue;
 197:   
 198:     /** 
 199:      * The data extract order (BY_ROW or BY_COLUMN). This denotes whether
 200:      * the data series are stored in rows (in which case the category names are
 201:      * derived from the column keys) or in columns (in which case the category
 202:      * names are derived from the row keys).
 203:      */
 204:     private TableOrder dataExtractOrder;
 205: 
 206:     /** The starting angle. */
 207:     private double startAngle;
 208: 
 209:     /** The direction for drawing the radar axis & plots. */
 210:     private Rotation direction;
 211: 
 212:     /** The legend item shape. */
 213:     private transient Shape legendItemShape;
 214: 
 215:     /** The paint for ALL series (overrides list). */
 216:     private transient Paint seriesPaint;
 217: 
 218:     /** The series paint list. */
 219:     private PaintList seriesPaintList;
 220: 
 221:     /** The base series paint (fallback). */
 222:     private transient Paint baseSeriesPaint;
 223: 
 224:     /** The outline paint for ALL series (overrides list). */
 225:     private transient Paint seriesOutlinePaint;
 226: 
 227:     /** The series outline paint list. */
 228:     private PaintList seriesOutlinePaintList;
 229: 
 230:     /** The base series outline paint (fallback). */
 231:     private transient Paint baseSeriesOutlinePaint;
 232: 
 233:     /** The outline stroke for ALL series (overrides list). */
 234:     private transient Stroke seriesOutlineStroke;
 235: 
 236:     /** The series outline stroke list. */
 237:     private StrokeList seriesOutlineStrokeList;
 238: 
 239:     /** The base series outline stroke (fallback). */
 240:     private transient Stroke baseSeriesOutlineStroke;
 241: 
 242:     /** The font used to display the category labels. */
 243:     private Font labelFont;
 244: 
 245:     /** The color used to draw the category labels. */
 246:     private transient Paint labelPaint;
 247:     
 248:     /** The label generator. */
 249:     private CategoryItemLabelGenerator labelGenerator;
 250: 
 251:     /** controls if the web polygons are filled or not */
 252:     private boolean webFilled = true;
 253:     
 254:     /** A tooltip generator for the plot (<code>null</code> permitted). */
 255:     private CategoryToolTipGenerator toolTipGenerator;
 256:     
 257:     /** A URL generator for the plot (<code>null</code> permitted). */
 258:     private CategoryURLGenerator urlGenerator;
 259:   
 260:     /**
 261:      * Creates a default plot with no dataset.
 262:      */
 263:     public SpiderWebPlot() {
 264:         this(null);   
 265:     }
 266:     
 267:     /**
 268:      * Creates a new spider web plot with the given dataset, with each row
 269:      * representing a series.  
 270:      * 
 271:      * @param dataset  the dataset (<code>null</code> permitted).
 272:      */
 273:     public SpiderWebPlot(CategoryDataset dataset) {
 274:         this(dataset, TableOrder.BY_ROW);
 275:     }
 276: 
 277:     /**
 278:      * Creates a new spider web plot with the given dataset.
 279:      * 
 280:      * @param dataset  the dataset.
 281:      * @param extract  controls how data is extracted ({@link TableOrder#BY_ROW}
 282:      *                 or {@link TableOrder#BY_COLUMN}).
 283:      */
 284:     public SpiderWebPlot(CategoryDataset dataset, TableOrder extract) {
 285:         super();
 286:         if (extract == null) {
 287:             throw new IllegalArgumentException("Null 'extract' argument.");
 288:         }
 289:         this.dataset = dataset;
 290:         if (dataset != null) {
 291:             dataset.addChangeListener(this);
 292:         }
 293: 
 294:         this.dataExtractOrder = extract;
 295:         this.headPercent = DEFAULT_HEAD;
 296:         this.axisLabelGap = DEFAULT_AXIS_LABEL_GAP;
 297:         this.axisLinePaint = Color.black;
 298:         this.axisLineStroke = new BasicStroke(1.0f);
 299:         
 300:         this.interiorGap = DEFAULT_INTERIOR_GAP;
 301:         this.startAngle = DEFAULT_START_ANGLE;
 302:         this.direction = Rotation.CLOCKWISE;
 303:         this.maxValue = DEFAULT_MAX_VALUE;
 304: 
 305:         this.seriesPaint = null;
 306:         this.seriesPaintList = new PaintList();
 307:         this.baseSeriesPaint = null;
 308: 
 309:         this.seriesOutlinePaint = null;
 310:         this.seriesOutlinePaintList = new PaintList();
 311:         this.baseSeriesOutlinePaint = DEFAULT_OUTLINE_PAINT;
 312: 
 313:         this.seriesOutlineStroke = null;
 314:         this.seriesOutlineStrokeList = new StrokeList();
 315:         this.baseSeriesOutlineStroke = DEFAULT_OUTLINE_STROKE;
 316: 
 317:         this.labelFont = DEFAULT_LABEL_FONT;
 318:         this.labelPaint = DEFAULT_LABEL_PAINT;
 319:         this.labelGenerator = new StandardCategoryItemLabelGenerator();
 320:         
 321:         this.legendItemShape = DEFAULT_LEGEND_ITEM_CIRCLE;
 322:     }
 323: 
 324:     /**
 325:      * Returns a short string describing the type of plot.
 326:      * 
 327:      * @return The plot type.
 328:      */
 329:     public String getPlotType() {
 330:         // return localizationResources.getString("Radar_Plot");
 331:         return ("Spider Web Plot");
 332:     }
 333:     
 334:     /**
 335:      * Returns the dataset.
 336:      * 
 337:      * @return The dataset (possibly <code>null</code>).
 338:      * 
 339:      * @see #setDataset(CategoryDataset)
 340:      */
 341:     public CategoryDataset getDataset() {
 342:         return this.dataset;   
 343:     }
 344:     
 345:     /**
 346:      * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
 347:      * to all registered listeners.
 348:      * 
 349:      * @param dataset  the dataset (<code>null</code> permitted).
 350:      * 
 351:      * @see #getDataset()
 352:      */
 353:     public void setDataset(CategoryDataset dataset) {
 354:         // if there is an existing dataset, remove the plot from the list of 
 355:         // change listeners...
 356:         if (this.dataset != null) {
 357:             this.dataset.removeChangeListener(this);
 358:         }
 359: 
 360:         // set the new dataset, and register the chart as a change listener...
 361:         this.dataset = dataset;
 362:         if (dataset != null) {
 363:             setDatasetGroup(dataset.getGroup());
 364:             dataset.addChangeListener(this);
 365:         }
 366: 
 367:         // send a dataset change event to self to trigger plot change event
 368:         datasetChanged(new DatasetChangeEvent(this, dataset));
 369:     }
 370:     
 371:     /**
 372:      * Method to determine if the web chart is to be filled.
 373:      * 
 374:      * @return A boolean.
 375:      * 
 376:      * @see #setWebFilled(boolean)
 377:      */
 378:     public boolean isWebFilled() {
 379:         return this.webFilled;
 380:     }
 381: 
 382:     /**
 383:      * Sets the webFilled flag and sends a {@link PlotChangeEvent} to all 
 384:      * registered listeners.
 385:      * 
 386:      * @param flag  the flag.
 387:      * 
 388:      * @see #isWebFilled()
 389:      */
 390:     public void setWebFilled(boolean flag) {
 391:         this.webFilled = flag;
 392:         notifyListeners(new PlotChangeEvent(this));
 393:     }
 394:   
 395:     /**
 396:      * Returns the data extract order (by row or by column).
 397:      * 
 398:      * @return The data extract order (never <code>null</code>).
 399:      * 
 400:      * @see #setDataExtractOrder(TableOrder)
 401:      */
 402:     public TableOrder getDataExtractOrder() {
 403:         return this.dataExtractOrder;
 404:     }
 405: 
 406:     /**
 407:      * Sets the data extract order (by row or by column) and sends a
 408:      * {@link PlotChangeEvent}to all registered listeners.
 409:      * 
 410:      * @param order the order (<code>null</code> not permitted).
 411:      * 
 412:      * @throws IllegalArgumentException if <code>order</code> is 
 413:      *     <code>null</code>.
 414:      *     
 415:      * @see #getDataExtractOrder()
 416:      */
 417:     public void setDataExtractOrder(TableOrder order) {
 418:         if (order == null) {
 419:             throw new IllegalArgumentException("Null 'order' argument");
 420:         }
 421:         this.dataExtractOrder = order;
 422:         notifyListeners(new PlotChangeEvent(this));
 423:     }
 424: 
 425:     /**
 426:      * Returns the head percent.
 427:      * 
 428:      * @return The head percent.
 429:      * 
 430:      * @see #setHeadPercent(double)
 431:      */
 432:     public double getHeadPercent() {
 433:         return this.headPercent;   
 434:     }
 435:     
 436:     /**
 437:      * Sets the head percent and sends a {@link PlotChangeEvent} to all 
 438:      * registered listeners.
 439:      * 
 440:      * @param percent  the percent.
 441:      * 
 442:      * @see #getHeadPercent()
 443:      */
 444:     public void setHeadPercent(double percent) {
 445:         this.headPercent = percent;
 446:         notifyListeners(new PlotChangeEvent(this));
 447:     }
 448:     
 449:     /**
 450:      * Returns the start angle for the first radar axis.
 451:      * <BR>
 452:      * This is measured in degrees starting from 3 o'clock (Java Arc2D default)
 453:      * and measuring anti-clockwise.
 454:      * 
 455:      * @return The start angle.
 456:      * 
 457:      * @see #setStartAngle(double)
 458:      */
 459:     public double getStartAngle() {
 460:         return this.startAngle;
 461:     }
 462: 
 463:     /**
 464:      * Sets the starting angle and sends a {@link PlotChangeEvent} to all
 465:      * registered listeners.
 466:      * <P>
 467:      * The initial default value is 90 degrees, which corresponds to 12 o'clock.
 468:      * A value of zero corresponds to 3 o'clock... this is the encoding used by
 469:      * Java's Arc2D class.
 470:      * 
 471:      * @param angle  the angle (in degrees).
 472:      * 
 473:      * @see #getStartAngle()
 474:      */
 475:     public void setStartAngle(double angle) {
 476:         this.startAngle = angle;
 477:         notifyListeners(new PlotChangeEvent(this));
 478:     }
 479: 
 480:     /**
 481:      * Returns the maximum value any category axis can take.
 482:      * 
 483:      * @return The maximum value.
 484:      * 
 485:      * @see #setMaxValue(double)
 486:      */
 487:     public double getMaxValue() {
 488:         return this.maxValue;
 489:     }
 490: 
 491:     /**
 492:      * Sets the maximum value any category axis can take and sends 
 493:      * a {@link PlotChangeEvent} to all registered listeners.
 494:      * 
 495:      * @param value  the maximum value.
 496:      * 
 497:      * @see #getMaxValue()
 498:      */
 499:     public void setMaxValue(double value) {
 500:         this.maxValue = value;
 501:         notifyListeners(new PlotChangeEvent(this));
 502:     }
 503: 
 504:     /**
 505:      * Returns the direction in which the radar axes are drawn
 506:      * (clockwise or anti-clockwise).
 507:      * 
 508:      * @return The direction (never <code>null</code>).
 509:      * 
 510:      * @see #setDirection(Rotation)
 511:      */
 512:     public Rotation getDirection() {
 513:         return this.direction;
 514:     }
 515: 
 516:     /**
 517:      * Sets the direction in which the radar axes are drawn and sends a
 518:      * {@link PlotChangeEvent} to all registered listeners.
 519:      * 
 520:      * @param direction  the direction (<code>null</code> not permitted).
 521:      * 
 522:      * @see #getDirection()
 523:      */
 524:     public void setDirection(Rotation direction) {
 525:         if (direction == null) {
 526:             throw new IllegalArgumentException("Null 'direction' argument.");
 527:         }
 528:         this.direction = direction;
 529:         notifyListeners(new PlotChangeEvent(this));
 530:     }
 531: 
 532:     /**
 533:      * Returns the interior gap, measured as a percentage of the available 
 534:      * drawing space.
 535:      * 
 536:      * @return The gap (as a percentage of the available drawing space).
 537:      * 
 538:      * @see #setInteriorGap(double)
 539:      */
 540:     public double getInteriorGap() {
 541:         return this.interiorGap;
 542:     }
 543: 
 544:     /**
 545:      * Sets the interior gap and sends a {@link PlotChangeEvent} to all 
 546:      * registered listeners. This controls the space between the edges of the 
 547:      * plot and the plot area itself (the region where the axis labels appear).
 548:      * 
 549:      * @param percent  the gap (as a percentage of the available drawing space).
 550:      * 
 551:      * @see #getInteriorGap()
 552:      */
 553:     public void setInteriorGap(double percent) {
 554:         if ((percent < 0.0) || (percent > MAX_INTERIOR_GAP)) {
 555:             throw new IllegalArgumentException(
 556:                     "Percentage outside valid range.");
 557:         }
 558:         if (this.interiorGap != percent) {
 559:             this.interiorGap = percent;
 560:             notifyListeners(new PlotChangeEvent(this));
 561:         }
 562:     }
 563: 
 564:     /**
 565:      * Returns the axis label gap.
 566:      * 
 567:      * @return The axis label gap.
 568:      * 
 569:      * @see #setAxisLabelGap(double)
 570:      */
 571:     public double getAxisLabelGap() {
 572:         return this.axisLabelGap;   
 573:     }
 574:     
 575:     /**
 576:      * Sets the axis label gap and sends a {@link PlotChangeEvent} to all 
 577:      * registered listeners.
 578:      * 
 579:      * @param gap  the gap.
 580:      * 
 581:      * @see #getAxisLabelGap()
 582:      */
 583:     public void setAxisLabelGap(double gap) {
 584:         this.axisLabelGap = gap;
 585:         notifyListeners(new PlotChangeEvent(this));
 586:     }
 587:     
 588:     /**
 589:      * Returns the paint used to draw the axis lines.
 590:      * 
 591:      * @return The paint used to draw the axis lines (never <code>null</code>).
 592:      * 
 593:      * @see #setAxisLinePaint(Paint)
 594:      * @see #getAxisLineStroke()
 595:      * @since 1.0.4
 596:      */
 597:     public Paint getAxisLinePaint() {
 598:         return this.axisLinePaint;
 599:     }
 600:     
 601:     /**
 602:      * Sets the paint used to draw the axis lines and sends a 
 603:      * {@link PlotChangeEvent} to all registered listeners.
 604:      * 
 605:      * @param paint  the paint (<code>null</code> not permitted).
 606:      * 
 607:      * @see #getAxisLinePaint()
 608:      * @since 1.0.4
 609:      */
 610:     public void setAxisLinePaint(Paint paint) {
 611:         if (paint == null) {
 612:             throw new IllegalArgumentException("Null 'paint' argument.");
 613:         }
 614:         this.axisLinePaint = paint;
 615:         notifyListeners(new PlotChangeEvent(this));
 616:     }
 617:     
 618:     /**
 619:      * Returns the stroke used to draw the axis lines.
 620:      * 
 621:      * @return The stroke used to draw the axis lines (never <code>null</code>).
 622:      * 
 623:      * @see #setAxisLineStroke(Stroke)
 624:      * @see #getAxisLinePaint()
 625:      * @since 1.0.4
 626:      */
 627:     public Stroke getAxisLineStroke() {
 628:         return this.axisLineStroke;
 629:     }
 630:     
 631:     /**
 632:      * Sets the stroke used to draw the axis lines and sends a 
 633:      * {@link PlotChangeEvent} to all registered listeners.
 634:      * 
 635:      * @param stroke  the stroke (<code>null</code> not permitted).
 636:      * 
 637:      * @see #getAxisLineStroke()
 638:      * @since 1.0.4
 639:      */
 640:     public void setAxisLineStroke(Stroke stroke) {
 641:         if (stroke == null) {
 642:             throw new IllegalArgumentException("Null 'stroke' argument.");
 643:         }
 644:         this.axisLineStroke = stroke;
 645:         notifyListeners(new PlotChangeEvent(this));
 646:     }
 647:     
 648:     //// SERIES PAINT /////////////////////////
 649: 
 650:     /**
 651:      * Returns the paint for ALL series in the plot.
 652:      * 
 653:      * @return The paint (possibly <code>null</code>).
 654:      * 
 655:      * @see #setSeriesPaint(Paint)
 656:      */
 657:     public Paint getSeriesPaint() {
 658:         return this.seriesPaint;
 659:     }
 660: 
 661:     /**
 662:      * Sets the paint for ALL series in the plot. If this is set to</code> null
 663:      * </code>, then a list of paints is used instead (to allow different colors
 664:      * to be used for each series of the radar group).
 665:      * 
 666:      * @param paint the paint (<code>null</code> permitted).
 667:      * 
 668:      * @see #getSeriesPaint()
 669:      */
 670:     public void setSeriesPaint(Paint paint) {
 671:         this.seriesPaint = paint;
 672:         notifyListeners(new PlotChangeEvent(this));
 673:     }
 674: 
 675:     /**
 676:      * Returns the paint for the specified series.
 677:      * 
 678:      * @param series  the series index (zero-based).
 679:      * 
 680:      * @return The paint (never <code>null</code>).
 681:      * 
 682:      * @see #setSeriesPaint(int, Paint)
 683:      */
 684:     public Paint getSeriesPaint(int series) {
 685: 
 686:         // return the override, if there is one...
 687:         if (this.seriesPaint != null) {
 688:             return this.seriesPaint;
 689:         }
 690: 
 691:         // otherwise look up the paint list
 692:         Paint result = this.seriesPaintList.getPaint(series);
 693:         if (result == null) {
 694:             DrawingSupplier supplier = getDrawingSupplier();
 695:             if (supplier != null) {
 696:                 Paint p = supplier.getNextPaint();
 697:                 this.seriesPaintList.setPaint(series, p);
 698:                 result = p;
 699:             }
 700:             else {
 701:                 result = this.baseSeriesPaint;
 702:             }
 703:         }
 704:         return result;
 705: 
 706:     }
 707: 
 708:     /**
 709:      * Sets the paint used to fill a series of the radar and sends a
 710:      * {@link PlotChangeEvent} to all registered listeners.
 711:      * 
 712:      * @param series  the series index (zero-based).
 713:      * @param paint  the paint (<code>null</code> permitted).
 714:      * 
 715:      * @see #getSeriesPaint(int)
 716:      */
 717:     public void setSeriesPaint(int series, Paint paint) {
 718:         this.seriesPaintList.setPaint(series, paint);
 719:         notifyListeners(new PlotChangeEvent(this));
 720:     }
 721: 
 722:     /**
 723:      * Returns the base series paint. This is used when no other paint is
 724:      * available.
 725:      * 
 726:      * @return The paint (never <code>null</code>).
 727:      * 
 728:      * @see #setBaseSeriesPaint(Paint)
 729:      */
 730:     public Paint getBaseSeriesPaint() {
 731:       return this.baseSeriesPaint;
 732:     }
 733: 
 734:     /**
 735:      * Sets the base series paint.
 736:      * 
 737:      * @param paint  the paint (<code>null</code> not permitted).
 738:      * 
 739:      * @see #getBaseSeriesPaint()
 740:      */
 741:     public void setBaseSeriesPaint(Paint paint) {
 742:         if (paint == null) {
 743:             throw new IllegalArgumentException("Null 'paint' argument.");
 744:         }
 745:         this.baseSeriesPaint = paint;
 746:         notifyListeners(new PlotChangeEvent(this));
 747:     }
 748: 
 749:     //// SERIES OUTLINE PAINT ////////////////////////////
 750: 
 751:     /**
 752:      * Returns the outline paint for ALL series in the plot.
 753:      * 
 754:      * @return The paint (possibly <code>null</code>).
 755:      */
 756:     public Paint getSeriesOutlinePaint() {
 757:         return this.seriesOutlinePaint;
 758:     }
 759: 
 760:     /**
 761:      * Sets the outline paint for ALL series in the plot. If this is set to
 762:      * </code> null</code>, then a list of paints is used instead (to allow
 763:      * different colors to be used for each series).
 764:      * 
 765:      * @param paint  the paint (<code>null</code> permitted).
 766:      */
 767:     public void setSeriesOutlinePaint(Paint paint) {
 768:         this.seriesOutlinePaint = paint;
 769:         notifyListeners(new PlotChangeEvent(this));
 770:     }
 771: 
 772:     /**
 773:      * Returns the paint for the specified series.
 774:      * 
 775:      * @param series  the series index (zero-based).
 776:      * 
 777:      * @return The paint (never <code>null</code>).
 778:      */
 779:     public Paint getSeriesOutlinePaint(int series) {
 780:         // return the override, if there is one...
 781:         if (this.seriesOutlinePaint != null) {
 782:             return this.seriesOutlinePaint;
 783:         }
 784:         // otherwise look up the paint list
 785:         Paint result = this.seriesOutlinePaintList.getPaint(series);
 786:         if (result == null) {
 787:             result = this.baseSeriesOutlinePaint;
 788:         }
 789:         return result;
 790:     }
 791: 
 792:     /**
 793:      * Sets the paint used to fill a series of the radar and sends a
 794:      * {@link PlotChangeEvent} to all registered listeners.
 795:      * 
 796:      * @param series  the series index (zero-based).
 797:      * @param paint  the paint (<code>null</code> permitted).
 798:      */
 799:     public void setSeriesOutlinePaint(int series, Paint paint) {
 800:         this.seriesOutlinePaintList.setPaint(series, paint);
 801:         notifyListeners(new PlotChangeEvent(this));  
 802:     }
 803: 
 804:     /**
 805:      * Returns the base series paint. This is used when no other paint is
 806:      * available.
 807:      * 
 808:      * @return The paint (never <code>null</code>).
 809:      */
 810:     public Paint getBaseSeriesOutlinePaint() {
 811:         return this.baseSeriesOutlinePaint;
 812:     }
 813: 
 814:     /**
 815:      * Sets the base series paint.
 816:      * 
 817:      * @param paint  the paint (<code>null</code> not permitted).
 818:      */
 819:     public void setBaseSeriesOutlinePaint(Paint paint) {
 820:         if (paint == null) {
 821:             throw new IllegalArgumentException("Null 'paint' argument.");
 822:         }
 823:         this.baseSeriesOutlinePaint = paint;
 824:         notifyListeners(new PlotChangeEvent(this));
 825:     }
 826: 
 827:     //// SERIES OUTLINE STROKE /////////////////////
 828: 
 829:     /**
 830:      * Returns the outline stroke for ALL series in the plot.
 831:      * 
 832:      * @return The stroke (possibly <code>null</code>).
 833:      */
 834:     public Stroke getSeriesOutlineStroke() {
 835:         return this.seriesOutlineStroke;
 836:     }
 837: 
 838:     /**
 839:      * Sets the outline stroke for ALL series in the plot. If this is set to
 840:      * </code> null</code>, then a list of paints is used instead (to allow
 841:      * different colors to be used for each series).
 842:      * 
 843:      * @param stroke  the stroke (<code>null</code> permitted).
 844:      */
 845:     public void setSeriesOutlineStroke(Stroke stroke) {
 846:         this.seriesOutlineStroke = stroke;
 847:         notifyListeners(new PlotChangeEvent(this));
 848:     }
 849: 
 850:     /**
 851:      * Returns the stroke for the specified series.
 852:      * 
 853:      * @param series  the series index (zero-based).
 854:      * 
 855:      * @return The stroke (never <code>null</code>).
 856:      */
 857:     public Stroke getSeriesOutlineStroke(int series) {
 858: 
 859:         // return the override, if there is one...
 860:         if (this.seriesOutlineStroke != null) {
 861:             return this.seriesOutlineStroke;
 862:         }
 863: 
 864:         // otherwise look up the paint list
 865:         Stroke result = this.seriesOutlineStrokeList.getStroke(series);
 866:         if (result == null) {
 867:             result = this.baseSeriesOutlineStroke;
 868:         }
 869:         return result;
 870: 
 871:     }
 872: 
 873:     /**
 874:      * Sets the stroke used to fill a series of the radar and sends a
 875:      * {@link PlotChangeEvent} to all registered listeners.
 876:      * 
 877:      * @param series  the series index (zero-based).
 878:      * @param stroke  the stroke (<code>null</code> permitted).
 879:      */
 880:     public void setSeriesOutlineStroke(int series, Stroke stroke) {
 881:         this.seriesOutlineStrokeList.setStroke(series, stroke);
 882:         notifyListeners(new PlotChangeEvent(this));
 883:     }
 884: 
 885:     /**
 886:      * Returns the base series stroke. This is used when no other stroke is
 887:      * available.
 888:      * 
 889:      * @return The stroke (never <code>null</code>).
 890:      */
 891:     public Stroke getBaseSeriesOutlineStroke() {
 892:         return this.baseSeriesOutlineStroke;
 893:     }
 894: 
 895:     /**
 896:      * Sets the base series stroke.
 897:      * 
 898:      * @param stroke  the stroke (<code>null</code> not permitted).
 899:      */
 900:     public void setBaseSeriesOutlineStroke(Stroke stroke) {
 901:         if (stroke == null) {
 902:             throw new IllegalArgumentException("Null 'stroke' argument.");
 903:         }
 904:         this.baseSeriesOutlineStroke = stroke;
 905:         notifyListeners(new PlotChangeEvent(this));
 906:     }
 907: 
 908:     /**
 909:      * Returns the shape used for legend items.
 910:      * 
 911:      * @return The shape (never <code>null</code>).
 912:      * 
 913:      * @see #setLegendItemShape(Shape)
 914:      */
 915:     public Shape getLegendItemShape() {
 916:         return this.legendItemShape;
 917:     }
 918: 
 919:     /**
 920:      * Sets the shape used for legend items and sends a {@link PlotChangeEvent} 
 921:      * to all registered listeners.
 922:      * 
 923:      * @param shape  the shape (<code>null</code> not permitted).
 924:      * 
 925:      * @see #getLegendItemShape()
 926:      */
 927:     public void setLegendItemShape(Shape shape) {
 928:         if (shape == null) {
 929:             throw new IllegalArgumentException("Null 'shape' argument.");
 930:         }
 931:         this.legendItemShape = shape;
 932:         notifyListeners(new PlotChangeEvent(this));
 933:     }
 934: 
 935:     /**
 936:      * Returns the series label font.
 937:      * 
 938:      * @return The font (never <code>null</code>).
 939:      * 
 940:      * @see #setLabelFont(Font)
 941:      */
 942:     public Font getLabelFont() {
 943:         return this.labelFont;
 944:     }
 945: 
 946:     /**
 947:      * Sets the series label font and sends a {@link PlotChangeEvent} to all
 948:      * registered listeners.
 949:      * 
 950:      * @param font  the font (<code>null</code> not permitted).
 951:      * 
 952:      * @see #getLabelFont()
 953:      */
 954:     public void setLabelFont(Font font) {
 955:         if (font == null) {
 956:             throw new IllegalArgumentException("Null 'font' argument.");
 957:         }
 958:         this.labelFont = font;
 959:         notifyListeners(new PlotChangeEvent(this));
 960:     }
 961: 
 962:     /**
 963:      * Returns the series label paint.
 964:      * 
 965:      * @return The paint (never <code>null</code>).
 966:      * 
 967:      * @see #setLabelPaint(Paint)
 968:      */
 969:     public Paint getLabelPaint() {
 970:         return this.labelPaint;
 971:     }
 972: 
 973:     /**
 974:      * Sets the series label paint and sends a {@link PlotChangeEvent} to all
 975:      * registered listeners.
 976:      * 
 977:      * @param paint  the paint (<code>null</code> not permitted).
 978:      * 
 979:      * @see #getLabelPaint()
 980:      */
 981:     public void setLabelPaint(Paint paint) {
 982:         if (paint == null) {
 983:             throw new IllegalArgumentException("Null 'paint' argument.");
 984:         }
 985:         this.labelPaint = paint;
 986:         notifyListeners(new PlotChangeEvent(this));
 987:     }
 988: 
 989:     /**
 990:      * Returns the label generator.
 991:      * 
 992:      * @return The label generator (never <code>null</code>).
 993:      * 
 994:      * @see #setLabelGenerator(CategoryItemLabelGenerator)
 995:      */
 996:     public CategoryItemLabelGenerator getLabelGenerator() {
 997:         return this.labelGenerator;   
 998:     }
 999:     
1000:     /**
1001:      * Sets the label generator and sends a {@link PlotChangeEvent} to all
1002:      * registered listeners.
1003:      * 
1004:      * @param generator  the generator (<code>null</code> not permitted).
1005:      * 
1006:      * @see #getLabelGenerator()
1007:      */
1008:     public void setLabelGenerator(CategoryItemLabelGenerator generator) {
1009:         if (generator == null) {
1010:             throw new IllegalArgumentException("Null 'generator' argument.");   
1011:         }
1012:         this.labelGenerator = generator;    
1013:     }
1014:     
1015:     /**
1016:      * Returns the tool tip generator for the plot.
1017:      * 
1018:      * @return The tool tip generator (possibly <code>null</code>).
1019:      * 
1020:      * @see #setToolTipGenerator(CategoryToolTipGenerator)
1021:      * 
1022:      * @since 1.0.2
1023:      */
1024:     public CategoryToolTipGenerator getToolTipGenerator() {
1025:         return this.toolTipGenerator;    
1026:     }
1027:     
1028:     /**
1029:      * Sets the tool tip generator for the plot and sends a 
1030:      * {@link PlotChangeEvent} to all registered listeners.
1031:      * 
1032:      * @param generator  the generator (<code>null</code> permitted).
1033:      * 
1034:      * @see #getToolTipGenerator()
1035:      * 
1036:      * @since 1.0.2
1037:      */
1038:     public void setToolTipGenerator(CategoryToolTipGenerator generator) {
1039:         this.toolTipGenerator = generator;
1040:         this.notifyListeners(new PlotChangeEvent(this));
1041:     }
1042:     
1043:     /**
1044:      * Returns the URL generator for the plot.
1045:      * 
1046:      * @return The URL generator (possibly <code>null</code>).
1047:      * 
1048:      * @see #setURLGenerator(CategoryURLGenerator)
1049:      * 
1050:      * @since 1.0.2
1051:      */
1052:     public CategoryURLGenerator getURLGenerator() {
1053:         return this.urlGenerator;    
1054:     }
1055:     
1056:     /**
1057:      * Sets the URL generator for the plot and sends a 
1058:      * {@link PlotChangeEvent} to all registered listeners.
1059:      * 
1060:      * @param generator  the generator (<code>null</code> permitted).
1061:      * 
1062:      * @see #getURLGenerator()
1063:      * 
1064:      * @since 1.0.2
1065:      */
1066:     public void setURLGenerator(CategoryURLGenerator generator) {
1067:         this.urlGenerator = generator;
1068:         this.notifyListeners(new PlotChangeEvent(this));
1069:     }
1070:     
1071:     /**
1072:      * Returns a collection of legend items for the radar chart.
1073:      * 
1074:      * @return The legend items.
1075:      */
1076:     public LegendItemCollection getLegendItems() {
1077:         LegendItemCollection result = new LegendItemCollection();
1078: 
1079:         List keys = null;
1080: 
1081:         if (this.dataExtractOrder == TableOrder.BY_ROW) {
1082:             keys = this.dataset.getRowKeys();
1083:         }
1084:         else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1085:             keys = this.dataset.getColumnKeys();
1086:         }
1087: 
1088:         if (keys != null) {
1089:             int series = 0;
1090:             Iterator iterator = keys.iterator();
1091:             Shape shape = getLegendItemShape();
1092: 
1093:             while (iterator.hasNext()) {
1094:                 String label = iterator.next().toString();
1095:                 String description = label;
1096: 
1097:                 Paint paint = getSeriesPaint(series);
1098:                 Paint outlinePaint = getSeriesOutlinePaint(series);
1099:                 Stroke stroke = getSeriesOutlineStroke(series);
1100:                 LegendItem item = new LegendItem(label, description, 
1101:                         null, null, shape, paint, stroke, outlinePaint);
1102:                 item.setDataset(getDataset());
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(
1342:                                 (int) (point.getX() - headW),
1343:                                 (int) (point.getY() - headH), 
1344:                                 (int) (headW * 2), (int) (headH * 2));
1345:                         CategoryItemEntity entity = new CategoryItemEntity(
1346:                                 area, tip, url, this.dataset, 
1347:                                 this.dataset.getRowKey(series),
1348:                                 this.dataset.getColumnKey(cat)); 
1349:                         entities.add(entity);                                
1350:                     }
1351: 
1352:                 }
1353:             }
1354:         }
1355:         // Plot the polygon
1356:     
1357:         Paint paint = getSeriesPaint(series);
1358:         g2.setPaint(paint);
1359:         g2.setStroke(getSeriesOutlineStroke(series));
1360:         g2.draw(polygon);
1361: 
1362:         // Lastly, fill the web polygon if this is required
1363:     
1364:         if (this.webFilled) {
1365:             g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
1366:                     0.1f));
1367:             g2.fill(polygon);
1368:             g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
1369:                     getForegroundAlpha()));
1370:         }
1371:     }
1372: 
1373:     /**
1374:      * Returns the value to be plotted at the interseries of the 
1375:      * series and the category.  This allows us to plot
1376:      * <code>BY_ROW</code> or <code>BY_COLUMN</code> which basically is just 
1377:      * reversing the definition of the categories and data series being 
1378:      * plotted.
1379:      * 
1380:      * @param series the series to be plotted.
1381:      * @param cat the category within the series to be plotted.
1382:      * 
1383:      * @return The value to be plotted (possibly <code>null</code>).
1384:      * 
1385:      * @see #getDataExtractOrder()
1386:      */
1387:     protected Number getPlotValue(int series, int cat) {
1388:         Number value = null;
1389:         if (this.dataExtractOrder == TableOrder.BY_ROW) {
1390:             value = this.dataset.getValue(series, cat);
1391:         }
1392:         else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
1393:             value = this.dataset.getValue(cat, series);
1394:         }
1395:         return value;
1396:     }
1397: 
1398:     /**
1399:      * Draws the label for one axis.
1400:      * 
1401:      * @param g2  the graphics device.
1402:      * @param plotArea  the plot area
1403:      * @param value  the value of the label (ignored).
1404:      * @param cat  the category (zero-based index).
1405:      * @param startAngle  the starting angle.
1406:      * @param extent  the extent of the arc.
1407:      */
1408:     protected void drawLabel(Graphics2D g2, Rectangle2D plotArea, double value, 
1409:                              int cat, double startAngle, double extent) {
1410:         FontRenderContext frc = g2.getFontRenderContext();
1411:  
1412:         String label = null;
1413:         if (this.dataExtractOrder == TableOrder.BY_ROW) {
1414:             // if series are in rows, then the categories are the column keys
1415:             label = this.labelGenerator.generateColumnLabel(this.dataset, cat);
1416:         }
1417:         else {
1418:             // if series are in columns, then the categories are the row keys
1419:             label = this.labelGenerator.generateRowLabel(this.dataset, cat);
1420:         }
1421:  
1422:         Rectangle2D labelBounds = getLabelFont().getStringBounds(label, frc);
1423:         LineMetrics lm = getLabelFont().getLineMetrics(label, frc);
1424:         double ascent = lm.getAscent();
1425: 
1426:         Point2D labelLocation = calculateLabelLocation(labelBounds, ascent, 
1427:                 plotArea, startAngle);
1428: 
1429:         Composite saveComposite = g2.getComposite();
1430:     
1431:         g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
1432:                 1.0f));
1433:         g2.setPaint(getLabelPaint());
1434:         g2.setFont(getLabelFont());
1435:         g2.drawString(label, (float) labelLocation.getX(), 
1436:                 (float) labelLocation.getY());
1437:         g2.setComposite(saveComposite);
1438:     }
1439: 
1440:     /**
1441:      * Returns the location for a label
1442:      * 
1443:      * @param labelBounds the label bounds.
1444:      * @param ascent the ascent (height of font).
1445:      * @param plotArea the plot area
1446:      * @param startAngle the start angle for the pie series.
1447:      * 
1448:      * @return The location for a label.
1449:      */
1450:     protected Point2D calculateLabelLocation(Rectangle2D labelBounds, 
1451:                                              double ascent,
1452:                                              Rectangle2D plotArea, 
1453:                                              double startAngle)
1454:     {
1455:         Arc2D arc1 = new Arc2D.Double(plotArea, startAngle, 0, Arc2D.OPEN);
1456:         Point2D point1 = arc1.getEndPoint();
1457: 
1458:         double deltaX = -(point1.getX() - plotArea.getCenterX()) 
1459:                         * this.axisLabelGap;
1460:         double deltaY = -(point1.getY() - plotArea.getCenterY()) 
1461:                         * this.axisLabelGap;
1462: 
1463:         double labelX = point1.getX() - deltaX;
1464:         double labelY = point1.getY() - deltaY;
1465: 
1466:         if (labelX < plotArea.getCenterX()) {
1467:             labelX -= labelBounds.getWidth();
1468:         }
1469:     
1470:         if (labelX == plotArea.getCenterX()) {
1471:             labelX -= labelBounds.getWidth() / 2;
1472:         }
1473: 
1474:         if (labelY > plotArea.getCenterY()) {
1475:             labelY += ascent;
1476:         }
1477: 
1478:         return new Point2D.Double(labelX, labelY);
1479:     }
1480:     
1481:     /**
1482:      * Tests this plot for equality with an arbitrary object.
1483:      * 
1484:      * @param obj  the object (<code>null</code> permitted).
1485:      * 
1486:      * @return A boolean.
1487:      */
1488:     public boolean equals(Object obj) {
1489:         if (obj == this) {
1490:             return true;   
1491:         }
1492:         if (!(obj instanceof SpiderWebPlot)) {
1493:             return false;   
1494:         }
1495:         if (!super.equals(obj)) {
1496:             return false;   
1497:         }
1498:         SpiderWebPlot that = (SpiderWebPlot) obj;
1499:         if (!this.dataExtractOrder.equals(that.dataExtractOrder)) {
1500:             return false;   
1501:         }
1502:         if (this.headPercent != that.headPercent) {
1503:             return false;   
1504:         }
1505:         if (this.interiorGap != that.interiorGap) {
1506:             return false;   
1507:         }
1508:         if (this.startAngle != that.startAngle) {
1509:             return false;   
1510:         }
1511:         if (!this.direction.equals(that.direction)) {
1512:             return false;   
1513:         }
1514:         if (this.maxValue != that.maxValue) {
1515:             return false;   
1516:         }
1517:         if (this.webFilled != that.webFilled) {
1518:             return false;   
1519:         }
1520:         if (this.axisLabelGap != that.axisLabelGap) {
1521:             return false;
1522:         }
1523:         if (!PaintUtilities.equal(this.axisLinePaint, that.axisLinePaint)) {
1524:             return false;
1525:         }
1526:         if (!this.axisLineStroke.equals(that.axisLineStroke)) {
1527:             return false;
1528:         }
1529:         if (!ShapeUtilities.equal(this.legendItemShape, that.legendItemShape)) {
1530:             return false;   
1531:         }
1532:         if (!PaintUtilities.equal(this.seriesPaint, that.seriesPaint)) {
1533:             return false;   
1534:         }
1535:         if (!this.seriesPaintList.equals(that.seriesPaintList)) {
1536:             return false;   
1537:         }
1538:         if (!PaintUtilities.equal(this.baseSeriesPaint, that.baseSeriesPaint)) {
1539:             return false;   
1540:         }
1541:         if (!PaintUtilities.equal(this.seriesOutlinePaint, 
1542:                 that.seriesOutlinePaint)) {
1543:             return false;   
1544:         }
1545:         if (!this.seriesOutlinePaintList.equals(that.seriesOutlinePaintList)) {
1546:             return false;   
1547:         }
1548:         if (!PaintUtilities.equal(this.baseSeriesOutlinePaint, 
1549:                 that.baseSeriesOutlinePaint)) {
1550:             return false;   
1551:         }
1552:         if (!ObjectUtilities.equal(this.seriesOutlineStroke, 
1553:                 that.seriesOutlineStroke)) {
1554:             return false;   
1555:         }
1556:         if (!this.seriesOutlineStrokeList.equals(
1557:                 that.seriesOutlineStrokeList)) {
1558:             return false;   
1559:         }
1560:         if (!this.baseSeriesOutlineStroke.equals(
1561:                 that.baseSeriesOutlineStroke)) {
1562:             return false;   
1563:         }
1564:         if (!this.labelFont.equals(that.labelFont)) {
1565:             return false;   
1566:         }
1567:         if (!PaintUtilities.equal(this.labelPaint, that.labelPaint)) {
1568:             return false;   
1569:         }
1570:         if (!this.labelGenerator.equals(that.labelGenerator)) {
1571:             return false;   
1572:         }
1573:         if (!ObjectUtilities.equal(this.toolTipGenerator, 
1574:                 that.toolTipGenerator)) {
1575:             return false;
1576:         }
1577:         if (!ObjectUtilities.equal(this.urlGenerator,
1578:                 that.urlGenerator)) {
1579:             return false;
1580:         }
1581:         return true;
1582:     }
1583:     
1584:     /**
1585:      * Returns a clone of this plot.
1586:      * 
1587:      * @return A clone of this plot.
1588:      * 
1589:      * @throws CloneNotSupportedException if the plot cannot be cloned for 
1590:      *         any reason.
1591:      */
1592:     public Object clone() throws CloneNotSupportedException {
1593:         SpiderWebPlot clone = (SpiderWebPlot) super.clone();
1594:         clone.legendItemShape = ShapeUtilities.clone(this.legendItemShape);
1595:         clone.seriesPaintList = (PaintList) this.seriesPaintList.clone();
1596:         clone.seriesOutlinePaintList 
1597:                 = (PaintList) this.seriesOutlinePaintList.clone();
1598:         clone.seriesOutlineStrokeList 
1599:                 = (StrokeList) this.seriesOutlineStrokeList.clone();
1600:         return clone;
1601:     }
1602:     
1603:     /**
1604:      * Provides serialization support.
1605:      *
1606:      * @param stream  the output stream.
1607:      *
1608:      * @throws IOException  if there is an I/O error.
1609:      */
1610:     private void writeObject(ObjectOutputStream stream) throws IOException {
1611:         stream.defaultWriteObject();
1612: 
1613:         SerialUtilities.writeShape(this.legendItemShape, stream);
1614:         SerialUtilities.writePaint(this.seriesPaint, stream);
1615:         SerialUtilities.writePaint(this.baseSeriesPaint, stream);
1616:         SerialUtilities.writePaint(this.seriesOutlinePaint, stream);
1617:         SerialUtilities.writePaint(this.baseSeriesOutlinePaint, stream);
1618:         SerialUtilities.writeStroke(this.seriesOutlineStroke, stream);
1619:         SerialUtilities.writeStroke(this.baseSeriesOutlineStroke, stream);
1620:         SerialUtilities.writePaint(this.labelPaint, stream);
1621:         SerialUtilities.writePaint(this.axisLinePaint, stream);
1622:         SerialUtilities.writeStroke(this.axisLineStroke, stream);
1623:     }
1624: 
1625:     /**
1626:      * Provides serialization support.
1627:      *
1628:      * @param stream  the input stream.
1629:      *
1630:      * @throws IOException  if there is an I/O error.
1631:      * @throws ClassNotFoundException  if there is a classpath problem.
1632:      */
1633:     private void readObject(ObjectInputStream stream) throws IOException,
1634:             ClassNotFoundException {
1635:         stream.defaultReadObject();
1636: 
1637:         this.legendItemShape = SerialUtilities.readShape(stream);
1638:         this.seriesPaint = SerialUtilities.readPaint(stream);
1639:         this.baseSeriesPaint = SerialUtilities.readPaint(stream);
1640:         this.seriesOutlinePaint = SerialUtilities.readPaint(stream);
1641:         this.baseSeriesOutlinePaint = SerialUtilities.readPaint(stream);
1642:         this.seriesOutlineStroke = SerialUtilities.readStroke(stream);
1643:         this.baseSeriesOutlineStroke = SerialUtilities.readStroke(stream);
1644:         this.labelPaint = SerialUtilities.readPaint(stream);
1645:         this.axisLinePaint = SerialUtilities.readPaint(stream);
1646:         this.axisLineStroke = SerialUtilities.readStroke(stream);
1647:         if (this.dataset != null) {
1648:             this.dataset.addChangeListener(this);
1649:         }
1650:     } 
1651: 
1652: }