001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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-2014, 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 079package org.jfree.chart.renderer.xy; 080 081import java.awt.Color; 082import java.awt.Graphics2D; 083import java.awt.Paint; 084import java.awt.Shape; 085import java.awt.Stroke; 086import java.awt.geom.Ellipse2D; 087import java.awt.geom.Line2D; 088import java.awt.geom.Point2D; 089import java.awt.geom.Rectangle2D; 090import java.io.IOException; 091import java.io.ObjectInputStream; 092import java.io.ObjectOutputStream; 093import java.io.Serializable; 094import java.util.ArrayList; 095import java.util.Collections; 096import java.util.Iterator; 097import java.util.List; 098 099import org.jfree.chart.axis.ValueAxis; 100import org.jfree.chart.entity.EntityCollection; 101import org.jfree.chart.event.RendererChangeEvent; 102import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator; 103import org.jfree.chart.plot.CrosshairState; 104import org.jfree.chart.plot.PlotOrientation; 105import org.jfree.chart.plot.PlotRenderingInfo; 106import org.jfree.chart.plot.XYPlot; 107import org.jfree.chart.renderer.Outlier; 108import org.jfree.chart.renderer.OutlierList; 109import org.jfree.chart.renderer.OutlierListCollection; 110import org.jfree.chart.util.ParamChecks; 111import org.jfree.data.Range; 112import org.jfree.data.statistics.BoxAndWhiskerXYDataset; 113import org.jfree.data.xy.XYDataset; 114import org.jfree.io.SerialUtilities; 115import org.jfree.ui.RectangleEdge; 116import org.jfree.util.PaintUtilities; 117import 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 */ 130public 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 ParamChecks.nullNotPermitted(paint, "paint"); 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 @Override 290 public Range findRangeBounds(XYDataset dataset) { 291 return findRangeBounds(dataset, true); 292 } 293 294 /** 295 * Returns the box paint or, if this is <code>null</code>, the item 296 * paint. 297 * 298 * @param series the series index. 299 * @param item the item index. 300 * 301 * @return The paint used to fill the box for the specified item (never 302 * <code>null</code>). 303 * 304 * @since 1.0.10 305 */ 306 protected Paint lookupBoxPaint(int series, int item) { 307 Paint p = getBoxPaint(); 308 if (p != null) { 309 return p; 310 } 311 else { 312 // TODO: could change this to itemFillPaint(). For backwards 313 // compatibility, it might require a useFillPaint flag. 314 return getItemPaint(series, item); 315 } 316 } 317 318 /** 319 * Draws the visual representation of a single data item. 320 * 321 * @param g2 the graphics device. 322 * @param state the renderer state. 323 * @param dataArea the area within which the plot is being drawn. 324 * @param info collects info about the drawing. 325 * @param plot the plot (can be used to obtain standard color 326 * information etc). 327 * @param domainAxis the domain axis. 328 * @param rangeAxis the range axis. 329 * @param dataset the dataset (must be an instance of 330 * {@link BoxAndWhiskerXYDataset}). 331 * @param series the series index (zero-based). 332 * @param item the item index (zero-based). 333 * @param crosshairState crosshair information for the plot 334 * (<code>null</code> permitted). 335 * @param pass the pass index. 336 */ 337 @Override 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; 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; 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 @Override 786 public boolean equals(Object obj) { 787 if (obj == this) { 788 return true; 789 } 790 if (!(obj instanceof XYBoxAndWhiskerRenderer)) { 791 return false; 792 } 793 if (!super.equals(obj)) { 794 return false; 795 } 796 XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj; 797 if (this.boxWidth != that.getBoxWidth()) { 798 return false; 799 } 800 if (!PaintUtilities.equal(this.boxPaint, that.boxPaint)) { 801 return false; 802 } 803 if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) { 804 return false; 805 } 806 if (this.fillBox != that.fillBox) { 807 return false; 808 } 809 return true; 810 811 } 812 813 /** 814 * Provides serialization support. 815 * 816 * @param stream the output stream. 817 * 818 * @throws IOException if there is an I/O error. 819 */ 820 private void writeObject(ObjectOutputStream stream) throws IOException { 821 stream.defaultWriteObject(); 822 SerialUtilities.writePaint(this.boxPaint, stream); 823 SerialUtilities.writePaint(this.artifactPaint, stream); 824 } 825 826 /** 827 * Provides serialization support. 828 * 829 * @param stream the input stream. 830 * 831 * @throws IOException if there is an I/O error. 832 * @throws ClassNotFoundException if there is a classpath problem. 833 */ 834 private void readObject(ObjectInputStream stream) 835 throws IOException, ClassNotFoundException { 836 837 stream.defaultReadObject(); 838 this.boxPaint = SerialUtilities.readPaint(stream); 839 this.artifactPaint = SerialUtilities.readPaint(stream); 840 } 841 842 /** 843 * Returns a clone of the renderer. 844 * 845 * @return A clone. 846 * 847 * @throws CloneNotSupportedException if the renderer cannot be cloned. 848 */ 849 @Override 850 public Object clone() throws CloneNotSupportedException { 851 return super.clone(); 852 } 853 854}