001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, 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 * [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-2020, 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 */ 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 * @since 1.0.10 264 */ 265 protected Paint lookupBoxPaint(int series, int item) { 266 Paint p = getBoxPaint(); 267 if (p != null) { 268 return p; 269 } 270 else { 271 // TODO: could change this to itemFillPaint(). For backwards 272 // compatibility, it might require a useFillPaint flag. 273 return getItemPaint(series, item); 274 } 275 } 276 277 /** 278 * Draws the visual representation of a single data item. 279 * 280 * @param g2 the graphics device. 281 * @param state the renderer state. 282 * @param dataArea the area within which the plot is being drawn. 283 * @param info collects info about the drawing. 284 * @param plot the plot (can be used to obtain standard color 285 * information etc). 286 * @param domainAxis the domain axis. 287 * @param rangeAxis the range axis. 288 * @param dataset the dataset (must be an instance of 289 * {@link BoxAndWhiskerXYDataset}). 290 * @param series the series index (zero-based). 291 * @param item the item index (zero-based). 292 * @param crosshairState crosshair information for the plot 293 * ({@code null} permitted). 294 * @param pass the pass index. 295 */ 296 @Override 297 public void drawItem(Graphics2D g2, XYItemRendererState state, 298 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 299 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 300 int series, int item, CrosshairState crosshairState, int pass) { 301 302 PlotOrientation orientation = plot.getOrientation(); 303 304 if (orientation == PlotOrientation.HORIZONTAL) { 305 drawHorizontalItem(g2, dataArea, info, plot, domainAxis, rangeAxis, 306 dataset, series, item, crosshairState, pass); 307 } 308 else if (orientation == PlotOrientation.VERTICAL) { 309 drawVerticalItem(g2, dataArea, info, plot, domainAxis, rangeAxis, 310 dataset, series, item, crosshairState, pass); 311 } 312 313 } 314 315 /** 316 * Draws the visual representation of a single data item. 317 * 318 * @param g2 the graphics device. 319 * @param dataArea the area within which the plot is being drawn. 320 * @param info collects info about the drawing. 321 * @param plot the plot (can be used to obtain standard color 322 * information etc). 323 * @param domainAxis the domain axis. 324 * @param rangeAxis the range axis. 325 * @param dataset the dataset (must be an instance of 326 * {@link BoxAndWhiskerXYDataset}). 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} permitted). 331 * @param pass the pass index. 332 */ 333 public void drawHorizontalItem(Graphics2D g2, Rectangle2D dataArea, 334 PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis, 335 ValueAxis rangeAxis, XYDataset dataset, int series, 336 int item, CrosshairState crosshairState, int pass) { 337 338 // setup for collecting optional entity info... 339 EntityCollection entities = null; 340 if (info != null) { 341 entities = info.getOwner().getEntityCollection(); 342 } 343 344 BoxAndWhiskerXYDataset boxAndWhiskerData 345 = (BoxAndWhiskerXYDataset) dataset; 346 347 Number x = boxAndWhiskerData.getX(series, item); 348 Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item); 349 Number yMin = boxAndWhiskerData.getMinRegularValue(series, item); 350 Number yMedian = boxAndWhiskerData.getMedianValue(series, item); 351 Number yAverage = boxAndWhiskerData.getMeanValue(series, item); 352 Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item); 353 Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item); 354 355 double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 356 plot.getDomainAxisEdge()); 357 358 RectangleEdge location = plot.getRangeAxisEdge(); 359 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 360 location); 361 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 362 location); 363 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 364 dataArea, location); 365 double yyAverage = 0.0; 366 if (yAverage != null) { 367 yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 368 dataArea, location); 369 } 370 double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 371 dataArea, location); 372 double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 373 dataArea, location); 374 375 double exactBoxWidth = getBoxWidth(); 376 double width = exactBoxWidth; 377 double dataAreaX = dataArea.getHeight(); 378 double maxBoxPercent = 0.1; 379 double maxBoxWidth = dataAreaX * maxBoxPercent; 380 if (exactBoxWidth <= 0.0) { 381 int itemCount = boxAndWhiskerData.getItemCount(series); 382 exactBoxWidth = dataAreaX / itemCount * 4.5 / 7; 383 if (exactBoxWidth < 3) { 384 width = 3; 385 } 386 else if (exactBoxWidth > maxBoxWidth) { 387 width = maxBoxWidth; 388 } 389 else { 390 width = exactBoxWidth; 391 } 392 } 393 394 g2.setPaint(getItemPaint(series, item)); 395 Stroke s = getItemStroke(series, item); 396 g2.setStroke(s); 397 398 // draw the upper shadow 399 g2.draw(new Line2D.Double(yyMax, xx, yyQ3Median, xx)); 400 g2.draw(new Line2D.Double(yyMax, xx - width / 2, yyMax, 401 xx + width / 2)); 402 403 // draw the lower shadow 404 g2.draw(new Line2D.Double(yyMin, xx, yyQ1Median, xx)); 405 g2.draw(new Line2D.Double(yyMin, xx - width / 2, yyMin, 406 xx + width / 2)); 407 408 // draw the body 409 Shape box; 410 if (yyQ1Median < yyQ3Median) { 411 box = new Rectangle2D.Double(yyQ1Median, xx - width / 2, 412 yyQ3Median - yyQ1Median, width); 413 } 414 else { 415 box = new Rectangle2D.Double(yyQ3Median, xx - width / 2, 416 yyQ1Median - yyQ3Median, width); 417 } 418 if (this.fillBox) { 419 g2.setPaint(lookupBoxPaint(series, item)); 420 g2.fill(box); 421 } 422 g2.setStroke(getItemOutlineStroke(series, item)); 423 g2.setPaint(getItemOutlinePaint(series, item)); 424 g2.draw(box); 425 426 // draw median 427 g2.setPaint(getArtifactPaint()); 428 g2.draw(new Line2D.Double(yyMedian, 429 xx - width / 2, yyMedian, xx + width / 2)); 430 431 // draw average - SPECIAL AIMS REQUIREMENT 432 if (yAverage != null) { 433 double aRadius = width / 4; 434 // here we check that the average marker will in fact be visible 435 // before drawing it... 436 if ((yyAverage > (dataArea.getMinX() - aRadius)) 437 && (yyAverage < (dataArea.getMaxX() + aRadius))) { 438 Ellipse2D.Double avgEllipse = new Ellipse2D.Double( 439 yyAverage - aRadius, xx - aRadius, aRadius * 2, 440 aRadius * 2); 441 g2.fill(avgEllipse); 442 g2.draw(avgEllipse); 443 } 444 } 445 446 // FIXME: draw outliers 447 448 // add an entity for the item... 449 if (entities != null && box.intersects(dataArea)) { 450 addEntity(entities, box, dataset, series, item, yyAverage, xx); 451 } 452 453 } 454 455 /** 456 * Draws the visual representation of a single data item. 457 * 458 * @param g2 the graphics device. 459 * @param dataArea the area within which the plot is being drawn. 460 * @param info collects info about the drawing. 461 * @param plot the plot (can be used to obtain standard color 462 * information etc). 463 * @param domainAxis the domain axis. 464 * @param rangeAxis the range axis. 465 * @param dataset the dataset (must be an instance of 466 * {@link BoxAndWhiskerXYDataset}). 467 * @param series the series index (zero-based). 468 * @param item the item index (zero-based). 469 * @param crosshairState crosshair information for the plot 470 * ({@code null} permitted). 471 * @param pass the pass index. 472 */ 473 public void drawVerticalItem(Graphics2D g2, Rectangle2D dataArea, 474 PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis, 475 ValueAxis rangeAxis, XYDataset dataset, int series, 476 int item, CrosshairState crosshairState, int pass) { 477 478 // setup for collecting optional entity info... 479 EntityCollection entities = null; 480 if (info != null) { 481 entities = info.getOwner().getEntityCollection(); 482 } 483 484 BoxAndWhiskerXYDataset boxAndWhiskerData 485 = (BoxAndWhiskerXYDataset) dataset; 486 487 Number x = boxAndWhiskerData.getX(series, item); 488 Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item); 489 Number yMin = boxAndWhiskerData.getMinRegularValue(series, item); 490 Number yMedian = boxAndWhiskerData.getMedianValue(series, item); 491 Number yAverage = boxAndWhiskerData.getMeanValue(series, item); 492 Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item); 493 Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item); 494 List yOutliers = boxAndWhiskerData.getOutliers(series, item); 495 // yOutliers can be null, but we'd prefer it to be an empty list in 496 // that case... 497 if (yOutliers == null) { 498 yOutliers = Collections.EMPTY_LIST; 499 } 500 501 double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 502 plot.getDomainAxisEdge()); 503 504 RectangleEdge location = plot.getRangeAxisEdge(); 505 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 506 location); 507 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 508 location); 509 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 510 dataArea, location); 511 double yyAverage = 0.0; 512 if (yAverage != null) { 513 yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 514 dataArea, location); 515 } 516 double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 517 dataArea, location); 518 double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 519 dataArea, location); 520 double yyOutlier; 521 522 double exactBoxWidth = getBoxWidth(); 523 double width = exactBoxWidth; 524 double dataAreaX = dataArea.getMaxX() - dataArea.getMinX(); 525 double maxBoxPercent = 0.1; 526 double maxBoxWidth = dataAreaX * maxBoxPercent; 527 if (exactBoxWidth <= 0.0) { 528 int itemCount = boxAndWhiskerData.getItemCount(series); 529 exactBoxWidth = dataAreaX / itemCount * 4.5 / 7; 530 if (exactBoxWidth < 3) { 531 width = 3; 532 } 533 else if (exactBoxWidth > maxBoxWidth) { 534 width = maxBoxWidth; 535 } 536 else { 537 width = exactBoxWidth; 538 } 539 } 540 541 g2.setPaint(getItemPaint(series, item)); 542 Stroke s = getItemStroke(series, item); 543 g2.setStroke(s); 544 545 // draw the upper shadow 546 g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median)); 547 g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2, 548 yyMax)); 549 550 // draw the lower shadow 551 g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median)); 552 g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2, 553 yyMin)); 554 555 // draw the body 556 Shape box; 557 if (yyQ1Median > yyQ3Median) { 558 box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width, 559 yyQ1Median - yyQ3Median); 560 } 561 else { 562 box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width, 563 yyQ3Median - yyQ1Median); 564 } 565 if (this.fillBox) { 566 g2.setPaint(lookupBoxPaint(series, item)); 567 g2.fill(box); 568 } 569 g2.setStroke(getItemOutlineStroke(series, item)); 570 g2.setPaint(getItemOutlinePaint(series, item)); 571 g2.draw(box); 572 573 // draw median 574 g2.setPaint(getArtifactPaint()); 575 g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2, 576 yyMedian)); 577 578 double aRadius = 0; // average radius 579 double oRadius = width / 3; // outlier radius 580 581 // draw average - SPECIAL AIMS REQUIREMENT 582 if (yAverage != null) { 583 aRadius = width / 4; 584 // here we check that the average marker will in fact be visible 585 // before drawing it... 586 if ((yyAverage > (dataArea.getMinY() - aRadius)) 587 && (yyAverage < (dataArea.getMaxY() + aRadius))) { 588 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius, 589 yyAverage - aRadius, aRadius * 2, aRadius * 2); 590 g2.fill(avgEllipse); 591 g2.draw(avgEllipse); 592 } 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 for (int i = 0; i < yOutliers.size(); i++) { 604 double outlier = ((Number) yOutliers.get(i)).doubleValue(); 605 if (outlier > boxAndWhiskerData.getMaxOutlier(series, 606 item).doubleValue()) { 607 outlierListCollection.setHighFarOut(true); 608 } 609 else if (outlier < boxAndWhiskerData.getMinOutlier(series, 610 item).doubleValue()) { 611 outlierListCollection.setLowFarOut(true); 612 } 613 else if (outlier > boxAndWhiskerData.getMaxRegularValue(series, 614 item).doubleValue()) { 615 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 616 location); 617 outliers.add(new Outlier(xx, yyOutlier, oRadius)); 618 } 619 else if (outlier < boxAndWhiskerData.getMinRegularValue(series, 620 item).doubleValue()) { 621 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 622 location); 623 outliers.add(new Outlier(xx, yyOutlier, oRadius)); 624 } 625 Collections.sort(outliers); 626 } 627 628 // Process outliers. Each outlier is either added to the appropriate 629 // outlier list or a new outlier list is made 630 for (Iterator iterator = outliers.iterator(); iterator.hasNext();) { 631 Outlier outlier = (Outlier) iterator.next(); 632 outlierListCollection.add(outlier); 633 } 634 635 // draw yOutliers 636 double maxAxisValue = rangeAxis.valueToJava2D(rangeAxis.getUpperBound(), 637 dataArea, location) + aRadius; 638 double minAxisValue = rangeAxis.valueToJava2D(rangeAxis.getLowerBound(), 639 dataArea, location) - aRadius; 640 641 // draw outliers 642 for (Iterator iterator = outlierListCollection.iterator(); 643 iterator.hasNext();) { 644 OutlierList list = (OutlierList) iterator.next(); 645 Outlier outlier = list.getAveragedOutlier(); 646 Point2D point = outlier.getPoint(); 647 648 if (list.isMultiple()) { 649 drawMultipleEllipse(point, width, oRadius, g2); 650 } 651 else { 652 drawEllipse(point, oRadius, g2); 653 } 654 } 655 656 // draw farout 657 if (outlierListCollection.isHighFarOut()) { 658 drawHighFarOut(aRadius, g2, xx, maxAxisValue); 659 } 660 661 if (outlierListCollection.isLowFarOut()) { 662 drawLowFarOut(aRadius, g2, xx, minAxisValue); 663 } 664 665 // add an entity for the item... 666 if (entities != null && box.intersects(dataArea)) { 667 addEntity(entities, box, dataset, series, item, xx, yyAverage); 668 } 669 670 } 671 672 /** 673 * Draws an ellipse to represent an outlier. 674 * 675 * @param point the location. 676 * @param oRadius the radius. 677 * @param g2 the graphics device. 678 */ 679 protected void drawEllipse(Point2D point, double oRadius, Graphics2D g2) { 680 Ellipse2D.Double dot = new Ellipse2D.Double(point.getX() + oRadius / 2, 681 point.getY(), oRadius, oRadius); 682 g2.draw(dot); 683 } 684 685 /** 686 * Draws two ellipses to represent overlapping outliers. 687 * 688 * @param point the location. 689 * @param boxWidth the box width. 690 * @param oRadius the radius. 691 * @param g2 the graphics device. 692 */ 693 protected void drawMultipleEllipse(Point2D point, double boxWidth, 694 double oRadius, Graphics2D g2) { 695 696 Ellipse2D.Double dot1 = new Ellipse2D.Double(point.getX() 697 - (boxWidth / 2) + oRadius, point.getY(), oRadius, oRadius); 698 Ellipse2D.Double dot2 = new Ellipse2D.Double(point.getX() 699 + (boxWidth / 2), point.getY(), oRadius, oRadius); 700 g2.draw(dot1); 701 g2.draw(dot2); 702 703 } 704 705 /** 706 * Draws a triangle to indicate the presence of far out values. 707 * 708 * @param aRadius the radius. 709 * @param g2 the graphics device. 710 * @param xx the x value. 711 * @param m the max y value. 712 */ 713 protected void drawHighFarOut(double aRadius, Graphics2D g2, double xx, 714 double m) { 715 double side = aRadius * 2; 716 g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side)); 717 g2.draw(new Line2D.Double(xx - side, m + side, xx, m)); 718 g2.draw(new Line2D.Double(xx + side, m + side, xx, m)); 719 } 720 721 /** 722 * Draws a triangle to indicate the presence of far out values. 723 * 724 * @param aRadius the radius. 725 * @param g2 the graphics device. 726 * @param xx the x value. 727 * @param m the min y value. 728 */ 729 protected void drawLowFarOut(double aRadius, Graphics2D g2, double xx, 730 double m) { 731 double side = aRadius * 2; 732 g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side)); 733 g2.draw(new Line2D.Double(xx - side, m - side, xx, m)); 734 g2.draw(new Line2D.Double(xx + side, m - side, xx, m)); 735 } 736 737 /** 738 * Tests this renderer for equality with another object. 739 * 740 * @param obj the object ({@code null} permitted). 741 * 742 * @return {@code true} or {@code false}. 743 */ 744 @Override 745 public boolean equals(Object obj) { 746 if (obj == this) { 747 return true; 748 } 749 if (!(obj instanceof XYBoxAndWhiskerRenderer)) { 750 return false; 751 } 752 if (!super.equals(obj)) { 753 return false; 754 } 755 XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj; 756 if (this.boxWidth != that.getBoxWidth()) { 757 return false; 758 } 759 if (!PaintUtils.equal(this.boxPaint, that.boxPaint)) { 760 return false; 761 } 762 if (!PaintUtils.equal(this.artifactPaint, that.artifactPaint)) { 763 return false; 764 } 765 if (this.fillBox != that.fillBox) { 766 return false; 767 } 768 return true; 769 770 } 771 772 /** 773 * Provides serialization support. 774 * 775 * @param stream the output stream. 776 * 777 * @throws IOException if there is an I/O error. 778 */ 779 private void writeObject(ObjectOutputStream stream) throws IOException { 780 stream.defaultWriteObject(); 781 SerialUtils.writePaint(this.boxPaint, stream); 782 SerialUtils.writePaint(this.artifactPaint, stream); 783 } 784 785 /** 786 * Provides serialization support. 787 * 788 * @param stream the input stream. 789 * 790 * @throws IOException if there is an I/O error. 791 * @throws ClassNotFoundException if there is a classpath problem. 792 */ 793 private void readObject(ObjectInputStream stream) 794 throws IOException, ClassNotFoundException { 795 796 stream.defaultReadObject(); 797 this.boxPaint = SerialUtils.readPaint(stream); 798 this.artifactPaint = SerialUtils.readPaint(stream); 799 } 800 801 /** 802 * Returns a clone of the renderer. 803 * 804 * @return A clone. 805 * 806 * @throws CloneNotSupportedException if the renderer cannot be cloned. 807 */ 808 @Override 809 public Object clone() throws CloneNotSupportedException { 810 return super.clone(); 811 } 812 813}