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