    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}