Source for org.jfree.chart.renderer.category.BoxAndWhiskerRenderer

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2005, 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:  * BoxAndWhiskerRenderer.java
  29:  * --------------------------
  30:  * (C) Copyright 2003-2005, by David Browning and Contributors.
  31:  *
  32:  * Original Author:  David Browning (for the Australian Institute of Marine 
  33:  *                   Science);
  34:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  35:  *                   Tim Bardzil;
  36:  *
  37:  * $Id: BoxAndWhiskerRenderer.java,v 1.8.2.5 2005/12/01 17:21:14 mungady Exp $
  38:  *
  39:  * Changes
  40:  * -------
  41:  * 21-Aug-2003 : Version 1, contributed by David Browning (for the Australian 
  42:  *               Institute of Marine Science);
  43:  * 01-Sep-2003 : Incorporated outlier and farout symbols for low values 
  44:  *               also (DG);
  45:  * 08-Sep-2003 : Changed ValueAxis API (DG);
  46:  * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
  47:  * 07-Oct-2003 : Added renderer state (DG);
  48:  * 12-Nov-2003 : Fixed casting bug reported by Tim Bardzil (DG);
  49:  * 13-Nov-2003 : Added drawHorizontalItem() method contributed by Tim 
  50:  *               Bardzil (DG);
  51:  * 25-Apr-2004 : Added fillBox attribute, equals() method and added 
  52:  *               serialization code (DG);
  53:  * 29-Apr-2004 : Changed drawing of upper and lower shadows - see bug report 
  54:  *               944011 (DG);
  55:  * 05-Nov-2004 : Modified drawItem() signature (DG);
  56:  * 09-Mar-2005 : Override getLegendItem() method so that legend item shapes
  57:  *               are shown as blocks (DG);
  58:  * 20-Apr-2005 : Generate legend labels, tooltips and URLs (DG);
  59:  * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG);
  60:  * 
  61:  */
  62: 
  63: package org.jfree.chart.renderer.category;
  64: 
  65: import java.awt.Color;
  66: import java.awt.Graphics2D;
  67: import java.awt.Paint;
  68: import java.awt.Shape;
  69: import java.awt.Stroke;
  70: import java.awt.geom.Ellipse2D;
  71: import java.awt.geom.Line2D;
  72: import java.awt.geom.Point2D;
  73: import java.awt.geom.Rectangle2D;
  74: import java.io.IOException;
  75: import java.io.ObjectInputStream;
  76: import java.io.ObjectOutputStream;
  77: import java.io.Serializable;
  78: import java.util.ArrayList;
  79: import java.util.Collections;
  80: import java.util.Iterator;
  81: import java.util.List;
  82: 
  83: import org.jfree.chart.LegendItem;
  84: import org.jfree.chart.axis.CategoryAxis;
  85: import org.jfree.chart.axis.ValueAxis;
  86: import org.jfree.chart.entity.CategoryItemEntity;
  87: import org.jfree.chart.entity.EntityCollection;
  88: import org.jfree.chart.event.RendererChangeEvent;
  89: import org.jfree.chart.labels.CategoryToolTipGenerator;
  90: import org.jfree.chart.plot.CategoryPlot;
  91: import org.jfree.chart.plot.PlotOrientation;
  92: import org.jfree.chart.plot.PlotRenderingInfo;
  93: import org.jfree.chart.renderer.Outlier;
  94: import org.jfree.chart.renderer.OutlierList;
  95: import org.jfree.chart.renderer.OutlierListCollection;
  96: import org.jfree.data.category.CategoryDataset;
  97: import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset;
  98: import org.jfree.io.SerialUtilities;
  99: import org.jfree.ui.RectangleEdge;
 100: import org.jfree.util.PaintUtilities;
 101: import org.jfree.util.PublicCloneable;
 102: 
 103: /**
 104:  * A box-and-whisker renderer.
 105:  */
 106: public class BoxAndWhiskerRenderer extends AbstractCategoryItemRenderer 
 107:                                    implements Cloneable, PublicCloneable, 
 108:                                               Serializable {
 109: 
 110:     /** For serialization. */
 111:     private static final long serialVersionUID = 632027470694481177L;
 112:     
 113:     /** The color used to paint the median line and average marker. */
 114:     private transient Paint artifactPaint;
 115: 
 116:     /** A flag that controls whether or not the box is filled. */
 117:     private boolean fillBox;
 118:     
 119:     /** The margin between items (boxes) within a category. */
 120:     private double itemMargin;
 121: 
 122:     /**
 123:      * Default constructor.
 124:      */
 125:     public BoxAndWhiskerRenderer() {
 126:         this.artifactPaint = Color.black;
 127:         this.fillBox = true;
 128:         this.itemMargin = 0.20;
 129:     }
 130: 
 131:     /**
 132:      * Returns the paint used to color the median and average markers.
 133:      * 
 134:      * @return A paint.
 135:      */
 136:     public Paint getArtifactPaint() {
 137:         return this.artifactPaint;
 138:     }
 139: 
 140:     /**
 141:      * Sets the paint used to color the median and average markers.
 142:      * 
 143:      * @param paint  the paint.
 144:      */
 145:     public void setArtifactPaint(Paint paint) {
 146:         this.artifactPaint = paint;
 147:     }
 148: 
 149:     /**
 150:      * Returns the flag that controls whether or not the box is filled.
 151:      * 
 152:      * @return A boolean.
 153:      */
 154:     public boolean getFillBox() {
 155:         return this.fillBox;   
 156:     }
 157:     
 158:     /**
 159:      * Sets the flag that controls whether or not the box is filled and sends a 
 160:      * {@link RendererChangeEvent} to all registered listeners.
 161:      * 
 162:      * @param flag  the flag.
 163:      */
 164:     public void setFillBox(boolean flag) {
 165:         this.fillBox = flag;
 166:         notifyListeners(new RendererChangeEvent(this));
 167:     }
 168: 
 169:     /**
 170:      * Returns the item margin.  This is a percentage of the available space 
 171:      * that is allocated to the space between items in the chart.
 172:      * 
 173:      * @return The margin.
 174:      */
 175:     public double getItemMargin() {
 176:         return this.itemMargin;
 177:     }
 178: 
 179:     /**
 180:      * Sets the item margin.
 181:      * 
 182:      * @param margin  the margin.
 183:      */
 184:     public void setItemMargin(double margin) {
 185:         this.itemMargin = margin;
 186:     }
 187: 
 188:     /**
 189:      * Returns a legend item for a series.
 190:      *
 191:      * @param datasetIndex  the dataset index (zero-based).
 192:      * @param series  the series index (zero-based).
 193:      *
 194:      * @return The legend item.
 195:      */
 196:     public LegendItem getLegendItem(int datasetIndex, int series) {
 197: 
 198:         CategoryPlot cp = getPlot();
 199:         if (cp == null) {
 200:             return null;
 201:         }
 202: 
 203:         CategoryDataset dataset;
 204:         dataset = cp.getDataset(datasetIndex);
 205:         String label = getLegendItemLabelGenerator().generateLabel(
 206:             dataset, series
 207:         );
 208:         String description = label;
 209:         String toolTipText = null; 
 210:         if (getLegendItemToolTipGenerator() != null) {
 211:             toolTipText = getLegendItemToolTipGenerator().generateLabel(
 212:                 dataset, series
 213:             );   
 214:         }
 215:         String urlText = null;
 216:         if (getLegendItemURLGenerator() != null) {
 217:             urlText = getLegendItemURLGenerator().generateLabel(
 218:                 dataset, series
 219:             );   
 220:         }
 221:         Shape shape = new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0);
 222:         Paint paint = getSeriesPaint(series);
 223:         Paint outlinePaint = getSeriesOutlinePaint(series);
 224:         Stroke outlineStroke = getSeriesOutlineStroke(series);
 225: 
 226:         return new LegendItem(label, description, toolTipText, urlText, 
 227:             shape, paint, outlineStroke, outlinePaint);
 228: 
 229:     }
 230: 
 231:     /**
 232:      * Initialises the renderer.  This method gets called once at the start of 
 233:      * the process of drawing a chart.
 234:      *
 235:      * @param g2  the graphics device.
 236:      * @param dataArea  the area in which the data is to be plotted.
 237:      * @param plot  the plot.
 238:      * @param rendererIndex  the renderer index.
 239:      * @param info  collects chart rendering information for return to caller.
 240:      *
 241:      * @return The renderer state.
 242:      */
 243:     public CategoryItemRendererState initialise(Graphics2D g2,
 244:                                                 Rectangle2D dataArea,
 245:                                                 CategoryPlot plot,
 246:                                                 int rendererIndex,
 247:                                                 PlotRenderingInfo info) {
 248: 
 249:         CategoryItemRendererState state = super.initialise(
 250:             g2, dataArea, plot, rendererIndex, info
 251:         );
 252: 
 253:         // calculate the box width
 254:         CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex);
 255:         CategoryDataset dataset = plot.getDataset(rendererIndex);
 256:         if (dataset != null) {
 257:             int columns = dataset.getColumnCount();
 258:             int rows = dataset.getRowCount();
 259:             double space = 0.0;
 260:             PlotOrientation orientation = plot.getOrientation();
 261:             if (orientation == PlotOrientation.HORIZONTAL) {
 262:                 space = dataArea.getHeight();
 263:             }
 264:             else if (orientation == PlotOrientation.VERTICAL) {
 265:                 space = dataArea.getWidth();
 266:             }
 267:             double categoryMargin = 0.0;
 268:             double currentItemMargin = 0.0;
 269:             if (columns > 1) {
 270:                 categoryMargin = domainAxis.getCategoryMargin();
 271:             }
 272:             if (rows > 1) {
 273:                 currentItemMargin = getItemMargin();
 274:             }
 275:             double used = space * (1 - domainAxis.getLowerMargin() 
 276:                                      - domainAxis.getUpperMargin()
 277:                                      - categoryMargin - currentItemMargin);
 278:             if ((rows * columns) > 0) {
 279:                 state.setBarWidth(
 280:                     used / (dataset.getColumnCount() * dataset.getRowCount())
 281:                 );
 282:             }
 283:             else {
 284:                 state.setBarWidth(used);
 285:             }
 286:         }
 287:         
 288:         return state;
 289: 
 290:     }
 291: 
 292:     /**
 293:      * Draw a single data item.
 294:      *
 295:      * @param g2  the graphics device.
 296:      * @param state  the renderer state.
 297:      * @param dataArea  the area in which the data is drawn.
 298:      * @param plot  the plot.
 299:      * @param domainAxis  the domain axis.
 300:      * @param rangeAxis  the range axis.
 301:      * @param dataset  the data.
 302:      * @param row  the row index (zero-based).
 303:      * @param column  the column index (zero-based).
 304:      * @param pass  the pass index.
 305:      */
 306:     public void drawItem(Graphics2D g2,
 307:                          CategoryItemRendererState state,
 308:                          Rectangle2D dataArea,
 309:                          CategoryPlot plot,
 310:                          CategoryAxis domainAxis,
 311:                          ValueAxis rangeAxis,
 312:                          CategoryDataset dataset,
 313:                          int row,
 314:                          int column,
 315:                          int pass) {
 316:                              
 317:         if (!(dataset instanceof BoxAndWhiskerCategoryDataset)) {
 318:             throw new IllegalArgumentException(
 319:                 "BoxAndWhiskerRenderer.drawItem() : the data should be of type "
 320:                 + "BoxAndWhiskerCategoryDataset only."
 321:             );
 322:         }
 323: 
 324:         PlotOrientation orientation = plot.getOrientation();
 325: 
 326:         if (orientation == PlotOrientation.HORIZONTAL) {
 327:             drawHorizontalItem(
 328:                 g2, state, dataArea, plot, domainAxis, rangeAxis, 
 329:                 dataset, row, column
 330:             );
 331:         } 
 332:         else if (orientation == PlotOrientation.VERTICAL) {
 333:             drawVerticalItem(
 334:                 g2, state, dataArea, plot, domainAxis, rangeAxis, 
 335:                 dataset, row, column
 336:             );
 337:         }
 338:         
 339:     }
 340: 
 341:     /**
 342:      * Draws the visual representation of a single data item when the plot has 
 343:      * a horizontal orientation.
 344:      *
 345:      * @param g2  the graphics device.
 346:      * @param state  the renderer state.
 347:      * @param dataArea  the area within which the plot is being drawn.
 348:      * @param plot  the plot (can be used to obtain standard color 
 349:      *              information etc).
 350:      * @param domainAxis  the domain axis.
 351:      * @param rangeAxis  the range axis.
 352:      * @param dataset  the dataset.
 353:      * @param row  the row index (zero-based).
 354:      * @param column  the column index (zero-based).
 355:      */
 356:     public void drawHorizontalItem(Graphics2D g2,
 357:                                    CategoryItemRendererState state,
 358:                                    Rectangle2D dataArea,
 359:                                    CategoryPlot plot,
 360:                                    CategoryAxis domainAxis,
 361:                                    ValueAxis rangeAxis,
 362:                                    CategoryDataset dataset,
 363:                                    int row,
 364:                                    int column) {
 365: 
 366:         BoxAndWhiskerCategoryDataset bawDataset 
 367:             = (BoxAndWhiskerCategoryDataset) dataset;
 368: 
 369:         double categoryEnd = domainAxis.getCategoryEnd(
 370:             column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
 371:         );
 372:         double categoryStart = domainAxis.getCategoryStart(
 373:             column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
 374:         );
 375:         double categoryWidth = Math.abs(categoryEnd - categoryStart);
 376: 
 377:         double yy = categoryStart;
 378:         int seriesCount = getRowCount();
 379:         int categoryCount = getColumnCount();
 380: 
 381:         if (seriesCount > 1) {
 382:             double seriesGap = dataArea.getWidth() * getItemMargin()
 383:                                / (categoryCount * (seriesCount - 1));
 384:             double usedWidth = (state.getBarWidth() * seriesCount) 
 385:                                + (seriesGap * (seriesCount - 1));
 386:             // offset the start of the boxes if the total width used is smaller
 387:             // than the category width
 388:             double offset = (categoryWidth - usedWidth) / 2;
 389:             yy = yy + offset + (row * (state.getBarWidth() + seriesGap));
 390:         } 
 391:         else {
 392:             // offset the start of the box if the box width is smaller than 
 393:             // the category width
 394:             double offset = (categoryWidth - state.getBarWidth()) / 2;
 395:             yy = yy + offset;
 396:         }
 397: 
 398:         Paint p = getItemPaint(row, column);
 399:         if (p != null) {
 400:             g2.setPaint(p);
 401:         }
 402:         Stroke s = getItemStroke(row, column);
 403:         g2.setStroke(s);
 404: 
 405:         RectangleEdge location = plot.getRangeAxisEdge();
 406: 
 407:         Number xQ1 = bawDataset.getQ1Value(row, column);
 408:         Number xQ3 = bawDataset.getQ3Value(row, column);
 409:         Number xMax = bawDataset.getMaxRegularValue(row, column);
 410:         Number xMin = bawDataset.getMinRegularValue(row, column);
 411: 
 412:         Shape box = null;
 413:         if (xQ1 != null && xQ3 != null && xMax != null && xMin != null) {
 414: 
 415:             double xxQ1 = rangeAxis.valueToJava2D(
 416:                 xQ1.doubleValue(), dataArea, location
 417:             );
 418:             double xxQ3 = rangeAxis.valueToJava2D(
 419:                 xQ3.doubleValue(), dataArea, location
 420:             );
 421:             double xxMax = rangeAxis.valueToJava2D(
 422:                 xMax.doubleValue(), dataArea, location
 423:             );
 424:             double xxMin = rangeAxis.valueToJava2D(
 425:                 xMin.doubleValue(), dataArea, location
 426:             );
 427:             double yymid = yy + state.getBarWidth() / 2.0;
 428:             
 429:             // draw the upper shadow...
 430:             g2.draw(new Line2D.Double(xxMax, yymid, xxQ3, yymid));
 431:             g2.draw(
 432:                 new Line2D.Double(xxMax, yy, xxMax, yy + state.getBarWidth())
 433:             );
 434: 
 435:             // draw the lower shadow...
 436:             g2.draw(new Line2D.Double(xxMin, yymid, xxQ1, yymid));
 437:             g2.draw(
 438:                 new Line2D.Double(xxMin, yy, xxMin, yy + state.getBarWidth())
 439:             );
 440: 
 441:             // draw the box...
 442:             box = new Rectangle2D.Double(
 443:                 Math.min(xxQ1, xxQ3), yy, Math.abs(xxQ1 - xxQ3), 
 444:                 state.getBarWidth()
 445:             );
 446:             if (this.fillBox) {
 447:                 g2.fill(box);
 448:             } 
 449:             g2.draw(box);
 450: 
 451:         }
 452: 
 453:         g2.setPaint(this.artifactPaint);
 454:         double aRadius = 0;                 // average radius
 455: 
 456:         // draw mean - SPECIAL AIMS REQUIREMENT...
 457:         Number xMean = bawDataset.getMeanValue(row, column);
 458:         if (xMean != null) {
 459:             double xxMean = rangeAxis.valueToJava2D(
 460:                 xMean.doubleValue(), dataArea, location
 461:             );
 462:             aRadius = state.getBarWidth() / 4;
 463:             Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
 464:                 xxMean - aRadius, yy + aRadius, aRadius * 2, aRadius * 2
 465:             );
 466:             g2.fill(avgEllipse);
 467:             g2.draw(avgEllipse);
 468:         }
 469: 
 470:         // draw median...
 471:         Number xMedian = bawDataset.getMedianValue(row, column);
 472:         if (xMedian != null) {
 473:             double xxMedian = rangeAxis.valueToJava2D(
 474:                 xMedian.doubleValue(), dataArea, location
 475:             );
 476:             g2.draw(
 477:                 new Line2D.Double(
 478:                     xxMedian, yy, xxMedian, yy + state.getBarWidth()
 479:                 )
 480:             );
 481:         }
 482:         
 483:         // collect entity and tool tip information...
 484:         if (state.getInfo() != null) {
 485:             EntityCollection entities = state.getEntityCollection();
 486:             if (entities != null) {
 487:                 String tip = null;
 488:                 CategoryToolTipGenerator tipster 
 489:                     = getToolTipGenerator(row, column);
 490:                 if (tipster != null) {
 491:                     tip = tipster.generateToolTip(dataset, row, column);
 492:                 }
 493:                 String url = null;
 494:                 if (getItemURLGenerator(row, column) != null) {
 495:                     url = getItemURLGenerator(row, column).generateURL(
 496:                         dataset, row, column
 497:                     );
 498:                 }
 499:                 CategoryItemEntity entity = new CategoryItemEntity(
 500:                     box, tip, url, dataset, row, dataset.getColumnKey(column), 
 501:                     column
 502:                 );
 503:                 entities.add(entity);
 504:             }
 505:         }
 506: 
 507:     } 
 508:         
 509:     /**
 510:      * Draws the visual representation of a single data item when the plot has 
 511:      * a vertical orientation.
 512:      *
 513:      * @param g2  the graphics device.
 514:      * @param state  the renderer state.
 515:      * @param dataArea  the area within which the plot is being drawn.
 516:      * @param plot  the plot (can be used to obtain standard color information 
 517:      *              etc).
 518:      * @param domainAxis  the domain axis.
 519:      * @param rangeAxis  the range axis.
 520:      * @param dataset  the dataset.
 521:      * @param row  the row index (zero-based).
 522:      * @param column  the column index (zero-based).
 523:      */
 524:     public void drawVerticalItem(Graphics2D g2, 
 525:                                  CategoryItemRendererState state,
 526:                                  Rectangle2D dataArea,
 527:                                  CategoryPlot plot, 
 528:                                  CategoryAxis domainAxis, 
 529:                                  ValueAxis rangeAxis,
 530:                                  CategoryDataset dataset, 
 531:                                  int row, 
 532:                                  int column) {
 533: 
 534:         BoxAndWhiskerCategoryDataset bawDataset 
 535:             = (BoxAndWhiskerCategoryDataset) dataset;
 536:         
 537:         double categoryEnd = domainAxis.getCategoryEnd(
 538:             column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
 539:         );
 540:         double categoryStart = domainAxis.getCategoryStart(
 541:             column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
 542:         );
 543:         double categoryWidth = categoryEnd - categoryStart;
 544: 
 545:         double xx = categoryStart;
 546:         int seriesCount = getRowCount();
 547:         int categoryCount = getColumnCount();
 548: 
 549:         if (seriesCount > 1) {
 550:             double seriesGap = dataArea.getWidth() * getItemMargin() 
 551:                                / (categoryCount * (seriesCount - 1));
 552:             double usedWidth = (state.getBarWidth() * seriesCount) 
 553:                                + (seriesGap * (seriesCount - 1));
 554:             // offset the start of the boxes if the total width used is smaller
 555:             // than the category width
 556:             double offset = (categoryWidth - usedWidth) / 2;
 557:             xx = xx + offset + (row * (state.getBarWidth() + seriesGap));
 558:         } 
 559:         else {
 560:             // offset the start of the box if the box width is smaller than the 
 561:             // category width
 562:             double offset = (categoryWidth - state.getBarWidth()) / 2;
 563:             xx = xx + offset;
 564:         } 
 565:         
 566:         double yyAverage = 0.0;
 567:         double yyOutlier;
 568: 
 569:         Paint p = getItemPaint(row, column);
 570:         if (p != null) {
 571:             g2.setPaint(p);
 572:         }
 573:         Stroke s = getItemStroke(row, column);
 574:         g2.setStroke(s);
 575: 
 576:         double aRadius = 0;                 // average radius
 577: 
 578:         RectangleEdge location = plot.getRangeAxisEdge();
 579: 
 580:         Number yQ1 = bawDataset.getQ1Value(row, column);
 581:         Number yQ3 = bawDataset.getQ3Value(row, column);
 582:         Number yMax = bawDataset.getMaxRegularValue(row, column);
 583:         Number yMin = bawDataset.getMinRegularValue(row, column);
 584:         Shape box = null;
 585:         if (yQ1 != null && yQ3 != null && yMax != null && yMin != null) {
 586: 
 587:             double yyQ1 = rangeAxis.valueToJava2D(
 588:                 yQ1.doubleValue(), dataArea, location
 589:             );
 590:             double yyQ3 = rangeAxis.valueToJava2D(
 591:                 yQ3.doubleValue(), dataArea, location
 592:             );
 593:             double yyMax = rangeAxis.valueToJava2D(
 594:                 yMax.doubleValue(), dataArea, location
 595:             );
 596:             double yyMin = rangeAxis.valueToJava2D(
 597:                 yMin.doubleValue(), dataArea, location
 598:             );
 599:             double xxmid = xx + state.getBarWidth() / 2.0;
 600:             
 601:             // draw the upper shadow...
 602:             g2.draw(new Line2D.Double(xxmid, yyMax, xxmid, yyQ3));
 603:             g2.draw(
 604:                 new Line2D.Double(xx, yyMax, xx + state.getBarWidth(), yyMax)
 605:             );
 606: 
 607:             // draw the lower shadow...
 608:             g2.draw(new Line2D.Double(xxmid, yyMin, xxmid, yyQ1));
 609:             g2.draw(
 610:                 new Line2D.Double(xx, yyMin, xx + state.getBarWidth(), yyMin)
 611:             );
 612: 
 613:             // draw the body...
 614:             box = new Rectangle2D.Double(
 615:                 xx, Math.min(yyQ1, yyQ3), state.getBarWidth(), 
 616:                 Math.abs(yyQ1 - yyQ3)
 617:             );
 618:             if (this.fillBox) {
 619:                 g2.fill(box);
 620:             }
 621:             g2.draw(box);
 622:   
 623:         }
 624:         
 625:         g2.setPaint(this.artifactPaint);
 626: 
 627:         // draw mean - SPECIAL AIMS REQUIREMENT...
 628:         Number yMean = bawDataset.getMeanValue(row, column);
 629:         if (yMean != null) {
 630:             yyAverage = rangeAxis.valueToJava2D(
 631:                 yMean.doubleValue(), dataArea, location
 632:             );
 633:             aRadius = state.getBarWidth() / 4;
 634:             Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
 635:                 xx + aRadius, yyAverage - aRadius, aRadius * 2, aRadius * 2
 636:             );
 637:             g2.fill(avgEllipse);
 638:             g2.draw(avgEllipse);
 639:         }
 640: 
 641:         // draw median...
 642:         Number yMedian = bawDataset.getMedianValue(row, column);
 643:         if (yMedian != null) {
 644:             double yyMedian = rangeAxis.valueToJava2D(
 645:                 yMedian.doubleValue(), dataArea, location
 646:             );
 647:             g2.draw(
 648:                 new Line2D.Double(
 649:                     xx, yyMedian, xx + state.getBarWidth(), yyMedian
 650:                 )
 651:             );
 652:         }
 653:         
 654:         // draw yOutliers...
 655:         double maxAxisValue = rangeAxis.valueToJava2D(
 656:             rangeAxis.getUpperBound(), dataArea, location
 657:         ) + aRadius;
 658:         double minAxisValue = rangeAxis.valueToJava2D(
 659:             rangeAxis.getLowerBound(), dataArea, location
 660:         ) - aRadius;
 661: 
 662:         g2.setPaint(p);
 663: 
 664:         // draw outliers
 665:         double oRadius = state.getBarWidth() / 3;    // outlier radius
 666:         List outliers = new ArrayList();
 667:         OutlierListCollection outlierListCollection 
 668:             = new OutlierListCollection();
 669: 
 670:         // From outlier array sort out which are outliers and put these into a 
 671:         // list If there are any farouts, set the flag on the 
 672:         // OutlierListCollection
 673:         List yOutliers = bawDataset.getOutliers(row, column);
 674:         if (yOutliers != null) {
 675:             for (int i = 0; i < yOutliers.size(); i++) {
 676:                 double outlier = ((Number) yOutliers.get(i)).doubleValue();
 677:                 Number minOutlier = bawDataset.getMinOutlier(row, column);
 678:                 Number maxOutlier = bawDataset.getMaxOutlier(row, column);
 679:                 Number minRegular = bawDataset.getMinRegularValue(row, column);
 680:                 Number maxRegular = bawDataset.getMaxRegularValue(row, column);
 681:                 if (outlier > maxOutlier.doubleValue()) {
 682:                     outlierListCollection.setHighFarOut(true);
 683:                 } 
 684:                 else if (outlier < minOutlier.doubleValue()) {
 685:                     outlierListCollection.setLowFarOut(true);
 686:                 }
 687:                 else if (outlier > maxRegular.doubleValue()) {
 688:                     yyOutlier = rangeAxis.valueToJava2D(
 689:                         outlier, dataArea, location
 690:                     );
 691:                     outliers.add(
 692:                         new Outlier(
 693:                             xx + state.getBarWidth() / 2.0, yyOutlier, oRadius
 694:                         )
 695:                     );
 696:                 }
 697:                 else if (outlier < minRegular.doubleValue()) {
 698:                     yyOutlier = rangeAxis.valueToJava2D(
 699:                         outlier, dataArea, location
 700:                     );
 701:                     outliers.add(
 702:                         new Outlier(
 703:                             xx + state.getBarWidth() / 2.0, yyOutlier, oRadius
 704:                         )
 705:                     );
 706:                 }
 707:                 Collections.sort(outliers);
 708:             }
 709: 
 710:             // Process outliers. Each outlier is either added to the 
 711:             // appropriate outlier list or a new outlier list is made
 712:             for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
 713:                 Outlier outlier = (Outlier) iterator.next();
 714:                 outlierListCollection.add(outlier);
 715:             }
 716: 
 717:             for (Iterator iterator = outlierListCollection.iterator(); 
 718:                  iterator.hasNext();) {
 719:                 OutlierList list = (OutlierList) iterator.next();
 720:                 Outlier outlier = list.getAveragedOutlier();
 721:                 Point2D point = outlier.getPoint();
 722: 
 723:                 if (list.isMultiple()) {
 724:                     drawMultipleEllipse(
 725:                         point, state.getBarWidth(), oRadius, g2
 726:                     );
 727:                 } 
 728:                 else {
 729:                     drawEllipse(point, oRadius, g2);
 730:                 }
 731:             }
 732: 
 733:             // draw farout indicators
 734:             if (outlierListCollection.isHighFarOut()) {
 735:                 drawHighFarOut(
 736:                     aRadius / 2.0, g2, xx + state.getBarWidth() / 2.0, 
 737:                     maxAxisValue
 738:                 );
 739:             }
 740:         
 741:             if (outlierListCollection.isLowFarOut()) {
 742:                 drawLowFarOut(
 743:                     aRadius / 2.0, g2, xx + state.getBarWidth() / 2.0, 
 744:                     minAxisValue
 745:                 );
 746:             }
 747:         }
 748:         // collect entity and tool tip information...
 749:         if (state.getInfo() != null) {
 750:             EntityCollection entities = state.getEntityCollection();
 751:             if (entities != null) {
 752:                 String tip = null;
 753:                 CategoryToolTipGenerator tipster 
 754:                     = getToolTipGenerator(row, column);
 755:                 if (tipster != null) {
 756:                     tip = tipster.generateToolTip(dataset, row, column);
 757:                 }
 758:                 String url = null;
 759:                 if (getItemURLGenerator(row, column) != null) {
 760:                     url = getItemURLGenerator(row, column).generateURL(
 761:                         dataset, row, column
 762:                     );
 763:                 }
 764:                 CategoryItemEntity entity = new CategoryItemEntity(
 765:                     box, tip, url, dataset, row, dataset.getColumnKey(column), 
 766:                     column
 767:                 );
 768:                 entities.add(entity);
 769:             }
 770:         }
 771: 
 772:     }
 773: 
 774:     /**
 775:      * Draws a dot to represent an outlier. 
 776:      * 
 777:      * @param point  the location.
 778:      * @param oRadius  the radius.
 779:      * @param g2  the graphics device.
 780:      */
 781:     private void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
 782:         Ellipse2D dot = new Ellipse2D.Double(
 783:             point.getX() + oRadius / 2, point.getY(), oRadius, oRadius
 784:         );
 785:         g2.draw(dot);
 786:     }
 787: 
 788:     /**
 789:      * Draws two dots to represent the average value of more than one outlier.
 790:      * 
 791:      * @param point  the location
 792:      * @param boxWidth  the box width.
 793:      * @param oRadius  the radius.
 794:      * @param g2  the graphics device.
 795:      */
 796:     private void drawMultipleEllipse(Point2D point, double boxWidth, 
 797:                                      double oRadius, Graphics2D g2)  {
 798:                                          
 799:         Ellipse2D dot1 = new Ellipse2D.Double(
 800:             point.getX() - (boxWidth / 2) + oRadius, point.getY(), 
 801:             oRadius, oRadius
 802:         );
 803:         Ellipse2D dot2 = new Ellipse2D.Double(
 804:             point.getX() + (boxWidth / 2), point.getY(), oRadius, oRadius
 805:         );
 806:         g2.draw(dot1);
 807:         g2.draw(dot2);
 808:     }
 809: 
 810:     /**
 811:      * Draws a triangle to indicate the presence of far-out values.
 812:      * 
 813:      * @param aRadius  the radius.
 814:      * @param g2  the graphics device.
 815:      * @param xx  the x coordinate.
 816:      * @param m  the y coordinate.
 817:      */
 818:     private void drawHighFarOut(double aRadius, Graphics2D g2, double xx, 
 819:                                 double m) {
 820:         double side = aRadius * 2;
 821:         g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
 822:         g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
 823:         g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
 824:     }
 825: 
 826:     /**
 827:      * Draws a triangle to indicate the presence of far-out values.
 828:      * 
 829:      * @param aRadius  the radius.
 830:      * @param g2  the graphics device.
 831:      * @param xx  the x coordinate.
 832:      * @param m  the y coordinate.
 833:      */
 834:     private void drawLowFarOut(double aRadius, Graphics2D g2, double xx, 
 835:                                double m) {
 836:         double side = aRadius * 2;
 837:         g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
 838:         g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
 839:         g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
 840:     }
 841:     
 842:     /**
 843:      * Tests this renderer for equality with an arbitrary object.
 844:      *
 845:      * @param obj  the object (<code>null</code> permitted).
 846:      *
 847:      * @return <code>true</code> or <code>false</code>.
 848:      */
 849:     public boolean equals(Object obj) {
 850:         if (obj == this) {
 851:             return true;   
 852:         }
 853:         if (!(obj instanceof BoxAndWhiskerRenderer)) {
 854:             return false;   
 855:         }
 856:         if (!super.equals(obj)) {
 857:             return false;
 858:         }
 859:         BoxAndWhiskerRenderer that = (BoxAndWhiskerRenderer) obj;
 860:         if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) {
 861:             return false;
 862:         }
 863:         if (!(this.fillBox == that.fillBox)) {
 864:             return false;   
 865:         }
 866:         if (!(this.itemMargin == that.itemMargin)) {
 867:             return false;   
 868:         }
 869:         return true;
 870:     }
 871:     
 872:     /**
 873:      * Provides serialization support.
 874:      *
 875:      * @param stream  the output stream.
 876:      *
 877:      * @throws IOException  if there is an I/O error.
 878:      */
 879:     private void writeObject(ObjectOutputStream stream) throws IOException {
 880:         stream.defaultWriteObject();
 881:         SerialUtilities.writePaint(this.artifactPaint, stream);
 882:     }
 883: 
 884:     /**
 885:      * Provides serialization support.
 886:      *
 887:      * @param stream  the input stream.
 888:      *
 889:      * @throws IOException  if there is an I/O error.
 890:      * @throws ClassNotFoundException  if there is a classpath problem.
 891:      */
 892:     private void readObject(ObjectInputStream stream) 
 893:         throws IOException, ClassNotFoundException {
 894:         stream.defaultReadObject();
 895:         this.artifactPaint = SerialUtilities.readPaint(stream);
 896:     }
 897:    
 898: }