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