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