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