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