001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ---------------------------- 028 * XYBoxAndWhiskerRenderer.java 029 * ---------------------------- 030 * (C) Copyright 2003-2009, by David Browning and Contributors. 031 * 032 * Original Author: David Browning (for Australian Institute of Marine 033 * Science); 034 * Contributor(s): David Gilbert (for Object Refinery Limited); 035 * 036 * Changes 037 * ------- 038 * 05-Aug-2003 : Version 1, contributed by David Browning. Based on code in the 039 * CandlestickRenderer class. Additional modifications by David 040 * Gilbert to make the code work with 0.9.10 changes (DG); 041 * 08-Aug-2003 : Updated some of the Javadoc 042 * Allowed BoxAndwhiskerDataset Average value to be null - the 043 * average value is an AIMS requirement 044 * Allow the outlier and farout coefficients to be set - though 045 * at the moment this only affects the calculation of farouts. 046 * Added artifactPaint variable and setter/getter 047 * 12-Aug-2003 Rewrote code to sort out and process outliers to take 048 * advantage of changes in DefaultBoxAndWhiskerDataset 049 * Added a limit of 10% for width of box should no width be 050 * specified...maybe this should be setable??? 051 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG); 052 * 08-Sep-2003 : Changed ValueAxis API (DG); 053 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 054 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG); 055 * 23-Apr-2004 : Added fillBox attribute, extended equals() method and fixed 056 * serialization issue (DG); 057 * 29-Apr-2004 : Fixed problem with drawing upper and lower shadows - bug id 058 * 944011 (DG); 059 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 060 * getYValue() (DG); 061 * 01-Oct-2004 : Renamed 'paint' --> 'boxPaint' to avoid conflict with 062 * inherited attribute (DG); 063 * 10-Jun-2005 : Updated equals() to handle GradientPaint (DG); 064 * 06-Oct-2005 : Removed setPaint() call in drawItem(), it is causing a 065 * loop (DG); 066 * ------------- JFREECHART 1.0.x --------------------------------------------- 067 * 02-Feb-2007 : Removed author tags from all over JFreeChart sources (DG); 068 * 05-Feb-2007 : Added event notifications and fixed drawing for horizontal 069 * plot orientation (DG); 070 * 13-Jun-2007 : Replaced deprecated method call (DG); 071 * 03-Jan-2008 : Check visibility of average marker before drawing it (DG); 072 * 27-Mar-2008 : If boxPaint is null, revert to itemPaint (DG); 073 * 27-Mar-2009 : Added findRangeBounds() method override (DG); 074 * 08-Dec-2009 : Fix for bug 2909215, NullPointerException for null 075 * outliers (DG); 076 * 077 */ 078 079 package org.jfree.chart.renderer.xy; 080 081 import java.awt.Color; 082 import java.awt.Graphics2D; 083 import java.awt.Paint; 084 import java.awt.Shape; 085 import java.awt.Stroke; 086 import java.awt.geom.Ellipse2D; 087 import java.awt.geom.Line2D; 088 import java.awt.geom.Point2D; 089 import java.awt.geom.Rectangle2D; 090 import java.io.IOException; 091 import java.io.ObjectInputStream; 092 import java.io.ObjectOutputStream; 093 import java.io.Serializable; 094 import java.util.ArrayList; 095 import java.util.Collections; 096 import java.util.Iterator; 097 import java.util.List; 098 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.labels.BoxAndWhiskerXYToolTipGenerator; 103 import org.jfree.chart.plot.CrosshairState; 104 import org.jfree.chart.plot.PlotOrientation; 105 import org.jfree.chart.plot.PlotRenderingInfo; 106 import org.jfree.chart.plot.XYPlot; 107 import org.jfree.chart.renderer.Outlier; 108 import org.jfree.chart.renderer.OutlierList; 109 import org.jfree.chart.renderer.OutlierListCollection; 110 import org.jfree.data.Range; 111 import org.jfree.data.general.DatasetUtilities; 112 import org.jfree.data.statistics.BoxAndWhiskerXYDataset; 113 import org.jfree.data.xy.XYDataset; 114 import org.jfree.io.SerialUtilities; 115 import org.jfree.ui.RectangleEdge; 116 import org.jfree.util.PaintUtilities; 117 import org.jfree.util.PublicCloneable; 118 119 /** 120 * A renderer that draws box-and-whisker items on an {@link XYPlot}. This 121 * renderer requires a {@link BoxAndWhiskerXYDataset}). The example shown here 122 * is generated by the <code>BoxAndWhiskerChartDemo2.java</code> program 123 * included in the JFreeChart demo collection: 124 * <br><br> 125 * <img src="../../../../../images/XYBoxAndWhiskerRendererSample.png" 126 * alt="XYBoxAndWhiskerRendererSample.png" /> 127 * <P> 128 * This renderer does not include any code to calculate the crosshair point. 129 */ 130 public class XYBoxAndWhiskerRenderer extends AbstractXYItemRenderer 131 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 132 133 /** For serialization. */ 134 private static final long serialVersionUID = -8020170108532232324L; 135 136 /** The box width. */ 137 private double boxWidth; 138 139 /** The paint used to fill the box. */ 140 private transient Paint boxPaint; 141 142 /** A flag that controls whether or not the box is filled. */ 143 private boolean fillBox; 144 145 /** 146 * The paint used to draw various artifacts such as outliers, farout 147 * symbol, average ellipse and median line. 148 */ 149 private transient Paint artifactPaint = Color.black; 150 151 /** 152 * Creates a new renderer for box and whisker charts. 153 */ 154 public XYBoxAndWhiskerRenderer() { 155 this(-1.0); 156 } 157 158 /** 159 * Creates a new renderer for box and whisker charts. 160 * <P> 161 * Use -1 for the box width if you prefer the width to be calculated 162 * automatically. 163 * 164 * @param boxWidth the box width. 165 */ 166 public XYBoxAndWhiskerRenderer(double boxWidth) { 167 super(); 168 this.boxWidth = boxWidth; 169 this.boxPaint = Color.green; 170 this.fillBox = true; 171 setBaseToolTipGenerator(new BoxAndWhiskerXYToolTipGenerator()); 172 } 173 174 /** 175 * Returns the width of each box. 176 * 177 * @return The box width. 178 * 179 * @see #setBoxWidth(double) 180 */ 181 public double getBoxWidth() { 182 return this.boxWidth; 183 } 184 185 /** 186 * Sets the box width and sends a {@link RendererChangeEvent} to all 187 * registered listeners. 188 * <P> 189 * If you set the width to a negative value, the renderer will calculate 190 * the box width automatically based on the space available on the chart. 191 * 192 * @param width the width. 193 * 194 * @see #getBoxWidth() 195 */ 196 public void setBoxWidth(double width) { 197 if (width != this.boxWidth) { 198 this.boxWidth = width; 199 fireChangeEvent(); 200 } 201 } 202 203 /** 204 * Returns the paint used to fill boxes. 205 * 206 * @return The paint (possibly <code>null</code>). 207 * 208 * @see #setBoxPaint(Paint) 209 */ 210 public Paint getBoxPaint() { 211 return this.boxPaint; 212 } 213 214 /** 215 * Sets the paint used to fill boxes and sends a {@link RendererChangeEvent} 216 * to all registered listeners. 217 * 218 * @param paint the paint (<code>null</code> permitted). 219 * 220 * @see #getBoxPaint() 221 */ 222 public void setBoxPaint(Paint paint) { 223 this.boxPaint = paint; 224 fireChangeEvent(); 225 } 226 227 /** 228 * Returns the flag that controls whether or not the box is filled. 229 * 230 * @return A boolean. 231 * 232 * @see #setFillBox(boolean) 233 */ 234 public boolean getFillBox() { 235 return this.fillBox; 236 } 237 238 /** 239 * Sets the flag that controls whether or not the box is filled and sends a 240 * {@link RendererChangeEvent} to all registered listeners. 241 * 242 * @param flag the flag. 243 * 244 * @see #setFillBox(boolean) 245 */ 246 public void setFillBox(boolean flag) { 247 this.fillBox = flag; 248 fireChangeEvent(); 249 } 250 251 /** 252 * Returns the paint used to paint the various artifacts such as outliers, 253 * farout symbol, median line and the averages ellipse. 254 * 255 * @return The paint (never <code>null</code>). 256 * 257 * @see #setArtifactPaint(Paint) 258 */ 259 public Paint getArtifactPaint() { 260 return this.artifactPaint; 261 } 262 263 /** 264 * Sets the paint used to paint the various artifacts such as outliers, 265 * farout symbol, median line and the averages ellipse, and sends a 266 * {@link RendererChangeEvent} to all registered listeners. 267 * 268 * @param paint the paint (<code>null</code> not permitted). 269 * 270 * @see #getArtifactPaint() 271 */ 272 public void setArtifactPaint(Paint paint) { 273 if (paint == null) { 274 throw new IllegalArgumentException("Null 'paint' argument."); 275 } 276 this.artifactPaint = paint; 277 fireChangeEvent(); 278 } 279 280 /** 281 * Returns the range of values the renderer requires to display all the 282 * items from the specified dataset. 283 * 284 * @param dataset the dataset (<code>null</code> permitted). 285 * 286 * @return The range (<code>null</code> if the dataset is <code>null</code> 287 * or empty). 288 * 289 * @see #findDomainBounds(XYDataset) 290 */ 291 public Range findRangeBounds(XYDataset dataset) { 292 return findRangeBounds(dataset, true); 293 } 294 295 /** 296 * Returns the box paint or, if this is <code>null</code>, the item 297 * paint. 298 * 299 * @param series the series index. 300 * @param item the item index. 301 * 302 * @return The paint used to fill the box for the specified item (never 303 * <code>null</code>). 304 * 305 * @since 1.0.10 306 */ 307 protected Paint lookupBoxPaint(int series, int item) { 308 Paint p = getBoxPaint(); 309 if (p != null) { 310 return p; 311 } 312 else { 313 // TODO: could change this to itemFillPaint(). For backwards 314 // compatibility, it might require a useFillPaint flag. 315 return getItemPaint(series, item); 316 } 317 } 318 319 /** 320 * Draws the visual representation of a single data item. 321 * 322 * @param g2 the graphics device. 323 * @param state the renderer state. 324 * @param dataArea the area within which the plot is being drawn. 325 * @param info collects info about the drawing. 326 * @param plot the plot (can be used to obtain standard color 327 * information etc). 328 * @param domainAxis the domain axis. 329 * @param rangeAxis the range axis. 330 * @param dataset the dataset (must be an instance of 331 * {@link BoxAndWhiskerXYDataset}). 332 * @param series the series index (zero-based). 333 * @param item the item index (zero-based). 334 * @param crosshairState crosshair information for the plot 335 * (<code>null</code> permitted). 336 * @param pass the pass index. 337 */ 338 public void drawItem(Graphics2D g2, XYItemRendererState state, 339 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 340 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 341 int series, int item, CrosshairState crosshairState, int pass) { 342 343 PlotOrientation orientation = plot.getOrientation(); 344 345 if (orientation == PlotOrientation.HORIZONTAL) { 346 drawHorizontalItem(g2, dataArea, info, plot, domainAxis, rangeAxis, 347 dataset, series, item, crosshairState, pass); 348 } 349 else if (orientation == PlotOrientation.VERTICAL) { 350 drawVerticalItem(g2, dataArea, info, plot, domainAxis, rangeAxis, 351 dataset, series, item, crosshairState, pass); 352 } 353 354 } 355 356 /** 357 * Draws the visual representation of a single data item. 358 * 359 * @param g2 the graphics device. 360 * @param dataArea the area within which the plot is being drawn. 361 * @param info collects info about the drawing. 362 * @param plot the plot (can be used to obtain standard color 363 * information etc). 364 * @param domainAxis the domain axis. 365 * @param rangeAxis the range axis. 366 * @param dataset the dataset (must be an instance of 367 * {@link BoxAndWhiskerXYDataset}). 368 * @param series the series index (zero-based). 369 * @param item the item index (zero-based). 370 * @param crosshairState crosshair information for the plot 371 * (<code>null</code> permitted). 372 * @param pass the pass index. 373 */ 374 public void drawHorizontalItem(Graphics2D g2, Rectangle2D dataArea, 375 PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis, 376 ValueAxis rangeAxis, XYDataset dataset, int series, 377 int item, CrosshairState crosshairState, int pass) { 378 379 // setup for collecting optional entity info... 380 EntityCollection entities = null; 381 if (info != null) { 382 entities = info.getOwner().getEntityCollection(); 383 } 384 385 BoxAndWhiskerXYDataset boxAndWhiskerData 386 = (BoxAndWhiskerXYDataset) dataset; 387 388 Number x = boxAndWhiskerData.getX(series, item); 389 Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item); 390 Number yMin = boxAndWhiskerData.getMinRegularValue(series, item); 391 Number yMedian = boxAndWhiskerData.getMedianValue(series, item); 392 Number yAverage = boxAndWhiskerData.getMeanValue(series, item); 393 Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item); 394 Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item); 395 396 double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 397 plot.getDomainAxisEdge()); 398 399 RectangleEdge location = plot.getRangeAxisEdge(); 400 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 401 location); 402 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 403 location); 404 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 405 dataArea, location); 406 double yyAverage = 0.0; 407 if (yAverage != null) { 408 yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 409 dataArea, location); 410 } 411 double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 412 dataArea, location); 413 double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 414 dataArea, location); 415 416 double exactBoxWidth = getBoxWidth(); 417 double width = exactBoxWidth; 418 double dataAreaX = dataArea.getHeight(); 419 double maxBoxPercent = 0.1; 420 double maxBoxWidth = dataAreaX * maxBoxPercent; 421 if (exactBoxWidth <= 0.0) { 422 int itemCount = boxAndWhiskerData.getItemCount(series); 423 exactBoxWidth = dataAreaX / itemCount * 4.5 / 7; 424 if (exactBoxWidth < 3) { 425 width = 3; 426 } 427 else if (exactBoxWidth > maxBoxWidth) { 428 width = maxBoxWidth; 429 } 430 else { 431 width = exactBoxWidth; 432 } 433 } 434 435 g2.setPaint(getItemPaint(series, item)); 436 Stroke s = getItemStroke(series, item); 437 g2.setStroke(s); 438 439 // draw the upper shadow 440 g2.draw(new Line2D.Double(yyMax, xx, yyQ3Median, xx)); 441 g2.draw(new Line2D.Double(yyMax, xx - width / 2, yyMax, 442 xx + width / 2)); 443 444 // draw the lower shadow 445 g2.draw(new Line2D.Double(yyMin, xx, yyQ1Median, xx)); 446 g2.draw(new Line2D.Double(yyMin, xx - width / 2, yyMin, 447 xx + width / 2)); 448 449 // draw the body 450 Shape box = null; 451 if (yyQ1Median < yyQ3Median) { 452 box = new Rectangle2D.Double(yyQ1Median, xx - width / 2, 453 yyQ3Median - yyQ1Median, width); 454 } 455 else { 456 box = new Rectangle2D.Double(yyQ3Median, xx - width / 2, 457 yyQ1Median - yyQ3Median, width); 458 } 459 if (this.fillBox) { 460 g2.setPaint(lookupBoxPaint(series, item)); 461 g2.fill(box); 462 } 463 g2.setStroke(getItemOutlineStroke(series, item)); 464 g2.setPaint(getItemOutlinePaint(series, item)); 465 g2.draw(box); 466 467 // draw median 468 g2.setPaint(getArtifactPaint()); 469 g2.draw(new Line2D.Double(yyMedian, 470 xx - width / 2, yyMedian, xx + width / 2)); 471 472 // draw average - SPECIAL AIMS REQUIREMENT 473 if (yAverage != null) { 474 double aRadius = width / 4; 475 // here we check that the average marker will in fact be visible 476 // before drawing it... 477 if ((yyAverage > (dataArea.getMinX() - aRadius)) 478 && (yyAverage < (dataArea.getMaxX() + aRadius))) { 479 Ellipse2D.Double avgEllipse = new Ellipse2D.Double( 480 yyAverage - aRadius, xx - aRadius, aRadius * 2, 481 aRadius * 2); 482 g2.fill(avgEllipse); 483 g2.draw(avgEllipse); 484 } 485 } 486 487 // FIXME: draw outliers 488 489 // add an entity for the item... 490 if (entities != null && box.intersects(dataArea)) { 491 addEntity(entities, box, dataset, series, item, yyAverage, xx); 492 } 493 494 } 495 496 /** 497 * Draws the visual representation of a single data item. 498 * 499 * @param g2 the graphics device. 500 * @param dataArea the area within which the plot is being drawn. 501 * @param info collects info about the drawing. 502 * @param plot the plot (can be used to obtain standard color 503 * information etc). 504 * @param domainAxis the domain axis. 505 * @param rangeAxis the range axis. 506 * @param dataset the dataset (must be an instance of 507 * {@link BoxAndWhiskerXYDataset}). 508 * @param series the series index (zero-based). 509 * @param item the item index (zero-based). 510 * @param crosshairState crosshair information for the plot 511 * (<code>null</code> permitted). 512 * @param pass the pass index. 513 */ 514 public void drawVerticalItem(Graphics2D g2, Rectangle2D dataArea, 515 PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis, 516 ValueAxis rangeAxis, XYDataset dataset, int series, 517 int item, CrosshairState crosshairState, int pass) { 518 519 // setup for collecting optional entity info... 520 EntityCollection entities = null; 521 if (info != null) { 522 entities = info.getOwner().getEntityCollection(); 523 } 524 525 BoxAndWhiskerXYDataset boxAndWhiskerData 526 = (BoxAndWhiskerXYDataset) dataset; 527 528 Number x = boxAndWhiskerData.getX(series, item); 529 Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item); 530 Number yMin = boxAndWhiskerData.getMinRegularValue(series, item); 531 Number yMedian = boxAndWhiskerData.getMedianValue(series, item); 532 Number yAverage = boxAndWhiskerData.getMeanValue(series, item); 533 Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item); 534 Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item); 535 List yOutliers = boxAndWhiskerData.getOutliers(series, item); 536 // yOutliers can be null, but we'd prefer it to be an empty list in 537 // that case... 538 if (yOutliers == null) { 539 yOutliers = Collections.EMPTY_LIST; 540 } 541 542 double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 543 plot.getDomainAxisEdge()); 544 545 RectangleEdge location = plot.getRangeAxisEdge(); 546 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 547 location); 548 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 549 location); 550 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 551 dataArea, location); 552 double yyAverage = 0.0; 553 if (yAverage != null) { 554 yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 555 dataArea, location); 556 } 557 double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 558 dataArea, location); 559 double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 560 dataArea, location); 561 double yyOutlier; 562 563 double exactBoxWidth = getBoxWidth(); 564 double width = exactBoxWidth; 565 double dataAreaX = dataArea.getMaxX() - dataArea.getMinX(); 566 double maxBoxPercent = 0.1; 567 double maxBoxWidth = dataAreaX * maxBoxPercent; 568 if (exactBoxWidth <= 0.0) { 569 int itemCount = boxAndWhiskerData.getItemCount(series); 570 exactBoxWidth = dataAreaX / itemCount * 4.5 / 7; 571 if (exactBoxWidth < 3) { 572 width = 3; 573 } 574 else if (exactBoxWidth > maxBoxWidth) { 575 width = maxBoxWidth; 576 } 577 else { 578 width = exactBoxWidth; 579 } 580 } 581 582 g2.setPaint(getItemPaint(series, item)); 583 Stroke s = getItemStroke(series, item); 584 g2.setStroke(s); 585 586 // draw the upper shadow 587 g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median)); 588 g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2, 589 yyMax)); 590 591 // draw the lower shadow 592 g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median)); 593 g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2, 594 yyMin)); 595 596 // draw the body 597 Shape box = null; 598 if (yyQ1Median > yyQ3Median) { 599 box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width, 600 yyQ1Median - yyQ3Median); 601 } 602 else { 603 box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width, 604 yyQ3Median - yyQ1Median); 605 } 606 if (this.fillBox) { 607 g2.setPaint(lookupBoxPaint(series, item)); 608 g2.fill(box); 609 } 610 g2.setStroke(getItemOutlineStroke(series, item)); 611 g2.setPaint(getItemOutlinePaint(series, item)); 612 g2.draw(box); 613 614 // draw median 615 g2.setPaint(getArtifactPaint()); 616 g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2, 617 yyMedian)); 618 619 double aRadius = 0; // average radius 620 double oRadius = width / 3; // outlier radius 621 622 // draw average - SPECIAL AIMS REQUIREMENT 623 if (yAverage != null) { 624 aRadius = width / 4; 625 // here we check that the average marker will in fact be visible 626 // before drawing it... 627 if ((yyAverage > (dataArea.getMinY() - aRadius)) 628 && (yyAverage < (dataArea.getMaxY() + aRadius))) { 629 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius, 630 yyAverage - aRadius, aRadius * 2, aRadius * 2); 631 g2.fill(avgEllipse); 632 g2.draw(avgEllipse); 633 } 634 } 635 636 List outliers = new ArrayList(); 637 OutlierListCollection outlierListCollection 638 = new OutlierListCollection(); 639 640 /* From outlier array sort out which are outliers and put these into 641 * an arraylist. If there are any farouts, set the flag on the 642 * OutlierListCollection 643 */ 644 for (int i = 0; i < yOutliers.size(); i++) { 645 double outlier = ((Number) yOutliers.get(i)).doubleValue(); 646 if (outlier > boxAndWhiskerData.getMaxOutlier(series, 647 item).doubleValue()) { 648 outlierListCollection.setHighFarOut(true); 649 } 650 else if (outlier < boxAndWhiskerData.getMinOutlier(series, 651 item).doubleValue()) { 652 outlierListCollection.setLowFarOut(true); 653 } 654 else if (outlier > boxAndWhiskerData.getMaxRegularValue(series, 655 item).doubleValue()) { 656 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 657 location); 658 outliers.add(new Outlier(xx, yyOutlier, oRadius)); 659 } 660 else if (outlier < boxAndWhiskerData.getMinRegularValue(series, 661 item).doubleValue()) { 662 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 663 location); 664 outliers.add(new Outlier(xx, yyOutlier, oRadius)); 665 } 666 Collections.sort(outliers); 667 } 668 669 // Process outliers. Each outlier is either added to the appropriate 670 // outlier list or a new outlier list is made 671 for (Iterator iterator = outliers.iterator(); iterator.hasNext();) { 672 Outlier outlier = (Outlier) iterator.next(); 673 outlierListCollection.add(outlier); 674 } 675 676 // draw yOutliers 677 double maxAxisValue = rangeAxis.valueToJava2D(rangeAxis.getUpperBound(), 678 dataArea, location) + aRadius; 679 double minAxisValue = rangeAxis.valueToJava2D(rangeAxis.getLowerBound(), 680 dataArea, location) - aRadius; 681 682 // draw outliers 683 for (Iterator iterator = outlierListCollection.iterator(); 684 iterator.hasNext();) { 685 OutlierList list = (OutlierList) iterator.next(); 686 Outlier outlier = list.getAveragedOutlier(); 687 Point2D point = outlier.getPoint(); 688 689 if (list.isMultiple()) { 690 drawMultipleEllipse(point, width, oRadius, g2); 691 } 692 else { 693 drawEllipse(point, oRadius, g2); 694 } 695 } 696 697 // draw farout 698 if (outlierListCollection.isHighFarOut()) { 699 drawHighFarOut(aRadius, g2, xx, maxAxisValue); 700 } 701 702 if (outlierListCollection.isLowFarOut()) { 703 drawLowFarOut(aRadius, g2, xx, minAxisValue); 704 } 705 706 // add an entity for the item... 707 if (entities != null && box.intersects(dataArea)) { 708 addEntity(entities, box, dataset, series, item, xx, yyAverage); 709 } 710 711 } 712 713 /** 714 * Draws an ellipse to represent an outlier. 715 * 716 * @param point the location. 717 * @param oRadius the radius. 718 * @param g2 the graphics device. 719 */ 720 protected void drawEllipse(Point2D point, double oRadius, Graphics2D g2) { 721 Ellipse2D.Double dot = new Ellipse2D.Double(point.getX() + oRadius / 2, 722 point.getY(), oRadius, oRadius); 723 g2.draw(dot); 724 } 725 726 /** 727 * Draws two ellipses to represent overlapping outliers. 728 * 729 * @param point the location. 730 * @param boxWidth the box width. 731 * @param oRadius the radius. 732 * @param g2 the graphics device. 733 */ 734 protected void drawMultipleEllipse(Point2D point, double boxWidth, 735 double oRadius, Graphics2D g2) { 736 737 Ellipse2D.Double dot1 = new Ellipse2D.Double(point.getX() 738 - (boxWidth / 2) + oRadius, point.getY(), oRadius, oRadius); 739 Ellipse2D.Double dot2 = new Ellipse2D.Double(point.getX() 740 + (boxWidth / 2), point.getY(), oRadius, oRadius); 741 g2.draw(dot1); 742 g2.draw(dot2); 743 744 } 745 746 /** 747 * Draws a triangle to indicate the presence of far out values. 748 * 749 * @param aRadius the radius. 750 * @param g2 the graphics device. 751 * @param xx the x value. 752 * @param m the max y value. 753 */ 754 protected void drawHighFarOut(double aRadius, Graphics2D g2, double xx, 755 double m) { 756 double side = aRadius * 2; 757 g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side)); 758 g2.draw(new Line2D.Double(xx - side, m + side, xx, m)); 759 g2.draw(new Line2D.Double(xx + side, m + side, xx, m)); 760 } 761 762 /** 763 * Draws a triangle to indicate the presence of far out values. 764 * 765 * @param aRadius the radius. 766 * @param g2 the graphics device. 767 * @param xx the x value. 768 * @param m the min y value. 769 */ 770 protected void drawLowFarOut(double aRadius, Graphics2D g2, double xx, 771 double m) { 772 double side = aRadius * 2; 773 g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side)); 774 g2.draw(new Line2D.Double(xx - side, m - side, xx, m)); 775 g2.draw(new Line2D.Double(xx + side, m - side, xx, m)); 776 } 777 778 /** 779 * Tests this renderer for equality with another object. 780 * 781 * @param obj the object (<code>null</code> permitted). 782 * 783 * @return <code>true</code> or <code>false</code>. 784 */ 785 public boolean equals(Object obj) { 786 if (obj == this) { 787 return true; 788 } 789 if (!(obj instanceof XYBoxAndWhiskerRenderer)) { 790 return false; 791 } 792 if (!super.equals(obj)) { 793 return false; 794 } 795 XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj; 796 if (this.boxWidth != that.getBoxWidth()) { 797 return false; 798 } 799 if (!PaintUtilities.equal(this.boxPaint, that.boxPaint)) { 800 return false; 801 } 802 if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) { 803 return false; 804 } 805 if (this.fillBox != that.fillBox) { 806 return false; 807 } 808 return true; 809 810 } 811 812 /** 813 * Provides serialization support. 814 * 815 * @param stream the output stream. 816 * 817 * @throws IOException if there is an I/O error. 818 */ 819 private void writeObject(ObjectOutputStream stream) throws IOException { 820 stream.defaultWriteObject(); 821 SerialUtilities.writePaint(this.boxPaint, stream); 822 SerialUtilities.writePaint(this.artifactPaint, stream); 823 } 824 825 /** 826 * Provides serialization support. 827 * 828 * @param stream the input stream. 829 * 830 * @throws IOException if there is an I/O error. 831 * @throws ClassNotFoundException if there is a classpath problem. 832 */ 833 private void readObject(ObjectInputStream stream) 834 throws IOException, ClassNotFoundException { 835 836 stream.defaultReadObject(); 837 this.boxPaint = SerialUtilities.readPaint(stream); 838 this.artifactPaint = SerialUtilities.readPaint(stream); 839 } 840 841 /** 842 * Returns a clone of the renderer. 843 * 844 * @return A clone. 845 * 846 * @throws CloneNotSupportedException if the renderer cannot be cloned. 847 */ 848 public Object clone() throws CloneNotSupportedException { 849 return super.clone(); 850 } 851 852 }