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
069 package org.jfree.chart.renderer.category;
070
071 import java.awt.Color;
072 import java.awt.Graphics2D;
073 import java.awt.Paint;
074 import java.awt.Shape;
075 import java.awt.Stroke;
076 import java.awt.geom.Ellipse2D;
077 import java.awt.geom.Line2D;
078 import java.awt.geom.Point2D;
079 import java.awt.geom.Rectangle2D;
080 import java.io.IOException;
081 import java.io.ObjectInputStream;
082 import java.io.ObjectOutputStream;
083 import java.io.Serializable;
084 import java.util.ArrayList;
085 import java.util.Collections;
086 import java.util.Iterator;
087 import java.util.List;
088
089 import org.jfree.chart.LegendItem;
090 import org.jfree.chart.axis.CategoryAxis;
091 import org.jfree.chart.axis.ValueAxis;
092 import org.jfree.chart.entity.CategoryItemEntity;
093 import org.jfree.chart.entity.EntityCollection;
094 import org.jfree.chart.event.RendererChangeEvent;
095 import org.jfree.chart.labels.CategoryToolTipGenerator;
096 import org.jfree.chart.plot.CategoryPlot;
097 import org.jfree.chart.plot.PlotOrientation;
098 import org.jfree.chart.plot.PlotRenderingInfo;
099 import org.jfree.chart.renderer.Outlier;
100 import org.jfree.chart.renderer.OutlierList;
101 import org.jfree.chart.renderer.OutlierListCollection;
102 import org.jfree.data.category.CategoryDataset;
103 import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset;
104 import org.jfree.io.SerialUtilities;
105 import org.jfree.ui.RectangleEdge;
106 import org.jfree.util.PaintUtilities;
107 import 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 */
114 public 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 fireChangeEvent();
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 fireChangeEvent();
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 fireChangeEvent();
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 }