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 * BoxAndWhiskerRenderer.java 029 * -------------------------- 030 * (C) Copyright 2003-2020, by David Browning and Contributors. 031 * 032 * Original Author: David Browning (for the Australian Institute of Marine 033 * Science); 034 * Contributor(s): David Gilbert (for Object Refinery Limited); 035 * Tim Bardzil; 036 * Rob Van der Sanden (patches 1866446 and 1888422); 037 * Peter Becker (patches 2868585 and 2868608); 038 * Martin Krauskopf (patch 3421088); 039 * Martin Hoeller; 040 * John Matthews; 041 * 042 */ 043 044package org.jfree.chart.renderer.category; 045 046import java.awt.Color; 047import java.awt.Graphics2D; 048import java.awt.Paint; 049import java.awt.Shape; 050import java.awt.Stroke; 051import java.awt.geom.Ellipse2D; 052import java.awt.geom.Line2D; 053import java.awt.geom.Point2D; 054import java.awt.geom.Rectangle2D; 055import java.io.IOException; 056import java.io.ObjectInputStream; 057import java.io.ObjectOutputStream; 058import java.io.Serializable; 059import java.util.ArrayList; 060import java.util.Collections; 061import java.util.Iterator; 062import java.util.List; 063 064import org.jfree.chart.LegendItem; 065import org.jfree.chart.axis.CategoryAxis; 066import org.jfree.chart.axis.ValueAxis; 067import org.jfree.chart.entity.EntityCollection; 068import org.jfree.chart.event.RendererChangeEvent; 069import org.jfree.chart.plot.CategoryPlot; 070import org.jfree.chart.plot.PlotOrientation; 071import org.jfree.chart.plot.PlotRenderingInfo; 072import org.jfree.chart.renderer.Outlier; 073import org.jfree.chart.renderer.OutlierList; 074import org.jfree.chart.renderer.OutlierListCollection; 075import org.jfree.chart.ui.RectangleEdge; 076import org.jfree.chart.util.PaintUtils; 077import org.jfree.chart.util.Args; 078import org.jfree.chart.util.PublicCloneable; 079import org.jfree.chart.util.SerialUtils; 080import org.jfree.data.Range; 081import org.jfree.data.category.CategoryDataset; 082import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset; 083 084/** 085 * A box-and-whisker renderer. This renderer requires a 086 * {@link BoxAndWhiskerCategoryDataset} and is for use with the 087 * {@link CategoryPlot} class. The example shown here is generated 088 * by the {@code BoxAndWhiskerChartDemo1.java} program included in the 089 * JFreeChart Demo Collection: 090 * <br><br> 091 * <img src="doc-files/BoxAndWhiskerRendererSample.png" 092 * alt="BoxAndWhiskerRendererSample.png"> 093 */ 094public class BoxAndWhiskerRenderer extends AbstractCategoryItemRenderer 095 implements Cloneable, PublicCloneable, Serializable { 096 097 /** For serialization. */ 098 private static final long serialVersionUID = 632027470694481177L; 099 100 /** The color used to paint the median line and average marker. */ 101 private transient Paint artifactPaint; 102 103 /** A flag that controls whether or not the box is filled. */ 104 private boolean fillBox; 105 106 /** The margin between items (boxes) within a category. */ 107 private double itemMargin; 108 109 /** 110 * The maximum bar width as percentage of the available space in the plot. 111 * Take care with the encoding - for example, 0.05 is five percent. 112 */ 113 private double maximumBarWidth; 114 115 /** 116 * A flag that controls whether or not the median indicator is drawn. 117 * 118 * @since 1.0.13 119 */ 120 private boolean medianVisible; 121 122 /** 123 * A flag that controls whether or not the mean indicator is drawn. 124 * 125 * @since 1.0.13 126 */ 127 private boolean meanVisible; 128 129 /** 130 * A flag that controls whether or not the maxOutlier is visible. 131 */ 132 private boolean maxOutlierVisible; 133 134 /** 135 * A flag that controls whether or not the minOutlier is visible. 136 */ 137 private boolean minOutlierVisible; 138 139 /** 140 * A flag that, if {@code true}, causes the whiskers to be drawn 141 * using the outline paint for the series. The default value is 142 * {@code false} and in that case the regular series paint is used. 143 * 144 * @since 1.0.14 145 */ 146 private boolean useOutlinePaintForWhiskers; 147 148 /** 149 * The width of the whiskers as fraction of the bar width. 150 * 151 * @since 1.0.14 152 */ 153 private double whiskerWidth; 154 155 /** 156 * Default constructor. 157 */ 158 public BoxAndWhiskerRenderer() { 159 this.artifactPaint = Color.BLACK; 160 this.fillBox = true; 161 this.itemMargin = 0.20; 162 this.maximumBarWidth = 1.0; 163 this.medianVisible = true; 164 this.meanVisible = true; 165 this.minOutlierVisible = true; 166 this.maxOutlierVisible = true; 167 this.useOutlinePaintForWhiskers = false; 168 this.whiskerWidth = 1.0; 169 setDefaultLegendShape(new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0)); 170 } 171 172 /** 173 * Returns the paint used to color the median and average markers. 174 * 175 * @return The paint used to draw the median and average markers (never 176 * {@code null}). 177 * 178 * @see #setArtifactPaint(Paint) 179 */ 180 public Paint getArtifactPaint() { 181 return this.artifactPaint; 182 } 183 184 /** 185 * Sets the paint used to color the median and average markers and sends 186 * a {@link RendererChangeEvent} to all registered listeners. 187 * 188 * @param paint the paint ({@code null} not permitted). 189 * 190 * @see #getArtifactPaint() 191 */ 192 public void setArtifactPaint(Paint paint) { 193 Args.nullNotPermitted(paint, "paint"); 194 this.artifactPaint = paint; 195 fireChangeEvent(); 196 } 197 198 /** 199 * Returns the flag that controls whether or not the box is filled. 200 * 201 * @return A boolean. 202 * 203 * @see #setFillBox(boolean) 204 */ 205 public boolean getFillBox() { 206 return this.fillBox; 207 } 208 209 /** 210 * Sets the flag that controls whether or not the box is filled and sends a 211 * {@link RendererChangeEvent} to all registered listeners. 212 * 213 * @param flag the flag. 214 * 215 * @see #getFillBox() 216 */ 217 public void setFillBox(boolean flag) { 218 this.fillBox = flag; 219 fireChangeEvent(); 220 } 221 222 /** 223 * Returns the item margin. This is a percentage of the available space 224 * that is allocated to the space between items in the chart. 225 * 226 * @return The margin. 227 * 228 * @see #setItemMargin(double) 229 */ 230 public double getItemMargin() { 231 return this.itemMargin; 232 } 233 234 /** 235 * Sets the item margin and sends a {@link RendererChangeEvent} to all 236 * registered listeners. 237 * 238 * @param margin the margin (a percentage). 239 * 240 * @see #getItemMargin() 241 */ 242 public void setItemMargin(double margin) { 243 this.itemMargin = margin; 244 fireChangeEvent(); 245 } 246 247 /** 248 * Returns the maximum bar width as a percentage of the available drawing 249 * space. Take care with the encoding, for example 0.10 is ten percent. 250 * 251 * @return The maximum bar width. 252 * 253 * @see #setMaximumBarWidth(double) 254 * 255 * @since 1.0.10 256 */ 257 public double getMaximumBarWidth() { 258 return this.maximumBarWidth; 259 } 260 261 /** 262 * Sets the maximum bar width, which is specified as a percentage of the 263 * available space for all bars, and sends a {@link RendererChangeEvent} 264 * to all registered listeners. 265 * 266 * @param percent the maximum bar width (a percentage, where 0.10 is ten 267 * percent). 268 * 269 * @see #getMaximumBarWidth() 270 * 271 * @since 1.0.10 272 */ 273 public void setMaximumBarWidth(double percent) { 274 this.maximumBarWidth = percent; 275 fireChangeEvent(); 276 } 277 278 /** 279 * Returns the flag that controls whether or not the mean indicator is 280 * draw for each item. 281 * 282 * @return A boolean. 283 * 284 * @see #setMeanVisible(boolean) 285 * 286 * @since 1.0.13 287 */ 288 public boolean isMeanVisible() { 289 return this.meanVisible; 290 } 291 292 /** 293 * Sets the flag that controls whether or not the mean indicator is drawn 294 * for each item, and sends a {@link RendererChangeEvent} to all 295 * registered listeners. 296 * 297 * @param visible the new flag value. 298 * 299 * @see #isMeanVisible() 300 * 301 * @since 1.0.13 302 */ 303 public void setMeanVisible(boolean visible) { 304 if (this.meanVisible == visible) { 305 return; 306 } 307 this.meanVisible = visible; 308 fireChangeEvent(); 309 } 310 311 /** 312 * Returns the flag that controls whether or not the median indicator is 313 * draw for each item. 314 * 315 * @return A boolean. 316 * 317 * @see #setMedianVisible(boolean) 318 * 319 * @since 1.0.13 320 */ 321 public boolean isMedianVisible() { 322 return this.medianVisible; 323 } 324 325 /** 326 * Sets the flag that controls whether or not the median indicator is drawn 327 * for each item, and sends a {@link RendererChangeEvent} to all 328 * registered listeners. 329 * 330 * @param visible the new flag value. 331 * 332 * @see #isMedianVisible() 333 * 334 * @since 1.0.13 335 */ 336 public void setMedianVisible(boolean visible) { 337 if (this.medianVisible == visible) { 338 return; 339 } 340 this.medianVisible = visible; 341 fireChangeEvent(); 342 } 343 344 /** 345 * Returns the flag that controls whether or not the minimum outlier is 346 * draw for each item. 347 * 348 * @return A boolean. 349 * 350 * @see #setMinOutlierVisible(boolean) 351 * 352 * @since 1.5.2 353 */ 354 public boolean isMinOutlierVisible() { 355 return this.minOutlierVisible; 356 } 357 358 /** 359 * Sets the flag that controls whether or not the minimum outlier is drawn 360 * for each item, and sends a {@link RendererChangeEvent} to all 361 * registered listeners. 362 * 363 * @param visible the new flag value. 364 * 365 * @see #isMinOutlierVisible() 366 * 367 * @since 1.5.2 368 */ 369 public void setMinOutlierVisible(boolean visible) { 370 if (this.minOutlierVisible == visible) { 371 return; 372 } 373 this.minOutlierVisible = visible; 374 fireChangeEvent(); 375 } 376 377 /** 378 * Returns the flag that controls whether or not the maximum outlier is 379 * draw for each item. 380 * 381 * @return A boolean. 382 * 383 * @see #setMaxOutlierVisible(boolean) 384 * 385 * @since 1.5.2 386 */ 387 public boolean isMaxOutlierVisible() { 388 return this.maxOutlierVisible; 389 } 390 391 /** 392 * Sets the flag that controls whether or not the maximum outlier is drawn 393 * for each item, and sends a {@link RendererChangeEvent} to all 394 * registered listeners. 395 * 396 * @param visible the new flag value. 397 * 398 * @see #isMaxOutlierVisible() 399 * 400 * @since 1.5.2 401 */ 402 public void setMaxOutlierVisible(boolean visible) { 403 if (this.maxOutlierVisible == visible) { 404 return; 405 } 406 this.maxOutlierVisible = visible; 407 fireChangeEvent(); 408 } 409 410 /** 411 * Returns the flag that, if {@code true}, causes the whiskers to 412 * be drawn using the series outline paint. 413 * 414 * @return A boolean. 415 * 416 * @since 1.0.14 417 */ 418 public boolean getUseOutlinePaintForWhiskers() { 419 return this.useOutlinePaintForWhiskers; 420 } 421 422 /** 423 * Sets the flag that, if {@code true}, causes the whiskers to 424 * be drawn using the series outline paint, and sends a 425 * {@link RendererChangeEvent} to all registered listeners. 426 * 427 * @param flag the new flag value. 428 * 429 * @since 1.0.14 430 */ 431 public void setUseOutlinePaintForWhiskers(boolean flag) { 432 if (this.useOutlinePaintForWhiskers == flag) { 433 return; 434 } 435 this.useOutlinePaintForWhiskers = flag; 436 fireChangeEvent(); 437 } 438 439 /** 440 * Returns the width of the whiskers as fraction of the bar width. 441 * 442 * @return The width of the whiskers. 443 * 444 * @see #setWhiskerWidth(double) 445 * 446 * @since 1.0.14 447 */ 448 public double getWhiskerWidth() { 449 return this.whiskerWidth; 450 } 451 452 /** 453 * Sets the width of the whiskers as a fraction of the bar width and sends 454 * a {@link RendererChangeEvent} to all registered listeners. 455 * 456 * @param width a value between 0 and 1 indicating how wide the 457 * whisker is supposed to be compared to the bar. 458 * @see #getWhiskerWidth() 459 * @see CategoryItemRendererState#getBarWidth() 460 * 461 * @since 1.0.14 462 */ 463 public void setWhiskerWidth(double width) { 464 if (width < 0 || width > 1) { 465 throw new IllegalArgumentException( 466 "Value for whisker width out of range"); 467 } 468 if (width == this.whiskerWidth) { 469 return; 470 } 471 this.whiskerWidth = width; 472 fireChangeEvent(); 473 } 474 475 /** 476 * Returns a legend item for a series. 477 * 478 * @param datasetIndex the dataset index (zero-based). 479 * @param series the series index (zero-based). 480 * 481 * @return The legend item (possibly {@code null}). 482 */ 483 @Override 484 public LegendItem getLegendItem(int datasetIndex, int series) { 485 486 CategoryPlot cp = getPlot(); 487 if (cp == null) { 488 return null; 489 } 490 491 // check that a legend item needs to be displayed... 492 if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) { 493 return null; 494 } 495 496 CategoryDataset dataset = cp.getDataset(datasetIndex); 497 String label = getLegendItemLabelGenerator().generateLabel(dataset, 498 series); 499 String description = label; 500 String toolTipText = null; 501 if (getLegendItemToolTipGenerator() != null) { 502 toolTipText = getLegendItemToolTipGenerator().generateLabel( 503 dataset, series); 504 } 505 String urlText = null; 506 if (getLegendItemURLGenerator() != null) { 507 urlText = getLegendItemURLGenerator().generateLabel(dataset, 508 series); 509 } 510 Shape shape = lookupLegendShape(series); 511 Paint paint = lookupSeriesPaint(series); 512 Paint outlinePaint = lookupSeriesOutlinePaint(series); 513 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 514 LegendItem result = new LegendItem(label, description, toolTipText, 515 urlText, shape, paint, outlineStroke, outlinePaint); 516 result.setLabelFont(lookupLegendTextFont(series)); 517 Paint labelPaint = lookupLegendTextPaint(series); 518 if (labelPaint != null) { 519 result.setLabelPaint(labelPaint); 520 } 521 result.setDataset(dataset); 522 result.setDatasetIndex(datasetIndex); 523 result.setSeriesKey(dataset.getRowKey(series)); 524 result.setSeriesIndex(series); 525 return result; 526 527 } 528 529 /** 530 * Returns the range of values from the specified dataset that the 531 * renderer will require to display all the data. 532 * 533 * @param dataset the dataset. 534 * 535 * @return The range. 536 */ 537 @Override 538 public Range findRangeBounds(CategoryDataset dataset) { 539 return super.findRangeBounds(dataset, true); 540 } 541 542 /** 543 * Initialises the renderer. This method gets called once at the start of 544 * the process of drawing a chart. 545 * 546 * @param g2 the graphics device. 547 * @param dataArea the area in which the data is to be plotted. 548 * @param plot the plot. 549 * @param rendererIndex the renderer index. 550 * @param info collects chart rendering information for return to caller. 551 * 552 * @return The renderer state. 553 */ 554 @Override 555 public CategoryItemRendererState initialise(Graphics2D g2, 556 Rectangle2D dataArea, CategoryPlot plot, int rendererIndex, 557 PlotRenderingInfo info) { 558 559 CategoryItemRendererState state = super.initialise(g2, dataArea, plot, 560 rendererIndex, info); 561 // calculate the box width 562 CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex); 563 CategoryDataset dataset = plot.getDataset(rendererIndex); 564 if (dataset != null) { 565 int columns = dataset.getColumnCount(); 566 int rows = dataset.getRowCount(); 567 double space = 0.0; 568 PlotOrientation orientation = plot.getOrientation(); 569 if (orientation == PlotOrientation.HORIZONTAL) { 570 space = dataArea.getHeight(); 571 } 572 else if (orientation == PlotOrientation.VERTICAL) { 573 space = dataArea.getWidth(); 574 } 575 double maxWidth = space * getMaximumBarWidth(); 576 double categoryMargin = 0.0; 577 double currentItemMargin = 0.0; 578 if (columns > 1) { 579 categoryMargin = domainAxis.getCategoryMargin(); 580 } 581 if (rows > 1) { 582 currentItemMargin = getItemMargin(); 583 } 584 double used = space * (1 - domainAxis.getLowerMargin() 585 - domainAxis.getUpperMargin() 586 - categoryMargin - currentItemMargin); 587 if ((rows * columns) > 0) { 588 state.setBarWidth(Math.min(used / (dataset.getColumnCount() 589 * dataset.getRowCount()), maxWidth)); 590 } else { 591 state.setBarWidth(Math.min(used, maxWidth)); 592 } 593 } 594 return state; 595 596 } 597 598 /** 599 * Draw a single data item. 600 * 601 * @param g2 the graphics device. 602 * @param state the renderer state. 603 * @param dataArea the area in which the data is drawn. 604 * @param plot the plot. 605 * @param domainAxis the domain axis. 606 * @param rangeAxis the range axis. 607 * @param dataset the data (must be an instance of 608 * {@link BoxAndWhiskerCategoryDataset}). 609 * @param row the row index (zero-based). 610 * @param column the column index (zero-based). 611 * @param pass the pass index. 612 */ 613 @Override 614 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 615 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 616 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 617 int pass) { 618 619 // do nothing if item is not visible 620 if (!getItemVisible(row, column)) { 621 return; 622 } 623 624 if (!(dataset instanceof BoxAndWhiskerCategoryDataset)) { 625 throw new IllegalArgumentException( 626 "BoxAndWhiskerRenderer.drawItem() : the data should be " 627 + "of type BoxAndWhiskerCategoryDataset only."); 628 } 629 630 PlotOrientation orientation = plot.getOrientation(); 631 632 if (orientation == PlotOrientation.HORIZONTAL) { 633 drawHorizontalItem(g2, state, dataArea, plot, domainAxis, 634 rangeAxis, dataset, row, column); 635 } else if (orientation == PlotOrientation.VERTICAL) { 636 drawVerticalItem(g2, state, dataArea, plot, domainAxis, 637 rangeAxis, dataset, row, column); 638 } 639 640 } 641 642 /** 643 * Draws the visual representation of a single data item when the plot has 644 * a horizontal orientation. 645 * 646 * @param g2 the graphics device. 647 * @param state the renderer state. 648 * @param dataArea the area within which the plot is being drawn. 649 * @param plot the plot (can be used to obtain standard color 650 * information etc). 651 * @param domainAxis the domain axis. 652 * @param rangeAxis the range axis. 653 * @param dataset the dataset (must be an instance of 654 * {@link BoxAndWhiskerCategoryDataset}). 655 * @param row the row index (zero-based). 656 * @param column the column index (zero-based). 657 */ 658 public void drawHorizontalItem(Graphics2D g2, 659 CategoryItemRendererState state, Rectangle2D dataArea, 660 CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis, 661 CategoryDataset dataset, int row, int column) { 662 663 BoxAndWhiskerCategoryDataset bawDataset 664 = (BoxAndWhiskerCategoryDataset) dataset; 665 666 double categoryEnd = domainAxis.getCategoryEnd(column, 667 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 668 double categoryStart = domainAxis.getCategoryStart(column, 669 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 670 double categoryWidth = Math.abs(categoryEnd - categoryStart); 671 672 double yy = categoryStart; 673 int seriesCount = getRowCount(); 674 int categoryCount = getColumnCount(); 675 676 if (seriesCount > 1) { 677 double seriesGap = dataArea.getHeight() * getItemMargin() 678 / (categoryCount * (seriesCount - 1)); 679 double usedWidth = (state.getBarWidth() * seriesCount) 680 + (seriesGap * (seriesCount - 1)); 681 // offset the start of the boxes if the total width used is smaller 682 // than the category width 683 double offset = (categoryWidth - usedWidth) / 2; 684 yy = yy + offset + (row * (state.getBarWidth() + seriesGap)); 685 } else { 686 // offset the start of the box if the box width is smaller than 687 // the category width 688 double offset = (categoryWidth - state.getBarWidth()) / 2; 689 yy = yy + offset; 690 } 691 692 g2.setPaint(getItemPaint(row, column)); 693 Stroke s = getItemStroke(row, column); 694 g2.setStroke(s); 695 696 RectangleEdge location = plot.getRangeAxisEdge(); 697 698 Number xQ1 = bawDataset.getQ1Value(row, column); 699 Number xQ3 = bawDataset.getQ3Value(row, column); 700 Number xMax = bawDataset.getMaxRegularValue(row, column); 701 Number xMin = bawDataset.getMinRegularValue(row, column); 702 703 Shape box = null; 704 if (xQ1 != null && xQ3 != null && xMax != null && xMin != null) { 705 706 double xxQ1 = rangeAxis.valueToJava2D(xQ1.doubleValue(), dataArea, 707 location); 708 double xxQ3 = rangeAxis.valueToJava2D(xQ3.doubleValue(), dataArea, 709 location); 710 double xxMax = rangeAxis.valueToJava2D(xMax.doubleValue(), dataArea, 711 location); 712 double xxMin = rangeAxis.valueToJava2D(xMin.doubleValue(), dataArea, 713 location); 714 double yymid = yy + state.getBarWidth() / 2.0; 715 double halfW = (state.getBarWidth() / 2.0) * this.whiskerWidth; 716 717 // draw the box... 718 box = new Rectangle2D.Double(Math.min(xxQ1, xxQ3), yy, 719 Math.abs(xxQ1 - xxQ3), state.getBarWidth()); 720 if (this.fillBox) { 721 g2.fill(box); 722 } 723 724 Paint outlinePaint = getItemOutlinePaint(row, column); 725 if (this.useOutlinePaintForWhiskers) { 726 g2.setPaint(outlinePaint); 727 } 728 // draw the upper shadow... 729 g2.draw(new Line2D.Double(xxMax, yymid, xxQ3, yymid)); 730 g2.draw(new Line2D.Double(xxMax, yymid - halfW, xxMax, 731 yymid + halfW)); 732 733 // draw the lower shadow... 734 g2.draw(new Line2D.Double(xxMin, yymid, xxQ1, yymid)); 735 g2.draw(new Line2D.Double(xxMin, yymid - halfW, xxMin, 736 yymid + halfW)); 737 738 g2.setStroke(getItemOutlineStroke(row, column)); 739 g2.setPaint(outlinePaint); 740 g2.draw(box); 741 } 742 743 // draw mean - SPECIAL AIMS REQUIREMENT... 744 g2.setPaint(this.artifactPaint); 745 double aRadius; // average radius 746 if (this.meanVisible) { 747 Number xMean = bawDataset.getMeanValue(row, column); 748 if (xMean != null) { 749 double xxMean = rangeAxis.valueToJava2D(xMean.doubleValue(), 750 dataArea, location); 751 aRadius = state.getBarWidth() / 4; 752 // here we check that the average marker will in fact be 753 // visible before drawing it... 754 if ((xxMean > (dataArea.getMinX() - aRadius)) 755 && (xxMean < (dataArea.getMaxX() + aRadius))) { 756 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xxMean 757 - aRadius, yy + aRadius, aRadius * 2, aRadius * 2); 758 g2.fill(avgEllipse); 759 g2.draw(avgEllipse); 760 } 761 } 762 } 763 764 // draw median... 765 if (this.medianVisible) { 766 Number xMedian = bawDataset.getMedianValue(row, column); 767 if (xMedian != null) { 768 double xxMedian = rangeAxis.valueToJava2D(xMedian.doubleValue(), 769 dataArea, location); 770 g2.draw(new Line2D.Double(xxMedian, yy, xxMedian, 771 yy + state.getBarWidth())); 772 } 773 } 774 775 // collect entity and tool tip information... 776 if (state.getInfo() != null && box != null) { 777 EntityCollection entities = state.getEntityCollection(); 778 if (entities != null) { 779 addItemEntity(entities, dataset, row, column, box); 780 } 781 } 782 783 } 784 785 /** 786 * Draws the visual representation of a single data item when the plot has 787 * a vertical orientation. 788 * 789 * @param g2 the graphics device. 790 * @param state the renderer state. 791 * @param dataArea the area within which the plot is being drawn. 792 * @param plot the plot (can be used to obtain standard color information 793 * etc). 794 * @param domainAxis the domain axis. 795 * @param rangeAxis the range axis. 796 * @param dataset the dataset (must be an instance of 797 * {@link BoxAndWhiskerCategoryDataset}). 798 * @param row the row index (zero-based). 799 * @param column the column index (zero-based). 800 */ 801 public void drawVerticalItem(Graphics2D g2, CategoryItemRendererState state, 802 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 803 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column) { 804 805 BoxAndWhiskerCategoryDataset bawDataset 806 = (BoxAndWhiskerCategoryDataset) dataset; 807 808 double categoryEnd = domainAxis.getCategoryEnd(column, 809 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 810 double categoryStart = domainAxis.getCategoryStart(column, 811 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 812 double categoryWidth = categoryEnd - categoryStart; 813 814 double xx = categoryStart; 815 int seriesCount = getRowCount(); 816 int categoryCount = getColumnCount(); 817 818 if (seriesCount > 1) { 819 double seriesGap = dataArea.getWidth() * getItemMargin() 820 / (categoryCount * (seriesCount - 1)); 821 double usedWidth = (state.getBarWidth() * seriesCount) 822 + (seriesGap * (seriesCount - 1)); 823 // offset the start of the boxes if the total width used is smaller 824 // than the category width 825 double offset = (categoryWidth - usedWidth) / 2; 826 xx = xx + offset + (row * (state.getBarWidth() + seriesGap)); 827 } 828 else { 829 // offset the start of the box if the box width is smaller than the 830 // category width 831 double offset = (categoryWidth - state.getBarWidth()) / 2; 832 xx = xx + offset; 833 } 834 835 double yyAverage; 836 double yyOutlier; 837 838 Paint itemPaint = getItemPaint(row, column); 839 g2.setPaint(itemPaint); 840 Stroke s = getItemStroke(row, column); 841 g2.setStroke(s); 842 843 double aRadius = 0; // average radius 844 845 RectangleEdge location = plot.getRangeAxisEdge(); 846 847 Number yQ1 = bawDataset.getQ1Value(row, column); 848 Number yQ3 = bawDataset.getQ3Value(row, column); 849 Number yMax = bawDataset.getMaxRegularValue(row, column); 850 Number yMin = bawDataset.getMinRegularValue(row, column); 851 Shape box = null; 852 if (yQ1 != null && yQ3 != null && yMax != null && yMin != null) { 853 854 double yyQ1 = rangeAxis.valueToJava2D(yQ1.doubleValue(), dataArea, 855 location); 856 double yyQ3 = rangeAxis.valueToJava2D(yQ3.doubleValue(), dataArea, 857 location); 858 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), 859 dataArea, location); 860 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), 861 dataArea, location); 862 double xxmid = xx + state.getBarWidth() / 2.0; 863 double halfW = (state.getBarWidth() / 2.0) * this.whiskerWidth; 864 865 // draw the body... 866 box = new Rectangle2D.Double(xx, Math.min(yyQ1, yyQ3), 867 state.getBarWidth(), Math.abs(yyQ1 - yyQ3)); 868 if (this.fillBox) { 869 g2.fill(box); 870 } 871 872 Paint outlinePaint = getItemOutlinePaint(row, column); 873 if (this.useOutlinePaintForWhiskers) { 874 g2.setPaint(outlinePaint); 875 } 876 // draw the upper shadow... 877 g2.draw(new Line2D.Double(xxmid, yyMax, xxmid, yyQ3)); 878 g2.draw(new Line2D.Double(xxmid - halfW, yyMax, xxmid + halfW, yyMax)); 879 880 // draw the lower shadow... 881 g2.draw(new Line2D.Double(xxmid, yyMin, xxmid, yyQ1)); 882 g2.draw(new Line2D.Double(xxmid - halfW, yyMin, xxmid + halfW, yyMin)); 883 884 g2.setStroke(getItemOutlineStroke(row, column)); 885 g2.setPaint(outlinePaint); 886 g2.draw(box); 887 } 888 889 g2.setPaint(this.artifactPaint); 890 891 // draw mean - SPECIAL AIMS REQUIREMENT... 892 if (this.meanVisible) { 893 Number yMean = bawDataset.getMeanValue(row, column); 894 if (yMean != null) { 895 yyAverage = rangeAxis.valueToJava2D(yMean.doubleValue(), 896 dataArea, location); 897 aRadius = state.getBarWidth() / 4; 898 // here we check that the average marker will in fact be 899 // visible before drawing it... 900 if ((yyAverage > (dataArea.getMinY() - aRadius)) 901 && (yyAverage < (dataArea.getMaxY() + aRadius))) { 902 Ellipse2D.Double avgEllipse = new Ellipse2D.Double( 903 xx + aRadius, yyAverage - aRadius, aRadius * 2, 904 aRadius * 2); 905 g2.fill(avgEllipse); 906 g2.draw(avgEllipse); 907 } 908 } 909 } 910 911 // draw median... 912 if (this.medianVisible) { 913 Number yMedian = bawDataset.getMedianValue(row, column); 914 if (yMedian != null) { 915 double yyMedian = rangeAxis.valueToJava2D( 916 yMedian.doubleValue(), dataArea, location); 917 g2.draw(new Line2D.Double(xx, yyMedian, 918 xx + state.getBarWidth(), yyMedian)); 919 } 920 } 921 922 // draw yOutliers... 923 double maxAxisValue = rangeAxis.valueToJava2D( 924 rangeAxis.getUpperBound(), dataArea, location) + aRadius; 925 double minAxisValue = rangeAxis.valueToJava2D( 926 rangeAxis.getLowerBound(), dataArea, location) - aRadius; 927 928 g2.setPaint(itemPaint); 929 930 // draw outliers 931 double oRadius = state.getBarWidth() / 3; // outlier radius 932 List outliers = new ArrayList(); 933 OutlierListCollection outlierListCollection 934 = new OutlierListCollection(); 935 936 // From outlier array sort out which are outliers and put these into a 937 // list If there are any farouts, set the flag on the 938 // OutlierListCollection 939 List yOutliers = bawDataset.getOutliers(row, column); 940 if (yOutliers != null) { 941 for (int i = 0; i < yOutliers.size(); i++) { 942 double outlier = ((Number) yOutliers.get(i)).doubleValue(); 943 Number minOutlier = bawDataset.getMinOutlier(row, column); 944 Number maxOutlier = bawDataset.getMaxOutlier(row, column); 945 Number minRegular = bawDataset.getMinRegularValue(row, column); 946 Number maxRegular = bawDataset.getMaxRegularValue(row, column); 947 if (outlier > maxOutlier.doubleValue()) { 948 outlierListCollection.setHighFarOut(true); 949 } else if (outlier < minOutlier.doubleValue()) { 950 outlierListCollection.setLowFarOut(true); 951 } else if (outlier > maxRegular.doubleValue()) { 952 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 953 location); 954 outliers.add(new Outlier(xx + state.getBarWidth() / 2.0, 955 yyOutlier, oRadius)); 956 } else if (outlier < minRegular.doubleValue()) { 957 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 958 location); 959 outliers.add(new Outlier(xx + state.getBarWidth() / 2.0, 960 yyOutlier, oRadius)); 961 } 962 Collections.sort(outliers); 963 } 964 965 // Process outliers. Each outlier is either added to the 966 // appropriate outlier list or a new outlier list is made 967 for (Iterator iterator = outliers.iterator(); iterator.hasNext();) { 968 Outlier outlier = (Outlier) iterator.next(); 969 outlierListCollection.add(outlier); 970 } 971 972 for (Iterator iterator = outlierListCollection.iterator(); 973 iterator.hasNext();) { 974 OutlierList list = (OutlierList) iterator.next(); 975 Outlier outlier = list.getAveragedOutlier(); 976 Point2D point = outlier.getPoint(); 977 978 if (list.isMultiple()) { 979 drawMultipleEllipse(point, state.getBarWidth(), oRadius, 980 g2); 981 } else { 982 drawEllipse(point, oRadius, g2); 983 } 984 } 985 986 // draw farout indicators 987 if (isMaxOutlierVisible() && outlierListCollection.isHighFarOut()) { 988 drawHighFarOut(aRadius / 2.0, g2, 989 xx + state.getBarWidth() / 2.0, maxAxisValue); 990 } 991 992 if (isMinOutlierVisible() && outlierListCollection.isLowFarOut()) { 993 drawLowFarOut(aRadius / 2.0, g2, 994 xx + state.getBarWidth() / 2.0, minAxisValue); 995 } 996 } 997 // collect entity and tool tip information... 998 if (state.getInfo() != null && box != null) { 999 EntityCollection entities = state.getEntityCollection(); 1000 if (entities != null) { 1001 addItemEntity(entities, dataset, row, column, box); 1002 } 1003 } 1004 1005 } 1006 1007 /** 1008 * Draws a dot to represent an outlier. 1009 * 1010 * @param point the location. 1011 * @param oRadius the radius. 1012 * @param g2 the graphics device. 1013 */ 1014 private void drawEllipse(Point2D point, double oRadius, Graphics2D g2) { 1015 Ellipse2D dot = new Ellipse2D.Double(point.getX() + oRadius / 2, 1016 point.getY(), oRadius, oRadius); 1017 g2.draw(dot); 1018 } 1019 1020 /** 1021 * Draws two dots to represent the average value of more than one outlier. 1022 * 1023 * @param point the location 1024 * @param boxWidth the box width. 1025 * @param oRadius the radius. 1026 * @param g2 the graphics device. 1027 */ 1028 private void drawMultipleEllipse(Point2D point, double boxWidth, 1029 double oRadius, Graphics2D g2) { 1030 1031 Ellipse2D dot1 = new Ellipse2D.Double(point.getX() - (boxWidth / 2) 1032 + oRadius, point.getY(), oRadius, oRadius); 1033 Ellipse2D dot2 = new Ellipse2D.Double(point.getX() + (boxWidth / 2), 1034 point.getY(), oRadius, oRadius); 1035 g2.draw(dot1); 1036 g2.draw(dot2); 1037 } 1038 1039 /** 1040 * Draws a triangle to indicate the presence of far-out values. 1041 * 1042 * @param aRadius the radius. 1043 * @param g2 the graphics device. 1044 * @param xx the x coordinate. 1045 * @param m the y coordinate. 1046 */ 1047 private void drawHighFarOut(double aRadius, Graphics2D g2, double xx, 1048 double m) { 1049 double side = aRadius * 2; 1050 g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side)); 1051 g2.draw(new Line2D.Double(xx - side, m + side, xx, m)); 1052 g2.draw(new Line2D.Double(xx + side, m + side, xx, m)); 1053 } 1054 1055 /** 1056 * Draws a triangle to indicate the presence of far-out values. 1057 * 1058 * @param aRadius the radius. 1059 * @param g2 the graphics device. 1060 * @param xx the x coordinate. 1061 * @param m the y coordinate. 1062 */ 1063 private void drawLowFarOut(double aRadius, Graphics2D g2, double xx, 1064 double m) { 1065 double side = aRadius * 2; 1066 g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side)); 1067 g2.draw(new Line2D.Double(xx - side, m - side, xx, m)); 1068 g2.draw(new Line2D.Double(xx + side, m - side, xx, m)); 1069 } 1070 1071 /** 1072 * Tests this renderer for equality with an arbitrary object. 1073 * 1074 * @param obj the object ({@code null} permitted). 1075 * 1076 * @return {@code true} or {@code false}. 1077 */ 1078 @Override 1079 public boolean equals(Object obj) { 1080 if (obj == this) { 1081 return true; 1082 } 1083 if (!(obj instanceof BoxAndWhiskerRenderer)) { 1084 return false; 1085 } 1086 BoxAndWhiskerRenderer that = (BoxAndWhiskerRenderer) obj; 1087 if (this.fillBox != that.fillBox) { 1088 return false; 1089 } 1090 if (this.itemMargin != that.itemMargin) { 1091 return false; 1092 } 1093 if (this.maximumBarWidth != that.maximumBarWidth) { 1094 return false; 1095 } 1096 if (this.meanVisible != that.meanVisible) { 1097 return false; 1098 } 1099 if (this.medianVisible != that.medianVisible) { 1100 return false; 1101 } 1102 if (this.minOutlierVisible != that.minOutlierVisible) { 1103 return false; 1104 } 1105 if (this.maxOutlierVisible != that.maxOutlierVisible) { 1106 return false; 1107 } 1108 if (this.useOutlinePaintForWhiskers 1109 != that.useOutlinePaintForWhiskers) { 1110 return false; 1111 } 1112 if (this.whiskerWidth != that.whiskerWidth) { 1113 return false; 1114 } 1115 if (!PaintUtils.equal(this.artifactPaint, that.artifactPaint)) { 1116 return false; 1117 } 1118 return super.equals(obj); 1119 } 1120 1121 /** 1122 * Provides serialization support. 1123 * 1124 * @param stream the output stream. 1125 * 1126 * @throws IOException if there is an I/O error. 1127 */ 1128 private void writeObject(ObjectOutputStream stream) throws IOException { 1129 stream.defaultWriteObject(); 1130 SerialUtils.writePaint(this.artifactPaint, stream); 1131 } 1132 1133 /** 1134 * Provides serialization support. 1135 * 1136 * @param stream the input stream. 1137 * 1138 * @throws IOException if there is an I/O error. 1139 * @throws ClassNotFoundException if there is a classpath problem. 1140 */ 1141 private void readObject(ObjectInputStream stream) 1142 throws IOException, ClassNotFoundException { 1143 stream.defaultReadObject(); 1144 this.artifactPaint = SerialUtils.readPaint(stream); 1145 } 1146 1147}