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