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