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