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