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