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