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