001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jfreechart/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it 
010     * under the terms of the GNU Lesser General Public License as published by 
011     * the Free Software Foundation; either version 2.1 of the License, or 
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but 
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022     * USA.  
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     *
027     * ----------------------------
028     * XYBoxAndWhiskerRenderer.java
029     * ----------------------------
030     * (C) Copyright 2003-2008, by David Browning and Contributors.
031     *
032     * Original Author:  David Browning (for Australian Institute of Marine 
033     *                   Science);
034     * Contributor(s):   David Gilbert (for Object Refinery Limited);
035     *
036     * Changes
037     * -------
038     * 05-Aug-2003 : Version 1, contributed by David Browning.  Based on code in the
039     *               CandlestickRenderer class.  Additional modifications by David 
040     *               Gilbert to make the code work with 0.9.10 changes (DG);
041     * 08-Aug-2003 : Updated some of the Javadoc
042     *               Allowed BoxAndwhiskerDataset Average value to be null - the 
043     *               average value is an AIMS requirement
044     *               Allow the outlier and farout coefficients to be set - though 
045     *               at the moment this only affects the calculation of farouts.
046     *               Added artifactPaint variable and setter/getter
047     * 12-Aug-2003   Rewrote code to sort out and process outliers to take 
048     *               advantage of changes in DefaultBoxAndWhiskerDataset
049     *               Added a limit of 10% for width of box should no width be 
050     *               specified...maybe this should be setable???
051     * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
052     * 08-Sep-2003 : Changed ValueAxis API (DG);
053     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
054     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
055     * 23-Apr-2004 : Added fillBox attribute, extended equals() method and fixed 
056     *               serialization issue (DG);
057     * 29-Apr-2004 : Fixed problem with drawing upper and lower shadows - bug id 
058     *               944011 (DG);
059     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
060     *               getYValue() (DG);
061     * 01-Oct-2004 : Renamed 'paint' --> 'boxPaint' to avoid conflict with 
062     *               inherited attribute (DG);
063     * 10-Jun-2005 : Updated equals() to handle GradientPaint (DG);
064     * 06-Oct-2005 : Removed setPaint() call in drawItem(), it is causing a 
065     *               loop (DG);
066     * ------------- JFREECHART 1.0.x ---------------------------------------------
067     * 02-Feb-2007 : Removed author tags from all over JFreeChart sources (DG);
068     * 05-Feb-2007 : Added event notifications and fixed drawing for horizontal 
069     *               plot orientation (DG);
070     * 13-Jun-2007 : Replaced deprecated method call (DG);
071     * 03-Jan-2008 : Check visibility of average marker before drawing it (DG);
072     *
073     */
074    
075    package org.jfree.chart.renderer.xy;
076    
077    import java.awt.Color;
078    import java.awt.Graphics2D;
079    import java.awt.Paint;
080    import java.awt.Shape;
081    import java.awt.Stroke;
082    import java.awt.geom.Ellipse2D;
083    import java.awt.geom.Line2D;
084    import java.awt.geom.Point2D;
085    import java.awt.geom.Rectangle2D;
086    import java.io.IOException;
087    import java.io.ObjectInputStream;
088    import java.io.ObjectOutputStream;
089    import java.io.Serializable;
090    import java.util.ArrayList;
091    import java.util.Collections;
092    import java.util.Iterator;
093    import java.util.List;
094    
095    import org.jfree.chart.axis.ValueAxis;
096    import org.jfree.chart.entity.EntityCollection;
097    import org.jfree.chart.event.RendererChangeEvent;
098    import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator;
099    import org.jfree.chart.plot.CrosshairState;
100    import org.jfree.chart.plot.PlotOrientation;
101    import org.jfree.chart.plot.PlotRenderingInfo;
102    import org.jfree.chart.plot.XYPlot;
103    import org.jfree.chart.renderer.Outlier;
104    import org.jfree.chart.renderer.OutlierList;
105    import org.jfree.chart.renderer.OutlierListCollection;
106    import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
107    import org.jfree.data.xy.XYDataset;
108    import org.jfree.io.SerialUtilities;
109    import org.jfree.ui.RectangleEdge;
110    import org.jfree.util.PaintUtilities;
111    import org.jfree.util.PublicCloneable;
112    
113    /**
114     * A renderer that draws box-and-whisker items on an {@link XYPlot}.  This 
115     * renderer requires a {@link BoxAndWhiskerXYDataset}).
116     * <P>
117     * This renderer does not include any code to calculate the crosshair point.
118     */
119    public class XYBoxAndWhiskerRenderer extends AbstractXYItemRenderer 
120                                         implements XYItemRenderer, 
121                                                    Cloneable,
122                                                    PublicCloneable,
123                                                    Serializable {
124    
125        /** For serialization. */
126        private static final long serialVersionUID = -8020170108532232324L;
127        
128        /** The box width. */
129        private double boxWidth;
130    
131        /** The paint used to fill the box. */
132        private transient Paint boxPaint;
133    
134        /** A flag that controls whether or not the box is filled. */
135        private boolean fillBox;
136        
137        /** 
138         * The paint used to draw various artifacts such as outliers, farout 
139         * symbol, average ellipse and median line. 
140         */
141        private transient Paint artifactPaint = Color.black;
142    
143        /**
144         * Creates a new renderer for box and whisker charts.
145         */
146        public XYBoxAndWhiskerRenderer() {
147            this(-1.0);
148        }
149    
150        /**
151         * Creates a new renderer for box and whisker charts.
152         * <P>
153         * Use -1 for the box width if you prefer the width to be calculated 
154         * automatically.
155         *
156         * @param boxWidth  the box width.
157         */
158        public XYBoxAndWhiskerRenderer(double boxWidth) {
159            super();
160            this.boxWidth = boxWidth;
161            this.boxPaint = Color.green;
162            this.fillBox = true;
163            setBaseToolTipGenerator(new BoxAndWhiskerXYToolTipGenerator());
164        }
165    
166        /**
167         * Returns the width of each box.
168         *
169         * @return The box width.
170         * 
171         * @see #setBoxWidth(double)
172         */
173        public double getBoxWidth() {
174            return this.boxWidth;
175        }
176    
177        /**
178         * Sets the box width and sends a {@link RendererChangeEvent} to all 
179         * registered listeners.
180         * <P>
181         * If you set the width to a negative value, the renderer will calculate
182         * the box width automatically based on the space available on the chart.
183         *
184         * @param width  the width.
185         * 
186         * @see #getBoxWidth()
187         */
188        public void setBoxWidth(double width) {
189            if (width != this.boxWidth) {
190                this.boxWidth = width;
191                fireChangeEvent();
192            }
193        }
194    
195        /**
196         * Returns the paint used to fill boxes.
197         *
198         * @return The paint (possibly <code>null</code>).
199         * 
200         * @see #setBoxPaint(Paint)
201         */
202        public Paint getBoxPaint() {
203            return this.boxPaint;
204        }
205    
206        /**
207         * Sets the paint used to fill boxes and sends a {@link RendererChangeEvent}
208         * to all registered listeners.
209         *
210         * @param paint  the paint (<code>null</code> permitted).
211         * 
212         * @see #getBoxPaint()
213         */
214        public void setBoxPaint(Paint paint) {
215            this.boxPaint = paint;
216            fireChangeEvent();
217        }
218        
219        /**
220         * Returns the flag that controls whether or not the box is filled.
221         * 
222         * @return A boolean.
223         * 
224         * @see #setFillBox(boolean)
225         */
226        public boolean getFillBox() {
227            return this.fillBox;   
228        }
229        
230        /**
231         * Sets the flag that controls whether or not the box is filled and sends a 
232         * {@link RendererChangeEvent} to all registered listeners.
233         * 
234         * @param flag  the flag.
235         * 
236         * @see #setFillBox(boolean)
237         */
238        public void setFillBox(boolean flag) {
239            this.fillBox = flag;
240            fireChangeEvent();
241        }
242    
243        /**
244         * Returns the paint used to paint the various artifacts such as outliers, 
245         * farout symbol, median line and the averages ellipse.
246         *
247         * @return The paint (never <code>null</code>).
248         * 
249         * @see #setArtifactPaint(Paint)
250         */
251        public Paint getArtifactPaint() {
252            return this.artifactPaint;
253        }
254    
255        /**
256         * Sets the paint used to paint the various artifacts such as outliers, 
257         * farout symbol, median line and the averages ellipse, and sends a 
258         * {@link RendererChangeEvent} to all registered listeners.
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            fireChangeEvent();
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                // here we check that the average marker will in fact be visible
445                // before drawing it...
446                if ((yyAverage > (dataArea.getMinX() - aRadius)) 
447                        && (yyAverage < (dataArea.getMaxX() + aRadius))) {
448                    Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
449                            yyAverage - aRadius, xx - aRadius, aRadius * 2, 
450                            aRadius * 2);
451                    g2.fill(avgEllipse);
452                    g2.draw(avgEllipse);
453                }
454            }
455            
456            // FIXME: draw outliers
457            
458            // add an entity for the item...
459            if (entities != null && box.intersects(dataArea)) {
460                addEntity(entities, box, dataset, series, item, yyAverage, xx);
461            }
462    
463        }
464    
465        /**
466         * Draws the visual representation of a single data item.
467         *
468         * @param g2  the graphics device.
469         * @param dataArea  the area within which the plot is being drawn.
470         * @param info  collects info about the drawing.
471         * @param plot  the plot (can be used to obtain standard color 
472         *              information etc).
473         * @param domainAxis  the domain axis.
474         * @param rangeAxis  the range axis.
475         * @param dataset  the dataset.
476         * @param series  the series index (zero-based).
477         * @param item  the item index (zero-based).
478         * @param crosshairState  crosshair information for the plot 
479         *                        (<code>null</code> permitted).
480         * @param pass  the pass index.
481         */
482        public void drawVerticalItem(Graphics2D g2, 
483                                     Rectangle2D dataArea,
484                                     PlotRenderingInfo info,
485                                     XYPlot plot, 
486                                     ValueAxis domainAxis, 
487                                     ValueAxis rangeAxis,
488                                     XYDataset dataset, 
489                                     int series, 
490                                     int item,
491                                     CrosshairState crosshairState,
492                                     int pass) {
493    
494            // setup for collecting optional entity info...
495            EntityCollection entities = null;
496            if (info != null) {
497                entities = info.getOwner().getEntityCollection();
498            }
499    
500            BoxAndWhiskerXYDataset boxAndWhiskerData 
501                = (BoxAndWhiskerXYDataset) dataset;
502    
503            Number x = boxAndWhiskerData.getX(series, item);
504            Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
505            Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
506            Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
507            Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
508            Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
509            Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
510            List yOutliers = boxAndWhiskerData.getOutliers(series, item);
511    
512            double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 
513                    plot.getDomainAxisEdge());
514    
515            RectangleEdge location = plot.getRangeAxisEdge();
516            double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 
517                    location);
518            double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 
519                    location);
520            double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 
521                    dataArea, location);
522            double yyAverage = 0.0;
523            if (yAverage != null) {
524                yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 
525                        dataArea, location);
526            }
527            double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 
528                    dataArea, location);
529            double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 
530                    dataArea, location);
531            double yyOutlier;
532    
533    
534            double exactBoxWidth = getBoxWidth();
535            double width = exactBoxWidth;
536            double dataAreaX = dataArea.getMaxX() - dataArea.getMinX();
537            double maxBoxPercent = 0.1;
538            double maxBoxWidth = dataAreaX * maxBoxPercent;
539            if (exactBoxWidth <= 0.0) {
540                int itemCount = boxAndWhiskerData.getItemCount(series);
541                exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
542                if (exactBoxWidth < 3) {
543                    width = 3;
544                } 
545                else if (exactBoxWidth > maxBoxWidth) {
546                    width = maxBoxWidth;
547                } 
548                else {
549                    width = exactBoxWidth;
550                }
551            }
552    
553            Paint p = getBoxPaint();
554            if (p != null) {
555                g2.setPaint(p);
556            }
557            Stroke s = getItemStroke(series, item);
558    
559            g2.setStroke(s);
560    
561            // draw the upper shadow
562            g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median));
563            g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2, 
564                    yyMax));
565    
566            // draw the lower shadow
567            g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median));
568            g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2, 
569                    yyMin));
570            
571            // draw the body
572            Shape box = null;
573            if (yyQ1Median > yyQ3Median) {
574                box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width, 
575                        yyQ1Median - yyQ3Median);
576            }
577            else {
578                box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width, 
579                        yyQ3Median - yyQ1Median);
580            }
581            if (this.fillBox) {
582                g2.fill(box);   
583            }
584            g2.draw(box);
585    
586            // draw median
587            g2.setPaint(getArtifactPaint());
588            g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2, 
589                    yyMedian));
590    
591            double aRadius = 0;                 // average radius
592            double oRadius = width / 3;    // outlier radius
593    
594            // draw average - SPECIAL AIMS REQUIREMENT
595            if (yAverage != null) {
596                aRadius = width / 4;
597                // here we check that the average marker will in fact be visible
598                // before drawing it...
599                if ((yyAverage > (dataArea.getMinY() - aRadius)) 
600                        && (yyAverage < (dataArea.getMaxY() + aRadius))) {
601                    Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius, 
602                            yyAverage - aRadius, aRadius * 2, aRadius * 2);
603                    g2.fill(avgEllipse);
604                    g2.draw(avgEllipse);
605                }
606            }
607    
608            List outliers = new ArrayList();
609            OutlierListCollection outlierListCollection 
610                    = new OutlierListCollection();
611    
612            /* From outlier array sort out which are outliers and put these into 
613             * an arraylist. If there are any farouts, set the flag on the 
614             * OutlierListCollection
615             */
616    
617            for (int i = 0; i < yOutliers.size(); i++) {
618                double outlier = ((Number) yOutliers.get(i)).doubleValue();
619                if (outlier > boxAndWhiskerData.getMaxOutlier(series, 
620                        item).doubleValue()) {
621                    outlierListCollection.setHighFarOut(true);
622                } 
623                else if (outlier < boxAndWhiskerData.getMinOutlier(series, 
624                        item).doubleValue()) {
625                    outlierListCollection.setLowFarOut(true);
626                } 
627                else if (outlier > boxAndWhiskerData.getMaxRegularValue(series, 
628                        item).doubleValue()) {
629                    yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 
630                            location);
631                    outliers.add(new Outlier(xx, yyOutlier, oRadius));
632                }
633                else if (outlier < boxAndWhiskerData.getMinRegularValue(series, 
634                        item).doubleValue()) {
635                    yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 
636                            location);
637                    outliers.add(new Outlier(xx, yyOutlier, oRadius));
638                }
639                Collections.sort(outliers);
640            }
641    
642            // Process outliers. Each outlier is either added to the appropriate 
643            // outlier list or a new outlier list is made
644            for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
645                Outlier outlier = (Outlier) iterator.next();
646                outlierListCollection.add(outlier);
647            }
648    
649            // draw yOutliers
650            double maxAxisValue = rangeAxis.valueToJava2D(rangeAxis.getUpperBound(),
651                    dataArea, location) + aRadius;
652            double minAxisValue = rangeAxis.valueToJava2D(rangeAxis.getLowerBound(),
653                    dataArea, location) - aRadius;
654    
655            // draw outliers
656            for (Iterator iterator = outlierListCollection.iterator(); 
657                    iterator.hasNext();) {
658                OutlierList list = (OutlierList) iterator.next();
659                Outlier outlier = list.getAveragedOutlier();
660                Point2D point = outlier.getPoint();
661    
662                if (list.isMultiple()) {
663                    drawMultipleEllipse(point, width, oRadius, g2);
664                } 
665                else {
666                    drawEllipse(point, oRadius, g2);
667                }
668            }
669    
670            // draw farout
671            if (outlierListCollection.isHighFarOut()) {
672                drawHighFarOut(aRadius, g2, xx, maxAxisValue);
673            }
674    
675            if (outlierListCollection.isLowFarOut()) {
676                drawLowFarOut(aRadius, g2, xx, minAxisValue);
677            }
678            
679            // add an entity for the item...
680            if (entities != null && box.intersects(dataArea)) {
681                addEntity(entities, box, dataset, series, item, xx, yyAverage);
682            }
683    
684        }
685    
686        /**
687         * Draws an ellipse to represent an outlier.
688         * 
689         * @param point  the location.
690         * @param oRadius  the radius.
691         * @param g2  the graphics device.
692         */
693        protected void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
694            Ellipse2D.Double dot = new Ellipse2D.Double(point.getX() + oRadius / 2,
695                    point.getY(), oRadius, oRadius);
696            g2.draw(dot);
697        }
698    
699        /**
700         * Draws two ellipses to represent overlapping outliers.
701         * 
702         * @param point  the location.
703         * @param boxWidth  the box width.
704         * @param oRadius  the radius.
705         * @param g2  the graphics device.
706         */
707        protected void drawMultipleEllipse(Point2D point, double boxWidth, 
708                                           double oRadius, Graphics2D g2) {
709                                             
710            Ellipse2D.Double dot1 = new Ellipse2D.Double(point.getX() 
711                    - (boxWidth / 2) + oRadius, point.getY(), oRadius, oRadius);
712            Ellipse2D.Double dot2 = new Ellipse2D.Double(point.getX() 
713                    + (boxWidth / 2), point.getY(), oRadius, oRadius);
714            g2.draw(dot1);
715            g2.draw(dot2);
716            
717        }
718    
719        /**
720         * Draws a triangle to indicate the presence of far out values.
721         * 
722         * @param aRadius  the radius.
723         * @param g2  the graphics device.
724         * @param xx  the x value.
725         * @param m  the max y value.
726         */
727        protected void drawHighFarOut(double aRadius, Graphics2D g2, double xx, 
728                double m) {
729            double side = aRadius * 2;
730            g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
731            g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
732            g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
733        }
734    
735        /**
736         * Draws a triangle to indicate the presence of far out values.
737         * 
738         * @param aRadius  the radius.
739         * @param g2  the graphics device.
740         * @param xx  the x value.
741         * @param m  the min y value.
742         */
743        protected void drawLowFarOut(double aRadius, Graphics2D g2, double xx, 
744                double m) {
745            double side = aRadius * 2;
746            g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
747            g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
748            g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
749        }
750    
751        /**
752         * Tests this renderer for equality with another object.
753         *
754         * @param obj  the object (<code>null</code> permitted).
755         *
756         * @return <code>true</code> or <code>false</code>.
757         */
758        public boolean equals(Object obj) {
759            if (obj == this) {
760                return true;
761            }
762            if (!(obj instanceof XYBoxAndWhiskerRenderer)) {
763                return false;
764            }
765            if (!super.equals(obj)) {
766                return false;
767            }
768            XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj;
769            if (this.boxWidth != that.getBoxWidth()) {
770                return false;
771            }
772            if (!PaintUtilities.equal(this.boxPaint, that.boxPaint)) {
773                return false;
774            }
775            if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) {
776                return false;
777            }
778            if (this.fillBox != that.fillBox) {
779                return false;
780            }
781            return true;
782    
783        }
784    
785        /**
786         * Provides serialization support.
787         *
788         * @param stream  the output stream.
789         *
790         * @throws IOException  if there is an I/O error.
791         */
792        private void writeObject(ObjectOutputStream stream) throws IOException {
793            stream.defaultWriteObject();
794            SerialUtilities.writePaint(this.boxPaint, stream);
795            SerialUtilities.writePaint(this.artifactPaint, stream);
796        }
797    
798        /**
799         * Provides serialization support.
800         *
801         * @param stream  the input stream.
802         *
803         * @throws IOException  if there is an I/O error.
804         * @throws ClassNotFoundException  if there is a classpath problem.
805         */
806        private void readObject(ObjectInputStream stream) 
807            throws IOException, ClassNotFoundException {
808    
809            stream.defaultReadObject();
810            this.boxPaint = SerialUtilities.readPaint(stream);
811            this.artifactPaint = SerialUtilities.readPaint(stream);
812        }
813    
814        /**
815         * Returns a clone of the renderer.
816         * 
817         * @return A clone.
818         * 
819         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
820         */
821        public Object clone() throws CloneNotSupportedException {
822            return super.clone();
823        }
824    
825    }