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