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