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