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