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