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