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