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