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