001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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-2011, 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 * 087 */ 088 089package org.jfree.chart.renderer.category; 090 091import java.awt.Color; 092import java.awt.Graphics2D; 093import java.awt.Paint; 094import java.awt.Shape; 095import java.awt.Stroke; 096import java.awt.geom.Ellipse2D; 097import java.awt.geom.Line2D; 098import java.awt.geom.Point2D; 099import java.awt.geom.Rectangle2D; 100import java.io.IOException; 101import java.io.ObjectInputStream; 102import java.io.ObjectOutputStream; 103import java.io.Serializable; 104import java.util.ArrayList; 105import java.util.Collections; 106import java.util.Iterator; 107import java.util.List; 108 109import org.jfree.chart.LegendItem; 110import org.jfree.chart.axis.CategoryAxis; 111import org.jfree.chart.axis.ValueAxis; 112import org.jfree.chart.entity.EntityCollection; 113import org.jfree.chart.event.RendererChangeEvent; 114import org.jfree.chart.plot.CategoryPlot; 115import org.jfree.chart.plot.PlotOrientation; 116import org.jfree.chart.plot.PlotRenderingInfo; 117import org.jfree.chart.renderer.Outlier; 118import org.jfree.chart.renderer.OutlierList; 119import org.jfree.chart.renderer.OutlierListCollection; 120import org.jfree.data.Range; 121import org.jfree.data.category.CategoryDataset; 122import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset; 123import org.jfree.io.SerialUtilities; 124import org.jfree.ui.RectangleEdge; 125import org.jfree.util.PaintUtilities; 126import org.jfree.util.PublicCloneable; 127 128/** 129 * A box-and-whisker renderer. This renderer requires a 130 * {@link BoxAndWhiskerCategoryDataset} and is for use with the 131 * {@link CategoryPlot} class. The example shown here is generated 132 * by the <code>BoxAndWhiskerChartDemo1.java</code> program included in the 133 * JFreeChart Demo Collection: 134 * <br><br> 135 * <img src="../../../../../images/BoxAndWhiskerRendererSample.png" 136 * alt="BoxAndWhiskerRendererSample.png" /> 137 */ 138public class BoxAndWhiskerRenderer extends AbstractCategoryItemRenderer 139 implements Cloneable, PublicCloneable, Serializable { 140 141 /** For serialization. */ 142 private static final long serialVersionUID = 632027470694481177L; 143 144 /** The color used to paint the median line and average marker. */ 145 private transient Paint artifactPaint; 146 147 /** A flag that controls whether or not the box is filled. */ 148 private boolean fillBox; 149 150 /** The margin between items (boxes) within a category. */ 151 private double itemMargin; 152 153 /** 154 * The maximum bar width as percentage of the available space in the plot. 155 * Take care with the encoding - for example, 0.05 is five percent. 156 */ 157 private double maximumBarWidth; 158 159 /** 160 * A flag that controls whether or not the median indicator is drawn. 161 * 162 * @since 1.0.13 163 */ 164 private boolean medianVisible; 165 166 /** 167 * A flag that controls whether or not the mean indicator is drawn. 168 * 169 * @since 1.0.13 170 */ 171 private boolean meanVisible; 172 173 /** 174 * A flag that, if <code>true</code>, causes the whiskers to be drawn 175 * using the outline paint for the series. The default value is 176 * <code>false</code> and in that case the regular series paint is used. 177 * 178 * @since 1.0.14 179 */ 180 private boolean useOutlinePaintForWhiskers; 181 182 /** 183 * The width of the whiskers as fraction of the bar width. 184 * 185 * @since 1.0.14 186 */ 187 private double whiskerWidth; 188 189 /** 190 * Default constructor. 191 */ 192 public BoxAndWhiskerRenderer() { 193 this.artifactPaint = Color.black; 194 this.fillBox = true; 195 this.itemMargin = 0.20; 196 this.maximumBarWidth = 1.0; 197 this.medianVisible = true; 198 this.meanVisible = true; 199 this.useOutlinePaintForWhiskers = false; 200 this.whiskerWidth = 1.0; 201 setBaseLegendShape(new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0)); 202 } 203 204 /** 205 * Returns the paint used to color the median and average markers. 206 * 207 * @return The paint used to draw the median and average markers (never 208 * <code>null</code>). 209 * 210 * @see #setArtifactPaint(Paint) 211 */ 212 public Paint getArtifactPaint() { 213 return this.artifactPaint; 214 } 215 216 /** 217 * Sets the paint used to color the median and average markers and sends 218 * a {@link RendererChangeEvent} to all registered listeners. 219 * 220 * @param paint the paint (<code>null</code> not permitted). 221 * 222 * @see #getArtifactPaint() 223 */ 224 public void setArtifactPaint(Paint paint) { 225 if (paint == null) { 226 throw new IllegalArgumentException("Null 'paint' argument."); 227 } 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 public LegendItem getLegendItem(int datasetIndex, int series) { 452 453 CategoryPlot cp = getPlot(); 454 if (cp == null) { 455 return null; 456 } 457 458 // check that a legend item needs to be displayed... 459 if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) { 460 return null; 461 } 462 463 CategoryDataset dataset = cp.getDataset(datasetIndex); 464 String label = getLegendItemLabelGenerator().generateLabel(dataset, 465 series); 466 String description = label; 467 String toolTipText = null; 468 if (getLegendItemToolTipGenerator() != null) { 469 toolTipText = getLegendItemToolTipGenerator().generateLabel( 470 dataset, series); 471 } 472 String urlText = null; 473 if (getLegendItemURLGenerator() != null) { 474 urlText = getLegendItemURLGenerator().generateLabel(dataset, 475 series); 476 } 477 Shape shape = lookupLegendShape(series); 478 Paint paint = lookupSeriesPaint(series); 479 Paint outlinePaint = lookupSeriesOutlinePaint(series); 480 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 481 LegendItem result = new LegendItem(label, description, toolTipText, 482 urlText, shape, paint, outlineStroke, outlinePaint); 483 result.setLabelFont(lookupLegendTextFont(series)); 484 Paint labelPaint = lookupLegendTextPaint(series); 485 if (labelPaint != null) { 486 result.setLabelPaint(labelPaint); 487 } 488 result.setDataset(dataset); 489 result.setDatasetIndex(datasetIndex); 490 result.setSeriesKey(dataset.getRowKey(series)); 491 result.setSeriesIndex(series); 492 return result; 493 494 } 495 496 /** 497 * Returns the range of values from the specified dataset that the 498 * renderer will require to display all the data. 499 * 500 * @param dataset the dataset. 501 * 502 * @return The range. 503 */ 504 public Range findRangeBounds(CategoryDataset dataset) { 505 return super.findRangeBounds(dataset, true); 506 } 507 508 /** 509 * Initialises the renderer. This method gets called once at the start of 510 * the process of drawing a chart. 511 * 512 * @param g2 the graphics device. 513 * @param dataArea the area in which the data is to be plotted. 514 * @param plot the plot. 515 * @param rendererIndex the renderer index. 516 * @param info collects chart rendering information for return to caller. 517 * 518 * @return The renderer state. 519 */ 520 public CategoryItemRendererState initialise(Graphics2D g2, 521 Rectangle2D dataArea, 522 CategoryPlot plot, 523 int rendererIndex, 524 PlotRenderingInfo info) { 525 526 CategoryItemRendererState state = super.initialise(g2, dataArea, plot, 527 rendererIndex, info); 528 // calculate the box width 529 CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex); 530 CategoryDataset dataset = plot.getDataset(rendererIndex); 531 if (dataset != null) { 532 int columns = dataset.getColumnCount(); 533 int rows = dataset.getRowCount(); 534 double space = 0.0; 535 PlotOrientation orientation = plot.getOrientation(); 536 if (orientation == PlotOrientation.HORIZONTAL) { 537 space = dataArea.getHeight(); 538 } 539 else if (orientation == PlotOrientation.VERTICAL) { 540 space = dataArea.getWidth(); 541 } 542 double maxWidth = space * getMaximumBarWidth(); 543 double categoryMargin = 0.0; 544 double currentItemMargin = 0.0; 545 if (columns > 1) { 546 categoryMargin = domainAxis.getCategoryMargin(); 547 } 548 if (rows > 1) { 549 currentItemMargin = getItemMargin(); 550 } 551 double used = space * (1 - domainAxis.getLowerMargin() 552 - domainAxis.getUpperMargin() 553 - categoryMargin - currentItemMargin); 554 if ((rows * columns) > 0) { 555 state.setBarWidth(Math.min(used / (dataset.getColumnCount() 556 * dataset.getRowCount()), maxWidth)); 557 } 558 else { 559 state.setBarWidth(Math.min(used, maxWidth)); 560 } 561 } 562 return state; 563 564 } 565 566 /** 567 * Draw a single data item. 568 * 569 * @param g2 the graphics device. 570 * @param state the renderer state. 571 * @param dataArea the area in which the data is drawn. 572 * @param plot the plot. 573 * @param domainAxis the domain axis. 574 * @param rangeAxis the range axis. 575 * @param dataset the data (must be an instance of 576 * {@link BoxAndWhiskerCategoryDataset}). 577 * @param row the row index (zero-based). 578 * @param column the column index (zero-based). 579 * @param pass the pass index. 580 */ 581 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 582 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 583 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 584 int pass) { 585 586 // do nothing if item is not visible 587 if (!getItemVisible(row, column)) { 588 return; 589 } 590 591 if (!(dataset instanceof BoxAndWhiskerCategoryDataset)) { 592 throw new IllegalArgumentException( 593 "BoxAndWhiskerRenderer.drawItem() : the data should be " 594 + "of type BoxAndWhiskerCategoryDataset only."); 595 } 596 597 PlotOrientation orientation = plot.getOrientation(); 598 599 if (orientation == PlotOrientation.HORIZONTAL) { 600 drawHorizontalItem(g2, state, dataArea, plot, domainAxis, 601 rangeAxis, dataset, row, column); 602 } 603 else if (orientation == PlotOrientation.VERTICAL) { 604 drawVerticalItem(g2, state, dataArea, plot, domainAxis, 605 rangeAxis, dataset, row, column); 606 } 607 608 } 609 610 /** 611 * Draws the visual representation of a single data item when the plot has 612 * a horizontal orientation. 613 * 614 * @param g2 the graphics device. 615 * @param state the renderer state. 616 * @param dataArea the area within which the plot is being drawn. 617 * @param plot the plot (can be used to obtain standard color 618 * information etc). 619 * @param domainAxis the domain axis. 620 * @param rangeAxis the range axis. 621 * @param dataset the dataset (must be an instance of 622 * {@link BoxAndWhiskerCategoryDataset}). 623 * @param row the row index (zero-based). 624 * @param column the column index (zero-based). 625 */ 626 public void drawHorizontalItem(Graphics2D g2, 627 CategoryItemRendererState state, Rectangle2D dataArea, 628 CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis, 629 CategoryDataset dataset, int row, int column) { 630 631 BoxAndWhiskerCategoryDataset bawDataset 632 = (BoxAndWhiskerCategoryDataset) dataset; 633 634 double categoryEnd = domainAxis.getCategoryEnd(column, 635 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 636 double categoryStart = domainAxis.getCategoryStart(column, 637 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 638 double categoryWidth = Math.abs(categoryEnd - categoryStart); 639 640 double yy = categoryStart; 641 int seriesCount = getRowCount(); 642 int categoryCount = getColumnCount(); 643 644 if (seriesCount > 1) { 645 double seriesGap = dataArea.getHeight() * getItemMargin() 646 / (categoryCount * (seriesCount - 1)); 647 double usedWidth = (state.getBarWidth() * seriesCount) 648 + (seriesGap * (seriesCount - 1)); 649 // offset the start of the boxes if the total width used is smaller 650 // than the category width 651 double offset = (categoryWidth - usedWidth) / 2; 652 yy = yy + offset + (row * (state.getBarWidth() + seriesGap)); 653 } 654 else { 655 // offset the start of the box if the box width is smaller than 656 // the category width 657 double offset = (categoryWidth - state.getBarWidth()) / 2; 658 yy = yy + offset; 659 } 660 661 g2.setPaint(getItemPaint(row, column)); 662 Stroke s = getItemStroke(row, column); 663 g2.setStroke(s); 664 665 RectangleEdge location = plot.getRangeAxisEdge(); 666 667 Number xQ1 = bawDataset.getQ1Value(row, column); 668 Number xQ3 = bawDataset.getQ3Value(row, column); 669 Number xMax = bawDataset.getMaxRegularValue(row, column); 670 Number xMin = bawDataset.getMinRegularValue(row, column); 671 672 Shape box = null; 673 if (xQ1 != null && xQ3 != null && xMax != null && xMin != null) { 674 675 double xxQ1 = rangeAxis.valueToJava2D(xQ1.doubleValue(), dataArea, 676 location); 677 double xxQ3 = rangeAxis.valueToJava2D(xQ3.doubleValue(), dataArea, 678 location); 679 double xxMax = rangeAxis.valueToJava2D(xMax.doubleValue(), dataArea, 680 location); 681 double xxMin = rangeAxis.valueToJava2D(xMin.doubleValue(), dataArea, 682 location); 683 double yymid = yy + state.getBarWidth() / 2.0; 684 double halfW = (state.getBarWidth() / 2.0) * this.whiskerWidth; 685 686 // draw the box... 687 box = new Rectangle2D.Double(Math.min(xxQ1, xxQ3), yy, 688 Math.abs(xxQ1 - xxQ3), state.getBarWidth()); 689 if (this.fillBox) { 690 g2.fill(box); 691 } 692 693 Paint outlinePaint = getItemOutlinePaint(row, column); 694 if (this.useOutlinePaintForWhiskers) { 695 g2.setPaint(outlinePaint); 696 } 697 // draw the upper shadow... 698 g2.draw(new Line2D.Double(xxMax, yymid, xxQ3, yymid)); 699 g2.draw(new Line2D.Double(xxMax, yymid - halfW, xxMax, 700 yymid + halfW)); 701 702 // draw the lower shadow... 703 g2.draw(new Line2D.Double(xxMin, yymid, xxQ1, yymid)); 704 g2.draw(new Line2D.Double(xxMin, yymid - halfW, xxMin, 705 yy + halfW)); 706 707 g2.setStroke(getItemOutlineStroke(row, column)); 708 g2.setPaint(outlinePaint); 709 g2.draw(box); 710 } 711 712 // draw mean - SPECIAL AIMS REQUIREMENT... 713 g2.setPaint(this.artifactPaint); 714 double aRadius = 0; // average radius 715 if (this.meanVisible) { 716 Number xMean = bawDataset.getMeanValue(row, column); 717 if (xMean != null) { 718 double xxMean = rangeAxis.valueToJava2D(xMean.doubleValue(), 719 dataArea, location); 720 aRadius = state.getBarWidth() / 4; 721 // here we check that the average marker will in fact be 722 // visible before drawing it... 723 if ((xxMean > (dataArea.getMinX() - aRadius)) 724 && (xxMean < (dataArea.getMaxX() + aRadius))) { 725 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xxMean 726 - aRadius, yy + aRadius, aRadius * 2, aRadius * 2); 727 g2.fill(avgEllipse); 728 g2.draw(avgEllipse); 729 } 730 } 731 } 732 733 // draw median... 734 if (this.medianVisible) { 735 Number xMedian = bawDataset.getMedianValue(row, column); 736 if (xMedian != null) { 737 double xxMedian = rangeAxis.valueToJava2D(xMedian.doubleValue(), 738 dataArea, location); 739 g2.draw(new Line2D.Double(xxMedian, yy, xxMedian, 740 yy + state.getBarWidth())); 741 } 742 } 743 744 // collect entity and tool tip information... 745 if (state.getInfo() != null && box != null) { 746 EntityCollection entities = state.getEntityCollection(); 747 if (entities != null) { 748 addItemEntity(entities, dataset, row, column, box); 749 } 750 } 751 752 } 753 754 /** 755 * Draws the visual representation of a single data item when the plot has 756 * a vertical orientation. 757 * 758 * @param g2 the graphics device. 759 * @param state the renderer state. 760 * @param dataArea the area within which the plot is being drawn. 761 * @param plot the plot (can be used to obtain standard color information 762 * etc). 763 * @param domainAxis the domain axis. 764 * @param rangeAxis the range axis. 765 * @param dataset the dataset (must be an instance of 766 * {@link BoxAndWhiskerCategoryDataset}). 767 * @param row the row index (zero-based). 768 * @param column the column index (zero-based). 769 */ 770 public void drawVerticalItem(Graphics2D g2, CategoryItemRendererState state, 771 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 772 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column) { 773 774 BoxAndWhiskerCategoryDataset bawDataset 775 = (BoxAndWhiskerCategoryDataset) dataset; 776 777 double categoryEnd = domainAxis.getCategoryEnd(column, 778 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 779 double categoryStart = domainAxis.getCategoryStart(column, 780 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 781 double categoryWidth = categoryEnd - categoryStart; 782 783 double xx = categoryStart; 784 int seriesCount = getRowCount(); 785 int categoryCount = getColumnCount(); 786 787 if (seriesCount > 1) { 788 double seriesGap = dataArea.getWidth() * getItemMargin() 789 / (categoryCount * (seriesCount - 1)); 790 double usedWidth = (state.getBarWidth() * seriesCount) 791 + (seriesGap * (seriesCount - 1)); 792 // offset the start of the boxes if the total width used is smaller 793 // than the category width 794 double offset = (categoryWidth - usedWidth) / 2; 795 xx = xx + offset + (row * (state.getBarWidth() + seriesGap)); 796 } 797 else { 798 // offset the start of the box if the box width is smaller than the 799 // category width 800 double offset = (categoryWidth - state.getBarWidth()) / 2; 801 xx = xx + offset; 802 } 803 804 double yyAverage = 0.0; 805 double yyOutlier; 806 807 Paint itemPaint = getItemPaint(row, column); 808 g2.setPaint(itemPaint); 809 Stroke s = getItemStroke(row, column); 810 g2.setStroke(s); 811 812 double aRadius = 0; // average radius 813 814 RectangleEdge location = plot.getRangeAxisEdge(); 815 816 Number yQ1 = bawDataset.getQ1Value(row, column); 817 Number yQ3 = bawDataset.getQ3Value(row, column); 818 Number yMax = bawDataset.getMaxRegularValue(row, column); 819 Number yMin = bawDataset.getMinRegularValue(row, column); 820 Shape box = null; 821 if (yQ1 != null && yQ3 != null && yMax != null && yMin != null) { 822 823 double yyQ1 = rangeAxis.valueToJava2D(yQ1.doubleValue(), dataArea, 824 location); 825 double yyQ3 = rangeAxis.valueToJava2D(yQ3.doubleValue(), dataArea, 826 location); 827 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), 828 dataArea, location); 829 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), 830 dataArea, location); 831 double xxmid = xx + state.getBarWidth() / 2.0; 832 double halfW = (state.getBarWidth() / 2.0) * this.whiskerWidth; 833 834 // draw the body... 835 box = new Rectangle2D.Double(xx, Math.min(yyQ1, yyQ3), 836 state.getBarWidth(), Math.abs(yyQ1 - yyQ3)); 837 if (this.fillBox) { 838 g2.fill(box); 839 } 840 841 Paint outlinePaint = getItemOutlinePaint(row, column); 842 if (this.useOutlinePaintForWhiskers) { 843 g2.setPaint(outlinePaint); 844 } 845 // draw the upper shadow... 846 g2.draw(new Line2D.Double(xxmid, yyMax, xxmid, yyQ3)); 847 g2.draw(new Line2D.Double(xxmid - halfW, yyMax, xxmid + halfW, yyMax)); 848 849 // draw the lower shadow... 850 g2.draw(new Line2D.Double(xxmid, yyMin, xxmid, yyQ1)); 851 g2.draw(new Line2D.Double(xxmid - halfW, yyMin, xxmid + halfW, yyMin)); 852 853 g2.setStroke(getItemOutlineStroke(row, column)); 854 g2.setPaint(outlinePaint); 855 g2.draw(box); 856 } 857 858 g2.setPaint(this.artifactPaint); 859 860 // draw mean - SPECIAL AIMS REQUIREMENT... 861 if (this.meanVisible) { 862 Number yMean = bawDataset.getMeanValue(row, column); 863 if (yMean != null) { 864 yyAverage = rangeAxis.valueToJava2D(yMean.doubleValue(), 865 dataArea, location); 866 aRadius = state.getBarWidth() / 4; 867 // here we check that the average marker will in fact be 868 // visible before drawing it... 869 if ((yyAverage > (dataArea.getMinY() - aRadius)) 870 && (yyAverage < (dataArea.getMaxY() + aRadius))) { 871 Ellipse2D.Double avgEllipse = new Ellipse2D.Double( 872 xx + aRadius, yyAverage - aRadius, aRadius * 2, 873 aRadius * 2); 874 g2.fill(avgEllipse); 875 g2.draw(avgEllipse); 876 } 877 } 878 } 879 880 // draw median... 881 if (this.medianVisible) { 882 Number yMedian = bawDataset.getMedianValue(row, column); 883 if (yMedian != null) { 884 double yyMedian = rangeAxis.valueToJava2D( 885 yMedian.doubleValue(), dataArea, location); 886 g2.draw(new Line2D.Double(xx, yyMedian, 887 xx + state.getBarWidth(), yyMedian)); 888 } 889 } 890 891 // draw yOutliers... 892 double maxAxisValue = rangeAxis.valueToJava2D( 893 rangeAxis.getUpperBound(), dataArea, location) + aRadius; 894 double minAxisValue = rangeAxis.valueToJava2D( 895 rangeAxis.getLowerBound(), dataArea, location) - aRadius; 896 897 g2.setPaint(itemPaint); 898 899 // draw outliers 900 double oRadius = state.getBarWidth() / 3; // outlier radius 901 List outliers = new ArrayList(); 902 OutlierListCollection outlierListCollection 903 = new OutlierListCollection(); 904 905 // From outlier array sort out which are outliers and put these into a 906 // list If there are any farouts, set the flag on the 907 // OutlierListCollection 908 List yOutliers = bawDataset.getOutliers(row, column); 909 if (yOutliers != null) { 910 for (int i = 0; i < yOutliers.size(); i++) { 911 double outlier = ((Number) yOutliers.get(i)).doubleValue(); 912 Number minOutlier = bawDataset.getMinOutlier(row, column); 913 Number maxOutlier = bawDataset.getMaxOutlier(row, column); 914 Number minRegular = bawDataset.getMinRegularValue(row, column); 915 Number maxRegular = bawDataset.getMaxRegularValue(row, column); 916 if (outlier > maxOutlier.doubleValue()) { 917 outlierListCollection.setHighFarOut(true); 918 } 919 else if (outlier < minOutlier.doubleValue()) { 920 outlierListCollection.setLowFarOut(true); 921 } 922 else if (outlier > maxRegular.doubleValue()) { 923 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 924 location); 925 outliers.add(new Outlier(xx + state.getBarWidth() / 2.0, 926 yyOutlier, oRadius)); 927 } 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 } 954 else { 955 drawEllipse(point, oRadius, g2); 956 } 957 } 958 959 // draw farout indicators 960 if (outlierListCollection.isHighFarOut()) { 961 drawHighFarOut(aRadius / 2.0, g2, 962 xx + state.getBarWidth() / 2.0, maxAxisValue); 963 } 964 965 if (outlierListCollection.isLowFarOut()) { 966 drawLowFarOut(aRadius / 2.0, g2, 967 xx + state.getBarWidth() / 2.0, minAxisValue); 968 } 969 } 970 // collect entity and tool tip information... 971 if (state.getInfo() != null && box != null) { 972 EntityCollection entities = state.getEntityCollection(); 973 if (entities != null) { 974 addItemEntity(entities, dataset, row, column, box); 975 } 976 } 977 978 } 979 980 /** 981 * Draws a dot to represent an outlier. 982 * 983 * @param point the location. 984 * @param oRadius the radius. 985 * @param g2 the graphics device. 986 */ 987 private void drawEllipse(Point2D point, double oRadius, Graphics2D g2) { 988 Ellipse2D dot = new Ellipse2D.Double(point.getX() + oRadius / 2, 989 point.getY(), oRadius, oRadius); 990 g2.draw(dot); 991 } 992 993 /** 994 * Draws two dots to represent the average value of more than one outlier. 995 * 996 * @param point the location 997 * @param boxWidth the box width. 998 * @param oRadius the radius. 999 * @param g2 the graphics device. 1000 */ 1001 private void drawMultipleEllipse(Point2D point, double boxWidth, 1002 double oRadius, Graphics2D g2) { 1003 1004 Ellipse2D dot1 = new Ellipse2D.Double(point.getX() - (boxWidth / 2) 1005 + oRadius, point.getY(), oRadius, oRadius); 1006 Ellipse2D dot2 = new Ellipse2D.Double(point.getX() + (boxWidth / 2), 1007 point.getY(), oRadius, oRadius); 1008 g2.draw(dot1); 1009 g2.draw(dot2); 1010 } 1011 1012 /** 1013 * Draws a triangle to indicate the presence of far-out values. 1014 * 1015 * @param aRadius the radius. 1016 * @param g2 the graphics device. 1017 * @param xx the x coordinate. 1018 * @param m the y coordinate. 1019 */ 1020 private void drawHighFarOut(double aRadius, Graphics2D g2, double xx, 1021 double m) { 1022 double side = aRadius * 2; 1023 g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side)); 1024 g2.draw(new Line2D.Double(xx - side, m + side, xx, m)); 1025 g2.draw(new Line2D.Double(xx + side, m + side, xx, m)); 1026 } 1027 1028 /** 1029 * Draws a triangle to indicate the presence of far-out values. 1030 * 1031 * @param aRadius the radius. 1032 * @param g2 the graphics device. 1033 * @param xx the x coordinate. 1034 * @param m the y coordinate. 1035 */ 1036 private void drawLowFarOut(double aRadius, Graphics2D g2, double xx, 1037 double m) { 1038 double side = aRadius * 2; 1039 g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side)); 1040 g2.draw(new Line2D.Double(xx - side, m - side, xx, m)); 1041 g2.draw(new Line2D.Double(xx + side, m - side, xx, m)); 1042 } 1043 1044 /** 1045 * Tests this renderer for equality with an arbitrary object. 1046 * 1047 * @param obj the object (<code>null</code> permitted). 1048 * 1049 * @return <code>true</code> or <code>false</code>. 1050 */ 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.useOutlinePaintForWhiskers 1075 != that.useOutlinePaintForWhiskers) { 1076 return false; 1077 } 1078 if (this.whiskerWidth != that.whiskerWidth) { 1079 return false; 1080 } 1081 if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) { 1082 return false; 1083 } 1084 return super.equals(obj); 1085 } 1086 1087 /** 1088 * Provides serialization support. 1089 * 1090 * @param stream the output stream. 1091 * 1092 * @throws IOException if there is an I/O error. 1093 */ 1094 private void writeObject(ObjectOutputStream stream) throws IOException { 1095 stream.defaultWriteObject(); 1096 SerialUtilities.writePaint(this.artifactPaint, stream); 1097 } 1098 1099 /** 1100 * Provides serialization support. 1101 * 1102 * @param stream the input stream. 1103 * 1104 * @throws IOException if there is an I/O error. 1105 * @throws ClassNotFoundException if there is a classpath problem. 1106 */ 1107 private void readObject(ObjectInputStream stream) 1108 throws IOException, ClassNotFoundException { 1109 stream.defaultReadObject(); 1110 this.artifactPaint = SerialUtilities.readPaint(stream); 1111 } 1112 1113}