001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, 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 * XYBoxAndWhiskerRenderer.java
029 * ----------------------------
030 * (C) Copyright 2003-2009, by David Browning and Contributors.
031 *
032 * Original Author: David Browning (for Australian Institute of Marine
033 * Science);
034 * Contributor(s): David Gilbert (for Object Refinery Limited);
035 *
036 * Changes
037 * -------
038 * 05-Aug-2003 : Version 1, contributed by David Browning. Based on code in the
039 * CandlestickRenderer class. Additional modifications by David
040 * Gilbert to make the code work with 0.9.10 changes (DG);
041 * 08-Aug-2003 : Updated some of the Javadoc
042 * Allowed BoxAndwhiskerDataset Average value to be null - the
043 * average value is an AIMS requirement
044 * Allow the outlier and farout coefficients to be set - though
045 * at the moment this only affects the calculation of farouts.
046 * Added artifactPaint variable and setter/getter
047 * 12-Aug-2003 Rewrote code to sort out and process outliers to take
048 * advantage of changes in DefaultBoxAndWhiskerDataset
049 * Added a limit of 10% for width of box should no width be
050 * specified...maybe this should be setable???
051 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
052 * 08-Sep-2003 : Changed ValueAxis API (DG);
053 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
054 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
055 * 23-Apr-2004 : Added fillBox attribute, extended equals() method and fixed
056 * serialization issue (DG);
057 * 29-Apr-2004 : Fixed problem with drawing upper and lower shadows - bug id
058 * 944011 (DG);
059 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
060 * getYValue() (DG);
061 * 01-Oct-2004 : Renamed 'paint' --> 'boxPaint' to avoid conflict with
062 * inherited attribute (DG);
063 * 10-Jun-2005 : Updated equals() to handle GradientPaint (DG);
064 * 06-Oct-2005 : Removed setPaint() call in drawItem(), it is causing a
065 * loop (DG);
066 * ------------- JFREECHART 1.0.x ---------------------------------------------
067 * 02-Feb-2007 : Removed author tags from all over JFreeChart sources (DG);
068 * 05-Feb-2007 : Added event notifications and fixed drawing for horizontal
069 * plot orientation (DG);
070 * 13-Jun-2007 : Replaced deprecated method call (DG);
071 * 03-Jan-2008 : Check visibility of average marker before drawing it (DG);
072 * 27-Mar-2008 : If boxPaint is null, revert to itemPaint (DG);
073 * 27-Mar-2009 : Added findRangeBounds() method override (DG);
074 *
075 */
076
077 package org.jfree.chart.renderer.xy;
078
079 import java.awt.Color;
080 import java.awt.Graphics2D;
081 import java.awt.Paint;
082 import java.awt.Shape;
083 import java.awt.Stroke;
084 import java.awt.geom.Ellipse2D;
085 import java.awt.geom.Line2D;
086 import java.awt.geom.Point2D;
087 import java.awt.geom.Rectangle2D;
088 import java.io.IOException;
089 import java.io.ObjectInputStream;
090 import java.io.ObjectOutputStream;
091 import java.io.Serializable;
092 import java.util.ArrayList;
093 import java.util.Collections;
094 import java.util.Iterator;
095 import java.util.List;
096
097 import org.jfree.chart.axis.ValueAxis;
098 import org.jfree.chart.entity.EntityCollection;
099 import org.jfree.chart.event.RendererChangeEvent;
100 import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator;
101 import org.jfree.chart.plot.CrosshairState;
102 import org.jfree.chart.plot.PlotOrientation;
103 import org.jfree.chart.plot.PlotRenderingInfo;
104 import org.jfree.chart.plot.XYPlot;
105 import org.jfree.chart.renderer.Outlier;
106 import org.jfree.chart.renderer.OutlierList;
107 import org.jfree.chart.renderer.OutlierListCollection;
108 import org.jfree.data.Range;
109 import org.jfree.data.general.DatasetUtilities;
110 import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
111 import org.jfree.data.xy.XYDataset;
112 import org.jfree.io.SerialUtilities;
113 import org.jfree.ui.RectangleEdge;
114 import org.jfree.util.PaintUtilities;
115 import org.jfree.util.PublicCloneable;
116
117 /**
118 * A renderer that draws box-and-whisker items on an {@link XYPlot}. This
119 * renderer requires a {@link BoxAndWhiskerXYDataset}). The example shown here
120 * is generated by the <code>BoxAndWhiskerChartDemo2.java</code> program
121 * included in the JFreeChart demo collection:
122 * <br><br>
123 * <img src="../../../../../images/XYBoxAndWhiskerRendererSample.png"
124 * alt="XYBoxAndWhiskerRendererSample.png" />
125 * <P>
126 * This renderer does not include any code to calculate the crosshair point.
127 */
128 public class XYBoxAndWhiskerRenderer extends AbstractXYItemRenderer
129 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
130
131 /** For serialization. */
132 private static final long serialVersionUID = -8020170108532232324L;
133
134 /** The box width. */
135 private double boxWidth;
136
137 /** The paint used to fill the box. */
138 private transient Paint boxPaint;
139
140 /** A flag that controls whether or not the box is filled. */
141 private boolean fillBox;
142
143 /**
144 * The paint used to draw various artifacts such as outliers, farout
145 * symbol, average ellipse and median line.
146 */
147 private transient Paint artifactPaint = Color.black;
148
149 /**
150 * Creates a new renderer for box and whisker charts.
151 */
152 public XYBoxAndWhiskerRenderer() {
153 this(-1.0);
154 }
155
156 /**
157 * Creates a new renderer for box and whisker charts.
158 * <P>
159 * Use -1 for the box width if you prefer the width to be calculated
160 * automatically.
161 *
162 * @param boxWidth the box width.
163 */
164 public XYBoxAndWhiskerRenderer(double boxWidth) {
165 super();
166 this.boxWidth = boxWidth;
167 this.boxPaint = Color.green;
168 this.fillBox = true;
169 setBaseToolTipGenerator(new BoxAndWhiskerXYToolTipGenerator());
170 }
171
172 /**
173 * Returns the width of each box.
174 *
175 * @return The box width.
176 *
177 * @see #setBoxWidth(double)
178 */
179 public double getBoxWidth() {
180 return this.boxWidth;
181 }
182
183 /**
184 * Sets the box width and sends a {@link RendererChangeEvent} to all
185 * registered listeners.
186 * <P>
187 * If you set the width to a negative value, the renderer will calculate
188 * the box width automatically based on the space available on the chart.
189 *
190 * @param width the width.
191 *
192 * @see #getBoxWidth()
193 */
194 public void setBoxWidth(double width) {
195 if (width != this.boxWidth) {
196 this.boxWidth = width;
197 fireChangeEvent();
198 }
199 }
200
201 /**
202 * Returns the paint used to fill boxes.
203 *
204 * @return The paint (possibly <code>null</code>).
205 *
206 * @see #setBoxPaint(Paint)
207 */
208 public Paint getBoxPaint() {
209 return this.boxPaint;
210 }
211
212 /**
213 * Sets the paint used to fill boxes and sends a {@link RendererChangeEvent}
214 * to all registered listeners.
215 *
216 * @param paint the paint (<code>null</code> permitted).
217 *
218 * @see #getBoxPaint()
219 */
220 public void setBoxPaint(Paint paint) {
221 this.boxPaint = paint;
222 fireChangeEvent();
223 }
224
225 /**
226 * Returns the flag that controls whether or not the box is filled.
227 *
228 * @return A boolean.
229 *
230 * @see #setFillBox(boolean)
231 */
232 public boolean getFillBox() {
233 return this.fillBox;
234 }
235
236 /**
237 * Sets the flag that controls whether or not the box is filled and sends a
238 * {@link RendererChangeEvent} to all registered listeners.
239 *
240 * @param flag the flag.
241 *
242 * @see #setFillBox(boolean)
243 */
244 public void setFillBox(boolean flag) {
245 this.fillBox = flag;
246 fireChangeEvent();
247 }
248
249 /**
250 * Returns the paint used to paint the various artifacts such as outliers,
251 * farout symbol, median line and the averages ellipse.
252 *
253 * @return The paint (never <code>null</code>).
254 *
255 * @see #setArtifactPaint(Paint)
256 */
257 public Paint getArtifactPaint() {
258 return this.artifactPaint;
259 }
260
261 /**
262 * Sets the paint used to paint the various artifacts such as outliers,
263 * farout symbol, median line and the averages ellipse, and sends a
264 * {@link RendererChangeEvent} to all registered listeners.
265 *
266 * @param paint the paint (<code>null</code> not permitted).
267 *
268 * @see #getArtifactPaint()
269 */
270 public void setArtifactPaint(Paint paint) {
271 if (paint == null) {
272 throw new IllegalArgumentException("Null 'paint' argument.");
273 }
274 this.artifactPaint = paint;
275 fireChangeEvent();
276 }
277
278 /**
279 * Returns the range of values the renderer requires to display all the
280 * items from the specified dataset.
281 *
282 * @param dataset the dataset (<code>null</code> permitted).
283 *
284 * @return The range (<code>null</code> if the dataset is <code>null</code>
285 * or empty).
286 *
287 * @see #findDomainBounds(XYDataset)
288 */
289 public Range findRangeBounds(XYDataset dataset) {
290 return findRangeBounds(dataset, true);
291 }
292
293 /**
294 * Returns the box paint or, if this is <code>null</code>, the item
295 * paint.
296 *
297 * @param series the series index.
298 * @param item the item index.
299 *
300 * @return The paint used to fill the box for the specified item (never
301 * <code>null</code>).
302 *
303 * @since 1.0.10
304 */
305 protected Paint lookupBoxPaint(int series, int item) {
306 Paint p = getBoxPaint();
307 if (p != null) {
308 return p;
309 }
310 else {
311 // TODO: could change this to itemFillPaint(). For backwards
312 // compatibility, it might require a useFillPaint flag.
313 return getItemPaint(series, item);
314 }
315 }
316
317 /**
318 * Draws the visual representation of a single data item.
319 *
320 * @param g2 the graphics device.
321 * @param state the renderer state.
322 * @param dataArea the area within which the plot is being drawn.
323 * @param info collects info about the drawing.
324 * @param plot the plot (can be used to obtain standard color
325 * information etc).
326 * @param domainAxis the domain axis.
327 * @param rangeAxis the range axis.
328 * @param dataset the dataset (must be an instance of
329 * {@link BoxAndWhiskerXYDataset}).
330 * @param series the series index (zero-based).
331 * @param item the item index (zero-based).
332 * @param crosshairState crosshair information for the plot
333 * (<code>null</code> permitted).
334 * @param pass the pass index.
335 */
336 public void drawItem(Graphics2D g2,
337 XYItemRendererState state,
338 Rectangle2D dataArea,
339 PlotRenderingInfo info,
340 XYPlot plot,
341 ValueAxis domainAxis,
342 ValueAxis rangeAxis,
343 XYDataset dataset,
344 int series,
345 int item,
346 CrosshairState crosshairState,
347 int pass) {
348
349 PlotOrientation orientation = plot.getOrientation();
350
351 if (orientation == PlotOrientation.HORIZONTAL) {
352 drawHorizontalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
353 dataset, series, item, crosshairState, pass);
354 }
355 else if (orientation == PlotOrientation.VERTICAL) {
356 drawVerticalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
357 dataset, series, item, crosshairState, pass);
358 }
359
360 }
361
362 /**
363 * Draws the visual representation of a single data item.
364 *
365 * @param g2 the graphics device.
366 * @param dataArea the area within which the plot is being drawn.
367 * @param info collects info about the drawing.
368 * @param plot the plot (can be used to obtain standard color
369 * information etc).
370 * @param domainAxis the domain axis.
371 * @param rangeAxis the range axis.
372 * @param dataset the dataset (must be an instance of
373 * {@link BoxAndWhiskerXYDataset}).
374 * @param series the series index (zero-based).
375 * @param item the item index (zero-based).
376 * @param crosshairState crosshair information for the plot
377 * (<code>null</code> permitted).
378 * @param pass the pass index.
379 */
380 public void drawHorizontalItem(Graphics2D g2,
381 Rectangle2D dataArea,
382 PlotRenderingInfo info,
383 XYPlot plot,
384 ValueAxis domainAxis,
385 ValueAxis rangeAxis,
386 XYDataset dataset,
387 int series,
388 int item,
389 CrosshairState crosshairState,
390 int pass) {
391
392 // setup for collecting optional entity info...
393 EntityCollection entities = null;
394 if (info != null) {
395 entities = info.getOwner().getEntityCollection();
396 }
397
398 BoxAndWhiskerXYDataset boxAndWhiskerData
399 = (BoxAndWhiskerXYDataset) dataset;
400
401 Number x = boxAndWhiskerData.getX(series, item);
402 Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
403 Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
404 Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
405 Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
406 Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
407 Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
408
409 double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea,
410 plot.getDomainAxisEdge());
411
412 RectangleEdge location = plot.getRangeAxisEdge();
413 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea,
414 location);
415 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea,
416 location);
417 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
418 dataArea, location);
419 double yyAverage = 0.0;
420 if (yAverage != null) {
421 yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(),
422 dataArea, location);
423 }
424 double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(),
425 dataArea, location);
426 double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(),
427 dataArea, location);
428
429 double exactBoxWidth = getBoxWidth();
430 double width = exactBoxWidth;
431 double dataAreaX = dataArea.getHeight();
432 double maxBoxPercent = 0.1;
433 double maxBoxWidth = dataAreaX * maxBoxPercent;
434 if (exactBoxWidth <= 0.0) {
435 int itemCount = boxAndWhiskerData.getItemCount(series);
436 exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
437 if (exactBoxWidth < 3) {
438 width = 3;
439 }
440 else if (exactBoxWidth > maxBoxWidth) {
441 width = maxBoxWidth;
442 }
443 else {
444 width = exactBoxWidth;
445 }
446 }
447
448 g2.setPaint(getItemPaint(series, item));
449 Stroke s = getItemStroke(series, item);
450 g2.setStroke(s);
451
452 // draw the upper shadow
453 g2.draw(new Line2D.Double(yyMax, xx, yyQ3Median, xx));
454 g2.draw(new Line2D.Double(yyMax, xx - width / 2, yyMax,
455 xx + width / 2));
456
457 // draw the lower shadow
458 g2.draw(new Line2D.Double(yyMin, xx, yyQ1Median, xx));
459 g2.draw(new Line2D.Double(yyMin, xx - width / 2, yyMin,
460 xx + width / 2));
461
462 // draw the body
463 Shape box = null;
464 if (yyQ1Median < yyQ3Median) {
465 box = new Rectangle2D.Double(yyQ1Median, xx - width / 2,
466 yyQ3Median - yyQ1Median, width);
467 }
468 else {
469 box = new Rectangle2D.Double(yyQ3Median, xx - width / 2,
470 yyQ1Median - yyQ3Median, width);
471 }
472 if (this.fillBox) {
473 g2.setPaint(lookupBoxPaint(series, item));
474 g2.fill(box);
475 }
476 g2.setStroke(getItemOutlineStroke(series, item));
477 g2.setPaint(getItemOutlinePaint(series, item));
478 g2.draw(box);
479
480 // draw median
481 g2.setPaint(getArtifactPaint());
482 g2.draw(new Line2D.Double(yyMedian,
483 xx - width / 2, yyMedian, xx + width / 2));
484
485 // draw average - SPECIAL AIMS REQUIREMENT
486 if (yAverage != null) {
487 double aRadius = width / 4;
488 // here we check that the average marker will in fact be visible
489 // before drawing it...
490 if ((yyAverage > (dataArea.getMinX() - aRadius))
491 && (yyAverage < (dataArea.getMaxX() + aRadius))) {
492 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
493 yyAverage - aRadius, xx - aRadius, aRadius * 2,
494 aRadius * 2);
495 g2.fill(avgEllipse);
496 g2.draw(avgEllipse);
497 }
498 }
499
500 // FIXME: draw outliers
501
502 // add an entity for the item...
503 if (entities != null && box.intersects(dataArea)) {
504 addEntity(entities, box, dataset, series, item, yyAverage, xx);
505 }
506
507 }
508
509 /**
510 * Draws the visual representation of a single data item.
511 *
512 * @param g2 the graphics device.
513 * @param dataArea the area within which the plot is being drawn.
514 * @param info collects info about the drawing.
515 * @param plot the plot (can be used to obtain standard color
516 * information etc).
517 * @param domainAxis the domain axis.
518 * @param rangeAxis the range axis.
519 * @param dataset the dataset (must be an instance of
520 * {@link BoxAndWhiskerXYDataset}).
521 * @param series the series index (zero-based).
522 * @param item the item index (zero-based).
523 * @param crosshairState crosshair information for the plot
524 * (<code>null</code> permitted).
525 * @param pass the pass index.
526 */
527 public void drawVerticalItem(Graphics2D g2,
528 Rectangle2D dataArea,
529 PlotRenderingInfo info,
530 XYPlot plot,
531 ValueAxis domainAxis,
532 ValueAxis rangeAxis,
533 XYDataset dataset,
534 int series,
535 int item,
536 CrosshairState crosshairState,
537 int pass) {
538
539 // setup for collecting optional entity info...
540 EntityCollection entities = null;
541 if (info != null) {
542 entities = info.getOwner().getEntityCollection();
543 }
544
545 BoxAndWhiskerXYDataset boxAndWhiskerData
546 = (BoxAndWhiskerXYDataset) dataset;
547
548 Number x = boxAndWhiskerData.getX(series, item);
549 Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
550 Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
551 Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
552 Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
553 Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
554 Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
555 List yOutliers = boxAndWhiskerData.getOutliers(series, item);
556
557 double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea,
558 plot.getDomainAxisEdge());
559
560 RectangleEdge location = plot.getRangeAxisEdge();
561 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea,
562 location);
563 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea,
564 location);
565 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
566 dataArea, location);
567 double yyAverage = 0.0;
568 if (yAverage != null) {
569 yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(),
570 dataArea, location);
571 }
572 double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(),
573 dataArea, location);
574 double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(),
575 dataArea, location);
576 double yyOutlier;
577
578
579 double exactBoxWidth = getBoxWidth();
580 double width = exactBoxWidth;
581 double dataAreaX = dataArea.getMaxX() - dataArea.getMinX();
582 double maxBoxPercent = 0.1;
583 double maxBoxWidth = dataAreaX * maxBoxPercent;
584 if (exactBoxWidth <= 0.0) {
585 int itemCount = boxAndWhiskerData.getItemCount(series);
586 exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
587 if (exactBoxWidth < 3) {
588 width = 3;
589 }
590 else if (exactBoxWidth > maxBoxWidth) {
591 width = maxBoxWidth;
592 }
593 else {
594 width = exactBoxWidth;
595 }
596 }
597
598 g2.setPaint(getItemPaint(series, item));
599 Stroke s = getItemStroke(series, item);
600 g2.setStroke(s);
601
602 // draw the upper shadow
603 g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median));
604 g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2,
605 yyMax));
606
607 // draw the lower shadow
608 g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median));
609 g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2,
610 yyMin));
611
612 // draw the body
613 Shape box = null;
614 if (yyQ1Median > yyQ3Median) {
615 box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width,
616 yyQ1Median - yyQ3Median);
617 }
618 else {
619 box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width,
620 yyQ3Median - yyQ1Median);
621 }
622 if (this.fillBox) {
623 g2.setPaint(lookupBoxPaint(series, item));
624 g2.fill(box);
625 }
626 g2.setStroke(getItemOutlineStroke(series, item));
627 g2.setPaint(getItemOutlinePaint(series, item));
628 g2.draw(box);
629
630 // draw median
631 g2.setPaint(getArtifactPaint());
632 g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2,
633 yyMedian));
634
635 double aRadius = 0; // average radius
636 double oRadius = width / 3; // outlier radius
637
638 // draw average - SPECIAL AIMS REQUIREMENT
639 if (yAverage != null) {
640 aRadius = width / 4;
641 // here we check that the average marker will in fact be visible
642 // before drawing it...
643 if ((yyAverage > (dataArea.getMinY() - aRadius))
644 && (yyAverage < (dataArea.getMaxY() + aRadius))) {
645 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius,
646 yyAverage - aRadius, aRadius * 2, aRadius * 2);
647 g2.fill(avgEllipse);
648 g2.draw(avgEllipse);
649 }
650 }
651
652 List outliers = new ArrayList();
653 OutlierListCollection outlierListCollection
654 = new OutlierListCollection();
655
656 /* From outlier array sort out which are outliers and put these into
657 * an arraylist. If there are any farouts, set the flag on the
658 * OutlierListCollection
659 */
660
661 for (int i = 0; i < yOutliers.size(); i++) {
662 double outlier = ((Number) yOutliers.get(i)).doubleValue();
663 if (outlier > boxAndWhiskerData.getMaxOutlier(series,
664 item).doubleValue()) {
665 outlierListCollection.setHighFarOut(true);
666 }
667 else if (outlier < boxAndWhiskerData.getMinOutlier(series,
668 item).doubleValue()) {
669 outlierListCollection.setLowFarOut(true);
670 }
671 else if (outlier > boxAndWhiskerData.getMaxRegularValue(series,
672 item).doubleValue()) {
673 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
674 location);
675 outliers.add(new Outlier(xx, yyOutlier, oRadius));
676 }
677 else if (outlier < boxAndWhiskerData.getMinRegularValue(series,
678 item).doubleValue()) {
679 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
680 location);
681 outliers.add(new Outlier(xx, yyOutlier, oRadius));
682 }
683 Collections.sort(outliers);
684 }
685
686 // Process outliers. Each outlier is either added to the appropriate
687 // outlier list or a new outlier list is made
688 for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
689 Outlier outlier = (Outlier) iterator.next();
690 outlierListCollection.add(outlier);
691 }
692
693 // draw yOutliers
694 double maxAxisValue = rangeAxis.valueToJava2D(rangeAxis.getUpperBound(),
695 dataArea, location) + aRadius;
696 double minAxisValue = rangeAxis.valueToJava2D(rangeAxis.getLowerBound(),
697 dataArea, location) - aRadius;
698
699 // draw outliers
700 for (Iterator iterator = outlierListCollection.iterator();
701 iterator.hasNext();) {
702 OutlierList list = (OutlierList) iterator.next();
703 Outlier outlier = list.getAveragedOutlier();
704 Point2D point = outlier.getPoint();
705
706 if (list.isMultiple()) {
707 drawMultipleEllipse(point, width, oRadius, g2);
708 }
709 else {
710 drawEllipse(point, oRadius, g2);
711 }
712 }
713
714 // draw farout
715 if (outlierListCollection.isHighFarOut()) {
716 drawHighFarOut(aRadius, g2, xx, maxAxisValue);
717 }
718
719 if (outlierListCollection.isLowFarOut()) {
720 drawLowFarOut(aRadius, g2, xx, minAxisValue);
721 }
722
723 // add an entity for the item...
724 if (entities != null && box.intersects(dataArea)) {
725 addEntity(entities, box, dataset, series, item, xx, yyAverage);
726 }
727
728 }
729
730 /**
731 * Draws an ellipse to represent an outlier.
732 *
733 * @param point the location.
734 * @param oRadius the radius.
735 * @param g2 the graphics device.
736 */
737 protected void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
738 Ellipse2D.Double dot = new Ellipse2D.Double(point.getX() + oRadius / 2,
739 point.getY(), oRadius, oRadius);
740 g2.draw(dot);
741 }
742
743 /**
744 * Draws two ellipses to represent overlapping outliers.
745 *
746 * @param point the location.
747 * @param boxWidth the box width.
748 * @param oRadius the radius.
749 * @param g2 the graphics device.
750 */
751 protected void drawMultipleEllipse(Point2D point, double boxWidth,
752 double oRadius, Graphics2D g2) {
753
754 Ellipse2D.Double dot1 = new Ellipse2D.Double(point.getX()
755 - (boxWidth / 2) + oRadius, point.getY(), oRadius, oRadius);
756 Ellipse2D.Double dot2 = new Ellipse2D.Double(point.getX()
757 + (boxWidth / 2), point.getY(), oRadius, oRadius);
758 g2.draw(dot1);
759 g2.draw(dot2);
760
761 }
762
763 /**
764 * Draws a triangle to indicate the presence of far out values.
765 *
766 * @param aRadius the radius.
767 * @param g2 the graphics device.
768 * @param xx the x value.
769 * @param m the max y value.
770 */
771 protected void drawHighFarOut(double aRadius, Graphics2D g2, double xx,
772 double m) {
773 double side = aRadius * 2;
774 g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
775 g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
776 g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
777 }
778
779 /**
780 * Draws a triangle to indicate the presence of far out values.
781 *
782 * @param aRadius the radius.
783 * @param g2 the graphics device.
784 * @param xx the x value.
785 * @param m the min y value.
786 */
787 protected void drawLowFarOut(double aRadius, Graphics2D g2, double xx,
788 double m) {
789 double side = aRadius * 2;
790 g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
791 g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
792 g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
793 }
794
795 /**
796 * Tests this renderer for equality with another object.
797 *
798 * @param obj the object (<code>null</code> permitted).
799 *
800 * @return <code>true</code> or <code>false</code>.
801 */
802 public boolean equals(Object obj) {
803 if (obj == this) {
804 return true;
805 }
806 if (!(obj instanceof XYBoxAndWhiskerRenderer)) {
807 return false;
808 }
809 if (!super.equals(obj)) {
810 return false;
811 }
812 XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj;
813 if (this.boxWidth != that.getBoxWidth()) {
814 return false;
815 }
816 if (!PaintUtilities.equal(this.boxPaint, that.boxPaint)) {
817 return false;
818 }
819 if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) {
820 return false;
821 }
822 if (this.fillBox != that.fillBox) {
823 return false;
824 }
825 return true;
826
827 }
828
829 /**
830 * Provides serialization support.
831 *
832 * @param stream the output stream.
833 *
834 * @throws IOException if there is an I/O error.
835 */
836 private void writeObject(ObjectOutputStream stream) throws IOException {
837 stream.defaultWriteObject();
838 SerialUtilities.writePaint(this.boxPaint, stream);
839 SerialUtilities.writePaint(this.artifactPaint, stream);
840 }
841
842 /**
843 * Provides serialization support.
844 *
845 * @param stream the input stream.
846 *
847 * @throws IOException if there is an I/O error.
848 * @throws ClassNotFoundException if there is a classpath problem.
849 */
850 private void readObject(ObjectInputStream stream)
851 throws IOException, ClassNotFoundException {
852
853 stream.defaultReadObject();
854 this.boxPaint = SerialUtilities.readPaint(stream);
855 this.artifactPaint = SerialUtilities.readPaint(stream);
856 }
857
858 /**
859 * Returns a clone of the renderer.
860 *
861 * @return A clone.
862 *
863 * @throws CloneNotSupportedException if the renderer cannot be cloned.
864 */
865 public Object clone() throws CloneNotSupportedException {
866 return super.clone();
867 }
868
869 }