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