001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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-2014, 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
079package org.jfree.chart.renderer.xy;
080
081import java.awt.Color;
082import java.awt.Graphics2D;
083import java.awt.Paint;
084import java.awt.Shape;
085import java.awt.Stroke;
086import java.awt.geom.Ellipse2D;
087import java.awt.geom.Line2D;
088import java.awt.geom.Point2D;
089import java.awt.geom.Rectangle2D;
090import java.io.IOException;
091import java.io.ObjectInputStream;
092import java.io.ObjectOutputStream;
093import java.io.Serializable;
094import java.util.ArrayList;
095import java.util.Collections;
096import java.util.Iterator;
097import java.util.List;
098
099import org.jfree.chart.axis.ValueAxis;
100import org.jfree.chart.entity.EntityCollection;
101import org.jfree.chart.event.RendererChangeEvent;
102import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator;
103import org.jfree.chart.plot.CrosshairState;
104import org.jfree.chart.plot.PlotOrientation;
105import org.jfree.chart.plot.PlotRenderingInfo;
106import org.jfree.chart.plot.XYPlot;
107import org.jfree.chart.renderer.Outlier;
108import org.jfree.chart.renderer.OutlierList;
109import org.jfree.chart.renderer.OutlierListCollection;
110import org.jfree.chart.util.ParamChecks;
111import org.jfree.data.Range;
112import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
113import org.jfree.data.xy.XYDataset;
114import org.jfree.io.SerialUtilities;
115import org.jfree.ui.RectangleEdge;
116import org.jfree.util.PaintUtilities;
117import 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 */
130public 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        ParamChecks.nullNotPermitted(paint, "paint");
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    @Override
290    public Range findRangeBounds(XYDataset dataset) {
291        return findRangeBounds(dataset, true);
292    }
293
294    /**
295     * Returns the box paint or, if this is <code>null</code>, the item
296     * paint.
297     *
298     * @param series  the series index.
299     * @param item  the item index.
300     *
301     * @return The paint used to fill the box for the specified item (never
302     *         <code>null</code>).
303     *
304     * @since 1.0.10
305     */
306    protected Paint lookupBoxPaint(int series, int item) {
307        Paint p = getBoxPaint();
308        if (p != null) {
309            return p;
310        }
311        else {
312            // TODO: could change this to itemFillPaint().  For backwards
313            // compatibility, it might require a useFillPaint flag.
314            return getItemPaint(series, item);
315        }
316    }
317
318    /**
319     * Draws the visual representation of a single data item.
320     *
321     * @param g2  the graphics device.
322     * @param state  the renderer state.
323     * @param dataArea  the area within which the plot is being drawn.
324     * @param info  collects info about the drawing.
325     * @param plot  the plot (can be used to obtain standard color
326     *              information etc).
327     * @param domainAxis  the domain axis.
328     * @param rangeAxis  the range axis.
329     * @param dataset  the dataset (must be an instance of
330     *                 {@link BoxAndWhiskerXYDataset}).
331     * @param series  the series index (zero-based).
332     * @param item  the item index (zero-based).
333     * @param crosshairState  crosshair information for the plot
334     *                        (<code>null</code> permitted).
335     * @param pass  the pass index.
336     */
337    @Override
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;
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;
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    @Override
786    public boolean equals(Object obj) {
787        if (obj == this) {
788            return true;
789        }
790        if (!(obj instanceof XYBoxAndWhiskerRenderer)) {
791            return false;
792        }
793        if (!super.equals(obj)) {
794            return false;
795        }
796        XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj;
797        if (this.boxWidth != that.getBoxWidth()) {
798            return false;
799        }
800        if (!PaintUtilities.equal(this.boxPaint, that.boxPaint)) {
801            return false;
802        }
803        if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) {
804            return false;
805        }
806        if (this.fillBox != that.fillBox) {
807            return false;
808        }
809        return true;
810
811    }
812
813    /**
814     * Provides serialization support.
815     *
816     * @param stream  the output stream.
817     *
818     * @throws IOException  if there is an I/O error.
819     */
820    private void writeObject(ObjectOutputStream stream) throws IOException {
821        stream.defaultWriteObject();
822        SerialUtilities.writePaint(this.boxPaint, stream);
823        SerialUtilities.writePaint(this.artifactPaint, stream);
824    }
825
826    /**
827     * Provides serialization support.
828     *
829     * @param stream  the input stream.
830     *
831     * @throws IOException  if there is an I/O error.
832     * @throws ClassNotFoundException  if there is a classpath problem.
833     */
834    private void readObject(ObjectInputStream stream)
835        throws IOException, ClassNotFoundException {
836
837        stream.defaultReadObject();
838        this.boxPaint = SerialUtilities.readPaint(stream);
839        this.artifactPaint = SerialUtilities.readPaint(stream);
840    }
841
842    /**
843     * Returns a clone of the renderer.
844     *
845     * @return A clone.
846     *
847     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
848     */
849    @Override
850    public Object clone() throws CloneNotSupportedException {
851        return super.clone();
852    }
853
854}