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