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