001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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, 2004, 2007, 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 * $Id: XYBoxAndWhiskerRenderer.java,v 1.6.2.6 2007/02/05 11:42:29 mungady Exp $ 037 * 038 * Changes 039 * ------- 040 * 05-Aug-2003 : Version 1, contributed by David Browning. Based on code in the 041 * CandlestickRenderer class. Additional modifications by David 042 * Gilbert to make the code work with 0.9.10 changes (DG); 043 * 08-Aug-2003 : Updated some of the Javadoc 044 * Allowed BoxAndwhiskerDataset Average value to be null - the 045 * average value is an AIMS requirement 046 * Allow the outlier and farout coefficients to be set - though 047 * at the moment this only affects the calculation of farouts. 048 * Added artifactPaint variable and setter/getter 049 * 12-Aug-2003 Rewrote code to sort out and process outliers to take 050 * advantage of changes in DefaultBoxAndWhiskerDataset 051 * Added a limit of 10% for width of box should no width be 052 * specified...maybe this should be setable??? 053 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG); 054 * 08-Sep-2003 : Changed ValueAxis API (DG); 055 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 056 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG); 057 * 23-Apr-2004 : Added fillBox attribute, extended equals() method and fixed 058 * serialization issue (DG); 059 * 29-Apr-2004 : Fixed problem with drawing upper and lower shadows - bug id 060 * 944011 (DG); 061 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 062 * getYValue() (DG); 063 * 01-Oct-2004 : Renamed 'paint' --> 'boxPaint' to avoid conflict with 064 * inherited attribute (DG); 065 * 10-Jun-2005 : Updated equals() to handle GradientPaint (DG); 066 * 06-Oct-2005 : Removed setPaint() call in drawItem(), it is causing a 067 * loop (DG); 068 * ------------- JFREECHART 1.0.x --------------------------------------------- 069 * 02-Feb-2007 : Removed author tags from all over JFreeChart sources (DG); 070 * 05-Feb-2007 : Added event notifications and fixed drawing for horizontal 071 * plot orientation (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.entity.XYItemEntity; 098 import org.jfree.chart.event.RendererChangeEvent; 099 import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator; 100 import org.jfree.chart.labels.XYToolTipGenerator; 101 import org.jfree.chart.plot.CrosshairState; 102 import org.jfree.chart.plot.PlotOrientation; 103 import org.jfree.chart.plot.PlotRenderingInfo; 104 import org.jfree.chart.plot.XYPlot; 105 import org.jfree.chart.renderer.Outlier; 106 import org.jfree.chart.renderer.OutlierList; 107 import org.jfree.chart.renderer.OutlierListCollection; 108 import org.jfree.data.statistics.BoxAndWhiskerXYDataset; 109 import org.jfree.data.xy.XYDataset; 110 import org.jfree.io.SerialUtilities; 111 import org.jfree.ui.RectangleEdge; 112 import org.jfree.util.PaintUtilities; 113 import org.jfree.util.PublicCloneable; 114 115 /** 116 * A renderer that draws box-and-whisker items on an {@link XYPlot}. This 117 * renderer requires a {@link BoxAndWhiskerXYDataset}). 118 * <P> 119 * This renderer does not include any code to calculate the crosshair point. 120 */ 121 public class XYBoxAndWhiskerRenderer extends AbstractXYItemRenderer 122 implements XYItemRenderer, 123 Cloneable, 124 PublicCloneable, 125 Serializable { 126 127 /** For serialization. */ 128 private static final long serialVersionUID = -8020170108532232324L; 129 130 /** The box width. */ 131 private double boxWidth; 132 133 /** The paint used to fill the box. */ 134 private transient Paint boxPaint; 135 136 /** A flag that controls whether or not the box is filled. */ 137 private boolean fillBox; 138 139 /** 140 * The paint used to draw various artifacts such as outliers, farout 141 * symbol, average ellipse and median line. 142 */ 143 private transient Paint artifactPaint = Color.black; 144 145 /** 146 * Creates a new renderer for box and whisker charts. 147 */ 148 public XYBoxAndWhiskerRenderer() { 149 this(-1.0); 150 } 151 152 /** 153 * Creates a new renderer for box and whisker charts. 154 * <P> 155 * Use -1 for the box width if you prefer the width to be calculated 156 * automatically. 157 * 158 * @param boxWidth the box width. 159 */ 160 public XYBoxAndWhiskerRenderer(double boxWidth) { 161 super(); 162 this.boxWidth = boxWidth; 163 this.boxPaint = Color.green; 164 this.fillBox = true; 165 setToolTipGenerator(new BoxAndWhiskerXYToolTipGenerator()); 166 } 167 168 /** 169 * Returns the width of each box. 170 * 171 * @return The box width. 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 public void setBoxWidth(double width) { 187 if (width != this.boxWidth) { 188 this.boxWidth = width; 189 notifyListeners(new RendererChangeEvent(this)); 190 } 191 } 192 193 /** 194 * Returns the paint used to fill boxes. 195 * 196 * @return The paint (possibly <code>null</code>). 197 */ 198 public Paint getBoxPaint() { 199 return this.boxPaint; 200 } 201 202 /** 203 * Sets the paint used to fill boxes and sends a {@link RendererChangeEvent} 204 * to all registered listeners. 205 * 206 * @param paint the paint (<code>null</code> permitted). 207 */ 208 public void setBoxPaint(Paint paint) { 209 this.boxPaint = paint; 210 notifyListeners(new RendererChangeEvent(this)); 211 } 212 213 /** 214 * Returns the flag that controls whether or not the box is filled. 215 * 216 * @return A boolean. 217 */ 218 public boolean getFillBox() { 219 return this.fillBox; 220 } 221 222 /** 223 * Sets the flag that controls whether or not the box is filled and sends a 224 * {@link RendererChangeEvent} to all registered listeners. 225 * 226 * @param flag the flag. 227 */ 228 public void setFillBox(boolean flag) { 229 this.fillBox = flag; 230 notifyListeners(new RendererChangeEvent(this)); 231 } 232 233 /** 234 * Returns the paint used to paint the various artifacts such as outliers, 235 * farout symbol, median line and the averages ellipse. 236 * 237 * @return The paint (never <code>null</code>). 238 */ 239 public Paint getArtifactPaint() { 240 return this.artifactPaint; 241 } 242 243 /** 244 * Sets the paint used to paint the various artifacts such as outliers, 245 * farout symbol, median line and the averages ellipse. 246 * 247 * @param artifactPaint the paint (<code>null</code> not permitted). 248 */ 249 public void setArtifactPaint(Paint artifactPaint) { 250 if (artifactPaint == null) { 251 throw new IllegalArgumentException( 252 "Null 'artifactPaint' argument."); 253 } 254 this.artifactPaint = artifactPaint; 255 notifyListeners(new RendererChangeEvent(this)); 256 } 257 258 /** 259 * Draws the visual representation of a single data item. 260 * 261 * @param g2 the graphics device. 262 * @param state the renderer state. 263 * @param dataArea the area within which the plot is being drawn. 264 * @param info collects info about the drawing. 265 * @param plot the plot (can be used to obtain standard color 266 * information etc). 267 * @param domainAxis the domain axis. 268 * @param rangeAxis the range axis. 269 * @param dataset the dataset. 270 * @param series the series index (zero-based). 271 * @param item the item index (zero-based). 272 * @param crosshairState crosshair information for the plot 273 * (<code>null</code> permitted). 274 * @param pass the pass index. 275 */ 276 public void drawItem(Graphics2D g2, 277 XYItemRendererState state, 278 Rectangle2D dataArea, 279 PlotRenderingInfo info, 280 XYPlot plot, 281 ValueAxis domainAxis, 282 ValueAxis rangeAxis, 283 XYDataset dataset, 284 int series, 285 int item, 286 CrosshairState crosshairState, 287 int pass) { 288 289 PlotOrientation orientation = plot.getOrientation(); 290 291 if (orientation == PlotOrientation.HORIZONTAL) { 292 drawHorizontalItem(g2, dataArea, info, plot, domainAxis, rangeAxis, 293 dataset, series, item, crosshairState, pass); 294 } 295 else if (orientation == PlotOrientation.VERTICAL) { 296 drawVerticalItem(g2, dataArea, info, plot, domainAxis, rangeAxis, 297 dataset, series, item, crosshairState, pass); 298 } 299 300 } 301 302 /** 303 * Draws the visual representation of a single data item. 304 * 305 * @param g2 the graphics device. 306 * @param dataArea the area within which the plot is being drawn. 307 * @param info collects info about the drawing. 308 * @param plot the plot (can be used to obtain standard color 309 * information etc). 310 * @param domainAxis the domain axis. 311 * @param rangeAxis the range axis. 312 * @param dataset the dataset. 313 * @param series the series index (zero-based). 314 * @param item the item index (zero-based). 315 * @param crosshairState crosshair information for the plot 316 * (<code>null</code> permitted). 317 * @param pass the pass index. 318 */ 319 public void drawHorizontalItem(Graphics2D g2, 320 Rectangle2D dataArea, 321 PlotRenderingInfo info, 322 XYPlot plot, 323 ValueAxis domainAxis, 324 ValueAxis rangeAxis, 325 XYDataset dataset, 326 int series, 327 int item, 328 CrosshairState crosshairState, 329 int pass) { 330 331 // setup for collecting optional entity info... 332 EntityCollection entities = null; 333 if (info != null) { 334 entities = info.getOwner().getEntityCollection(); 335 } 336 337 BoxAndWhiskerXYDataset boxAndWhiskerData 338 = (BoxAndWhiskerXYDataset) dataset; 339 340 Number x = boxAndWhiskerData.getX(series, item); 341 Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item); 342 Number yMin = boxAndWhiskerData.getMinRegularValue(series, item); 343 Number yMedian = boxAndWhiskerData.getMedianValue(series, item); 344 Number yAverage = boxAndWhiskerData.getMeanValue(series, item); 345 Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item); 346 Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item); 347 348 double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 349 plot.getDomainAxisEdge()); 350 351 RectangleEdge location = plot.getRangeAxisEdge(); 352 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 353 location); 354 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 355 location); 356 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 357 dataArea, location); 358 double yyAverage = 0.0; 359 if (yAverage != null) { 360 yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 361 dataArea, location); 362 } 363 double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 364 dataArea, location); 365 double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 366 dataArea, location); 367 368 double exactBoxWidth = getBoxWidth(); 369 double width = exactBoxWidth; 370 double dataAreaX = dataArea.getHeight(); 371 double maxBoxPercent = 0.1; 372 double maxBoxWidth = dataAreaX * maxBoxPercent; 373 if (exactBoxWidth <= 0.0) { 374 int itemCount = boxAndWhiskerData.getItemCount(series); 375 exactBoxWidth = dataAreaX / itemCount * 4.5 / 7; 376 if (exactBoxWidth < 3) { 377 width = 3; 378 } 379 else if (exactBoxWidth > maxBoxWidth) { 380 width = maxBoxWidth; 381 } 382 else { 383 width = exactBoxWidth; 384 } 385 } 386 387 Paint p = getBoxPaint(); 388 if (p != null) { 389 g2.setPaint(p); 390 } 391 Stroke s = getItemStroke(series, item); 392 g2.setStroke(s); 393 394 // draw the upper shadow 395 g2.draw(new Line2D.Double(yyMax, xx, yyQ3Median, xx)); 396 g2.draw(new Line2D.Double(yyMax, xx - width / 2, yyMax, 397 xx + width / 2)); 398 399 // draw the lower shadow 400 g2.draw(new Line2D.Double(yyMin, xx, yyQ1Median, xx)); 401 g2.draw(new Line2D.Double(yyMin, xx - width / 2, yyMin, 402 xx + width / 2)); 403 404 // draw the body 405 Shape box = null; 406 if (yyQ1Median < yyQ3Median) { 407 box = new Rectangle2D.Double(yyQ1Median, xx - width / 2, 408 yyQ3Median - yyQ1Median, width); 409 } 410 else { 411 box = new Rectangle2D.Double(yyQ3Median, xx - width / 2, 412 yyQ1Median - yyQ3Median, width); 413 } 414 if (getBoxPaint() != null) { 415 g2.setPaint(getBoxPaint()); 416 } 417 if (this.fillBox) { 418 g2.fill(box); 419 } 420 g2.draw(box); 421 422 // draw median 423 g2.setPaint(getArtifactPaint()); 424 g2.draw(new Line2D.Double(yyMedian, 425 xx - width / 2, yyMedian, xx + width / 2)); 426 427 // draw average - SPECIAL AIMS REQUIREMENT 428 if (yAverage != null) { 429 double aRadius = width / 4; 430 Ellipse2D.Double avgEllipse = new Ellipse2D.Double( 431 yyAverage - aRadius, xx - aRadius, aRadius * 2, 432 aRadius * 2); 433 g2.fill(avgEllipse); 434 g2.draw(avgEllipse); 435 } 436 437 // FIXME: draw outliers 438 439 // add an entity for the item... 440 if (entities != null) { 441 String tip = null; 442 XYToolTipGenerator generator = getToolTipGenerator(series, item); 443 if (generator != null) { 444 tip = generator.generateToolTip(dataset, series, item); 445 } 446 String url = null; 447 if (getURLGenerator() != null) { 448 url = getURLGenerator().generateURL(dataset, series, item); 449 } 450 XYItemEntity entity = new XYItemEntity(box, dataset, series, item, 451 tip, url); 452 entities.add(entity); 453 } 454 455 } 456 457 /** 458 * Draws the visual representation of a single data item. 459 * 460 * @param g2 the graphics device. 461 * @param dataArea the area within which the plot is being drawn. 462 * @param info collects info about the drawing. 463 * @param plot the plot (can be used to obtain standard color 464 * information etc). 465 * @param domainAxis the domain axis. 466 * @param rangeAxis the range axis. 467 * @param dataset the dataset. 468 * @param series the series index (zero-based). 469 * @param item the item index (zero-based). 470 * @param crosshairState crosshair information for the plot 471 * (<code>null</code> permitted). 472 * @param pass the pass index. 473 */ 474 public void drawVerticalItem(Graphics2D g2, 475 Rectangle2D dataArea, 476 PlotRenderingInfo info, 477 XYPlot plot, 478 ValueAxis domainAxis, 479 ValueAxis rangeAxis, 480 XYDataset dataset, 481 int series, 482 int item, 483 CrosshairState crosshairState, 484 int pass) { 485 486 // setup for collecting optional entity info... 487 EntityCollection entities = null; 488 if (info != null) { 489 entities = info.getOwner().getEntityCollection(); 490 } 491 492 BoxAndWhiskerXYDataset boxAndWhiskerData 493 = (BoxAndWhiskerXYDataset) dataset; 494 495 Number x = boxAndWhiskerData.getX(series, item); 496 Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item); 497 Number yMin = boxAndWhiskerData.getMinRegularValue(series, item); 498 Number yMedian = boxAndWhiskerData.getMedianValue(series, item); 499 Number yAverage = boxAndWhiskerData.getMeanValue(series, item); 500 Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item); 501 Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item); 502 List yOutliers = boxAndWhiskerData.getOutliers(series, item); 503 504 double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 505 plot.getDomainAxisEdge()); 506 507 RectangleEdge location = plot.getRangeAxisEdge(); 508 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 509 location); 510 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 511 location); 512 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 513 dataArea, location); 514 double yyAverage = 0.0; 515 if (yAverage != null) { 516 yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 517 dataArea, location); 518 } 519 double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 520 dataArea, location); 521 double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 522 dataArea, location); 523 double yyOutlier; 524 525 526 double exactBoxWidth = getBoxWidth(); 527 double width = exactBoxWidth; 528 double dataAreaX = dataArea.getMaxX() - dataArea.getMinX(); 529 double maxBoxPercent = 0.1; 530 double maxBoxWidth = dataAreaX * maxBoxPercent; 531 if (exactBoxWidth <= 0.0) { 532 int itemCount = boxAndWhiskerData.getItemCount(series); 533 exactBoxWidth = dataAreaX / itemCount * 4.5 / 7; 534 if (exactBoxWidth < 3) { 535 width = 3; 536 } 537 else if (exactBoxWidth > maxBoxWidth) { 538 width = maxBoxWidth; 539 } 540 else { 541 width = exactBoxWidth; 542 } 543 } 544 545 Paint p = getBoxPaint(); 546 if (p != null) { 547 g2.setPaint(p); 548 } 549 Stroke s = getItemStroke(series, item); 550 551 g2.setStroke(s); 552 553 // draw the upper shadow 554 g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median)); 555 g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2, 556 yyMax)); 557 558 // draw the lower shadow 559 g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median)); 560 g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2, 561 yyMin)); 562 563 // draw the body 564 Shape box = null; 565 if (yyQ1Median > yyQ3Median) { 566 box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width, 567 yyQ1Median - yyQ3Median); 568 } 569 else { 570 box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width, 571 yyQ3Median - yyQ1Median); 572 } 573 if (this.fillBox) { 574 g2.fill(box); 575 } 576 g2.draw(box); 577 578 // draw median 579 g2.setPaint(getArtifactPaint()); 580 g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2, 581 yyMedian)); 582 583 double aRadius = 0; // average radius 584 double oRadius = width / 3; // outlier radius 585 586 // draw average - SPECIAL AIMS REQUIREMENT 587 if (yAverage != null) { 588 aRadius = width / 4; 589 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius, 590 yyAverage - aRadius, aRadius * 2, aRadius * 2); 591 g2.fill(avgEllipse); 592 g2.draw(avgEllipse); 593 } 594 595 List outliers = new ArrayList(); 596 OutlierListCollection outlierListCollection 597 = new OutlierListCollection(); 598 599 /* From outlier array sort out which are outliers and put these into 600 * an arraylist. If there are any farouts, set the flag on the 601 * OutlierListCollection 602 */ 603 604 for (int i = 0; i < yOutliers.size(); i++) { 605 double outlier = ((Number) yOutliers.get(i)).doubleValue(); 606 if (outlier > boxAndWhiskerData.getMaxOutlier(series, 607 item).doubleValue()) { 608 outlierListCollection.setHighFarOut(true); 609 } 610 else if (outlier < boxAndWhiskerData.getMinOutlier(series, 611 item).doubleValue()) { 612 outlierListCollection.setLowFarOut(true); 613 } 614 else if (outlier > boxAndWhiskerData.getMaxRegularValue(series, 615 item).doubleValue()) { 616 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 617 location); 618 outliers.add(new Outlier(xx, yyOutlier, oRadius)); 619 } 620 else if (outlier < boxAndWhiskerData.getMinRegularValue(series, 621 item).doubleValue()) { 622 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 623 location); 624 outliers.add(new Outlier(xx, yyOutlier, oRadius)); 625 } 626 Collections.sort(outliers); 627 } 628 629 // Process outliers. Each outlier is either added to the appropriate 630 // outlier list or a new outlier list is made 631 for (Iterator iterator = outliers.iterator(); iterator.hasNext();) { 632 Outlier outlier = (Outlier) iterator.next(); 633 outlierListCollection.add(outlier); 634 } 635 636 // draw yOutliers 637 double maxAxisValue = rangeAxis.valueToJava2D( 638 rangeAxis.getUpperBound(), dataArea, location 639 ) + aRadius; 640 double minAxisValue = rangeAxis.valueToJava2D( 641 rangeAxis.getLowerBound(), dataArea, location 642 ) - aRadius; 643 644 // draw outliers 645 for (Iterator iterator = outlierListCollection.iterator(); 646 iterator.hasNext();) { 647 OutlierList list = (OutlierList) iterator.next(); 648 Outlier outlier = list.getAveragedOutlier(); 649 Point2D point = outlier.getPoint(); 650 651 if (list.isMultiple()) { 652 drawMultipleEllipse(point, width, oRadius, g2); 653 } 654 else { 655 drawEllipse(point, oRadius, g2); 656 } 657 } 658 659 // draw farout 660 if (outlierListCollection.isHighFarOut()) { 661 drawHighFarOut(aRadius, g2, xx, maxAxisValue); 662 } 663 664 if (outlierListCollection.isLowFarOut()) { 665 drawLowFarOut(aRadius, g2, xx, minAxisValue); 666 } 667 668 // add an entity for the item... 669 if (entities != null) { 670 String tip = null; 671 XYToolTipGenerator generator = getToolTipGenerator(series, item); 672 if (generator != null) { 673 tip = generator.generateToolTip(dataset, series, item); 674 } 675 String url = null; 676 if (getURLGenerator() != null) { 677 url = getURLGenerator().generateURL(dataset, series, item); 678 } 679 XYItemEntity entity = new XYItemEntity(box, dataset, series, item, 680 tip, url); 681 entities.add(entity); 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 }