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