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