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.6 2007/02/05 11:42:29 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     *
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.entity.XYItemEntity;
098    import org.jfree.chart.event.RendererChangeEvent;
099    import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator;
100    import org.jfree.chart.labels.XYToolTipGenerator;
101    import org.jfree.chart.plot.CrosshairState;
102    import org.jfree.chart.plot.PlotOrientation;
103    import org.jfree.chart.plot.PlotRenderingInfo;
104    import org.jfree.chart.plot.XYPlot;
105    import org.jfree.chart.renderer.Outlier;
106    import org.jfree.chart.renderer.OutlierList;
107    import org.jfree.chart.renderer.OutlierListCollection;
108    import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
109    import org.jfree.data.xy.XYDataset;
110    import org.jfree.io.SerialUtilities;
111    import org.jfree.ui.RectangleEdge;
112    import org.jfree.util.PaintUtilities;
113    import org.jfree.util.PublicCloneable;
114    
115    /**
116     * A renderer that draws box-and-whisker items on an {@link XYPlot}.  This 
117     * renderer requires a {@link BoxAndWhiskerXYDataset}).
118     * <P>
119     * This renderer does not include any code to calculate the crosshair point.
120     */
121    public class XYBoxAndWhiskerRenderer extends AbstractXYItemRenderer 
122                                         implements XYItemRenderer, 
123                                                    Cloneable,
124                                                    PublicCloneable,
125                                                    Serializable {
126    
127        /** For serialization. */
128        private static final long serialVersionUID = -8020170108532232324L;
129        
130        /** The box width. */
131        private double boxWidth;
132    
133        /** The paint used to fill the box. */
134        private transient Paint boxPaint;
135    
136        /** A flag that controls whether or not the box is filled. */
137        private boolean fillBox;
138        
139        /** 
140         * The paint used to draw various artifacts such as outliers, farout 
141         * symbol, average ellipse and median line. 
142         */
143        private transient Paint artifactPaint = Color.black;
144    
145        /**
146         * Creates a new renderer for box and whisker charts.
147         */
148        public XYBoxAndWhiskerRenderer() {
149            this(-1.0);
150        }
151    
152        /**
153         * Creates a new renderer for box and whisker charts.
154         * <P>
155         * Use -1 for the box width if you prefer the width to be calculated 
156         * automatically.
157         *
158         * @param boxWidth  the box width.
159         */
160        public XYBoxAndWhiskerRenderer(double boxWidth) {
161            super();
162            this.boxWidth = boxWidth;
163            this.boxPaint = Color.green;
164            this.fillBox = true;
165            setToolTipGenerator(new BoxAndWhiskerXYToolTipGenerator());
166        }
167    
168        /**
169         * Returns the width of each box.
170         *
171         * @return The box width.
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        public void setBoxWidth(double width) {
187            if (width != this.boxWidth) {
188                this.boxWidth = width;
189                notifyListeners(new RendererChangeEvent(this));
190            }
191        }
192    
193        /**
194         * Returns the paint used to fill boxes.
195         *
196         * @return The paint (possibly <code>null</code>).
197         */
198        public Paint getBoxPaint() {
199            return this.boxPaint;
200        }
201    
202        /**
203         * Sets the paint used to fill boxes and sends a {@link RendererChangeEvent}
204         * to all registered listeners.
205         *
206         * @param paint  the paint (<code>null</code> permitted).
207         */
208        public void setBoxPaint(Paint paint) {
209            this.boxPaint = paint;
210            notifyListeners(new RendererChangeEvent(this));
211        }
212        
213        /**
214         * Returns the flag that controls whether or not the box is filled.
215         * 
216         * @return A boolean.
217         */
218        public boolean getFillBox() {
219            return this.fillBox;   
220        }
221        
222        /**
223         * Sets the flag that controls whether or not the box is filled and sends a 
224         * {@link RendererChangeEvent} to all registered listeners.
225         * 
226         * @param flag  the flag.
227         */
228        public void setFillBox(boolean flag) {
229            this.fillBox = flag;
230            notifyListeners(new RendererChangeEvent(this));
231        }
232    
233        /**
234         * Returns the paint used to paint the various artifacts such as outliers, 
235         * farout symbol, median line and the averages ellipse.
236         *
237         * @return The paint (never <code>null</code>).
238         */
239        public Paint getArtifactPaint() {
240            return this.artifactPaint;
241        }
242    
243        /**
244         * Sets the paint used to paint the various artifacts such as outliers, 
245         * farout symbol, median line and the averages ellipse.
246         * 
247         * @param artifactPaint  the paint (<code>null</code> not permitted).
248         */
249        public void setArtifactPaint(Paint artifactPaint) {
250            if (artifactPaint == null) {
251                throw new IllegalArgumentException(
252                        "Null 'artifactPaint' argument.");
253            }
254            this.artifactPaint = artifactPaint;
255            notifyListeners(new RendererChangeEvent(this));
256        }
257    
258        /**
259         * Draws the visual representation of a single data item.
260         *
261         * @param g2  the graphics device.
262         * @param state  the renderer state.
263         * @param dataArea  the area within which the plot is being drawn.
264         * @param info  collects info about the drawing.
265         * @param plot  the plot (can be used to obtain standard color 
266         *              information etc).
267         * @param domainAxis  the domain axis.
268         * @param rangeAxis  the range axis.
269         * @param dataset  the dataset.
270         * @param series  the series index (zero-based).
271         * @param item  the item index (zero-based).
272         * @param crosshairState  crosshair information for the plot 
273         *                        (<code>null</code> permitted).
274         * @param pass  the pass index.
275         */
276        public void drawItem(Graphics2D g2, 
277                             XYItemRendererState state,
278                             Rectangle2D dataArea,
279                             PlotRenderingInfo info,
280                             XYPlot plot, 
281                             ValueAxis domainAxis, 
282                             ValueAxis rangeAxis,
283                             XYDataset dataset, 
284                             int series, 
285                             int item,
286                             CrosshairState crosshairState,
287                             int pass) {
288    
289            PlotOrientation orientation = plot.getOrientation();
290    
291            if (orientation == PlotOrientation.HORIZONTAL) {
292                drawHorizontalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
293                        dataset, series, item, crosshairState, pass);
294            }
295            else if (orientation == PlotOrientation.VERTICAL) {
296                drawVerticalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
297                        dataset, series, item, crosshairState, pass);
298            }
299    
300        }
301    
302        /**
303         * Draws the visual representation of a single data item.
304         *
305         * @param g2  the graphics device.
306         * @param dataArea  the area within which the plot is being drawn.
307         * @param info  collects info about the drawing.
308         * @param plot  the plot (can be used to obtain standard color 
309         *              information etc).
310         * @param domainAxis  the domain axis.
311         * @param rangeAxis  the range axis.
312         * @param dataset  the dataset.
313         * @param series  the series index (zero-based).
314         * @param item  the item index (zero-based).
315         * @param crosshairState  crosshair information for the plot 
316         *                        (<code>null</code> permitted).
317         * @param pass  the pass index.
318         */
319        public void drawHorizontalItem(Graphics2D g2, 
320                                       Rectangle2D dataArea,
321                                       PlotRenderingInfo info,
322                                       XYPlot plot, 
323                                       ValueAxis domainAxis, 
324                                       ValueAxis rangeAxis,
325                                       XYDataset dataset, 
326                                       int series, 
327                                       int item,
328                                       CrosshairState crosshairState,
329                                       int pass) {
330    
331            // setup for collecting optional entity info...
332            EntityCollection entities = null;
333            if (info != null) {
334                entities = info.getOwner().getEntityCollection();
335            }
336    
337            BoxAndWhiskerXYDataset boxAndWhiskerData 
338                = (BoxAndWhiskerXYDataset) dataset;
339    
340            Number x = boxAndWhiskerData.getX(series, item);
341            Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
342            Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
343            Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
344            Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
345            Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
346            Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
347            
348            double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 
349                    plot.getDomainAxisEdge());
350    
351            RectangleEdge location = plot.getRangeAxisEdge();
352            double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 
353                    location);
354            double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 
355                    location);
356            double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 
357                    dataArea, location);
358            double yyAverage = 0.0;
359            if (yAverage != null) {
360                yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 
361                        dataArea, location);
362            }
363            double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 
364                    dataArea, location);
365            double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 
366                    dataArea, location);
367            
368            double exactBoxWidth = getBoxWidth();
369            double width = exactBoxWidth;
370            double dataAreaX = dataArea.getHeight();
371            double maxBoxPercent = 0.1;
372            double maxBoxWidth = dataAreaX * maxBoxPercent;
373            if (exactBoxWidth <= 0.0) {
374                int itemCount = boxAndWhiskerData.getItemCount(series);
375                exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
376                if (exactBoxWidth < 3) {
377                    width = 3;
378                }
379                else if (exactBoxWidth > maxBoxWidth) {
380                    width = maxBoxWidth;
381                }
382                else {
383                    width = exactBoxWidth;
384                }
385            }
386    
387            Paint p = getBoxPaint();
388            if (p != null) {
389                g2.setPaint(p);
390            }
391            Stroke s = getItemStroke(series, item);
392            g2.setStroke(s);
393    
394            // draw the upper shadow
395            g2.draw(new Line2D.Double(yyMax, xx, yyQ3Median, xx));
396            g2.draw(new Line2D.Double(yyMax, xx - width / 2, yyMax, 
397                    xx + width / 2));
398    
399            // draw the lower shadow
400            g2.draw(new Line2D.Double(yyMin, xx, yyQ1Median, xx));
401            g2.draw(new Line2D.Double(yyMin, xx - width / 2, yyMin, 
402                    xx + width / 2));
403    
404            // draw the body
405            Shape box = null;
406            if (yyQ1Median < yyQ3Median) {
407                box = new Rectangle2D.Double(yyQ1Median, xx - width / 2, 
408                        yyQ3Median - yyQ1Median, width);
409            }
410            else {
411                box = new Rectangle2D.Double(yyQ3Median, xx - width / 2, 
412                        yyQ1Median - yyQ3Median, width);
413            }
414            if (getBoxPaint() != null) {
415                g2.setPaint(getBoxPaint());
416            }
417            if (this.fillBox) {
418                g2.fill(box);   
419            }
420            g2.draw(box);
421    
422            // draw median
423            g2.setPaint(getArtifactPaint());
424            g2.draw(new Line2D.Double(yyMedian, 
425                    xx - width / 2, yyMedian, xx + width / 2));
426            
427            // draw average - SPECIAL AIMS REQUIREMENT
428            if (yAverage != null) {
429                double aRadius = width / 4;
430                Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
431                        yyAverage - aRadius, xx - aRadius, aRadius * 2, 
432                        aRadius * 2);
433                g2.fill(avgEllipse);
434                g2.draw(avgEllipse);
435            }
436            
437            // FIXME: draw outliers
438            
439            // add an entity for the item...
440            if (entities != null) {
441                String tip = null;
442                XYToolTipGenerator generator = getToolTipGenerator(series, item);
443                if (generator != null) {
444                    tip = generator.generateToolTip(dataset, series, item);
445                }
446                String url = null;
447                if (getURLGenerator() != null) {
448                    url = getURLGenerator().generateURL(dataset, series, item);
449                }
450                XYItemEntity entity = new XYItemEntity(box, dataset, series, item, 
451                        tip, url);
452                entities.add(entity);
453            }
454    
455        }
456    
457        /**
458         * Draws the visual representation of a single data item.
459         *
460         * @param g2  the graphics device.
461         * @param dataArea  the area within which the plot is being drawn.
462         * @param info  collects info about the drawing.
463         * @param plot  the plot (can be used to obtain standard color 
464         *              information etc).
465         * @param domainAxis  the domain axis.
466         * @param rangeAxis  the range axis.
467         * @param dataset  the dataset.
468         * @param series  the series index (zero-based).
469         * @param item  the item index (zero-based).
470         * @param crosshairState  crosshair information for the plot 
471         *                        (<code>null</code> permitted).
472         * @param pass  the pass index.
473         */
474        public void drawVerticalItem(Graphics2D g2, 
475                                     Rectangle2D dataArea,
476                                     PlotRenderingInfo info,
477                                     XYPlot plot, 
478                                     ValueAxis domainAxis, 
479                                     ValueAxis rangeAxis,
480                                     XYDataset dataset, 
481                                     int series, 
482                                     int item,
483                                     CrosshairState crosshairState,
484                                     int pass) {
485    
486            // setup for collecting optional entity info...
487            EntityCollection entities = null;
488            if (info != null) {
489                entities = info.getOwner().getEntityCollection();
490            }
491    
492            BoxAndWhiskerXYDataset boxAndWhiskerData 
493                = (BoxAndWhiskerXYDataset) dataset;
494    
495            Number x = boxAndWhiskerData.getX(series, item);
496            Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
497            Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
498            Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
499            Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
500            Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
501            Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
502            List yOutliers = boxAndWhiskerData.getOutliers(series, item);
503    
504            double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 
505                    plot.getDomainAxisEdge());
506    
507            RectangleEdge location = plot.getRangeAxisEdge();
508            double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 
509                    location);
510            double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 
511                    location);
512            double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 
513                    dataArea, location);
514            double yyAverage = 0.0;
515            if (yAverage != null) {
516                yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 
517                        dataArea, location);
518            }
519            double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 
520                    dataArea, location);
521            double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 
522                    dataArea, location);
523            double yyOutlier;
524    
525    
526            double exactBoxWidth = getBoxWidth();
527            double width = exactBoxWidth;
528            double dataAreaX = dataArea.getMaxX() - dataArea.getMinX();
529            double maxBoxPercent = 0.1;
530            double maxBoxWidth = dataAreaX * maxBoxPercent;
531            if (exactBoxWidth <= 0.0) {
532                int itemCount = boxAndWhiskerData.getItemCount(series);
533                exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
534                if (exactBoxWidth < 3) {
535                    width = 3;
536                } 
537                else if (exactBoxWidth > maxBoxWidth) {
538                    width = maxBoxWidth;
539                } 
540                else {
541                    width = exactBoxWidth;
542                }
543            }
544    
545            Paint p = getBoxPaint();
546            if (p != null) {
547                g2.setPaint(p);
548            }
549            Stroke s = getItemStroke(series, item);
550    
551            g2.setStroke(s);
552    
553            // draw the upper shadow
554            g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median));
555            g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2, 
556                    yyMax));
557    
558            // draw the lower shadow
559            g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median));
560            g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2, 
561                    yyMin));
562            
563            // draw the body
564            Shape box = null;
565            if (yyQ1Median > yyQ3Median) {
566                box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width, 
567                        yyQ1Median - yyQ3Median);
568            }
569            else {
570                box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width, 
571                        yyQ3Median - yyQ1Median);
572            }
573            if (this.fillBox) {
574                g2.fill(box);   
575            }
576            g2.draw(box);
577    
578            // draw median
579            g2.setPaint(getArtifactPaint());
580            g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2, 
581                    yyMedian));
582    
583            double aRadius = 0;                 // average radius
584            double oRadius = width / 3;    // outlier radius
585    
586            // draw average - SPECIAL AIMS REQUIREMENT
587            if (yAverage != null) {
588                aRadius = width / 4;
589                Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius, 
590                        yyAverage - aRadius, aRadius * 2, aRadius * 2);
591                g2.fill(avgEllipse);
592                g2.draw(avgEllipse);
593            }
594    
595            List outliers = new ArrayList();
596            OutlierListCollection outlierListCollection 
597                = new OutlierListCollection();
598    
599            /* From outlier array sort out which are outliers and put these into 
600             * an arraylist. If there are any farouts, set the flag on the 
601             * OutlierListCollection
602             */
603    
604            for (int i = 0; i < yOutliers.size(); i++) {
605                double outlier = ((Number) yOutliers.get(i)).doubleValue();
606                if (outlier > boxAndWhiskerData.getMaxOutlier(series, 
607                        item).doubleValue()) {
608                    outlierListCollection.setHighFarOut(true);
609                } 
610                else if (outlier < boxAndWhiskerData.getMinOutlier(series, 
611                        item).doubleValue()) {
612                    outlierListCollection.setLowFarOut(true);
613                } 
614                else if (outlier > boxAndWhiskerData.getMaxRegularValue(series, 
615                        item).doubleValue()) {
616                    yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 
617                            location);
618                    outliers.add(new Outlier(xx, yyOutlier, oRadius));
619                }
620                else if (outlier < boxAndWhiskerData.getMinRegularValue(series, 
621                        item).doubleValue()) {
622                    yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 
623                            location);
624                    outliers.add(new Outlier(xx, yyOutlier, oRadius));
625                }
626                Collections.sort(outliers);
627            }
628    
629            // Process outliers. Each outlier is either added to the appropriate 
630            // outlier list or a new outlier list is made
631            for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
632                Outlier outlier = (Outlier) iterator.next();
633                outlierListCollection.add(outlier);
634            }
635    
636            // draw yOutliers
637            double maxAxisValue = rangeAxis.valueToJava2D(
638                rangeAxis.getUpperBound(), dataArea, location
639            ) + aRadius;
640            double minAxisValue = rangeAxis.valueToJava2D(
641                rangeAxis.getLowerBound(), dataArea, location
642            ) - 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) {
670                String tip = null;
671                XYToolTipGenerator generator = getToolTipGenerator(series, item);
672                if (generator != null) {
673                    tip = generator.generateToolTip(dataset, series, item);
674                }
675                String url = null;
676                if (getURLGenerator() != null) {
677                    url = getURLGenerator().generateURL(dataset, series, item);
678                }
679                XYItemEntity entity = new XYItemEntity(box, dataset, series, item, 
680                        tip, url);
681                entities.add(entity);
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    }