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