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