    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
    077    package org.jfree.chart.renderer.xy;
    078
    079    import java.awt.Color;
    080    import java.awt.Graphics2D;
    081    import java.awt.Paint;
    082    import java.awt.Shape;
    083    import java.awt.Stroke;
    084    import java.awt.geom.Ellipse2D;
    085    import java.awt.geom.Line2D;
    086    import java.awt.geom.Point2D;
    087    import java.awt.geom.Rectangle2D;
    088    import java.io.IOException;
    089    import java.io.ObjectInputStream;
    090    import java.io.ObjectOutputStream;
    091    import java.io.Serializable;
    092    import java.util.ArrayList;
    093    import java.util.Collections;
    094    import java.util.Iterator;
    095    import java.util.List;
    096
    097    import org.jfree.chart.axis.ValueAxis;
    098    import org.jfree.chart.entity.EntityCollection;
    099    import org.jfree.chart.event.RendererChangeEvent;
    100    import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator;
    101    import org.jfree.chart.plot.CrosshairState;
    102    import org.jfree.chart.plot.PlotOrientation;
    103    import org.jfree.chart.plot.PlotRenderingInfo;
    104    import org.jfree.chart.plot.XYPlot;
    105    import org.jfree.chart.renderer.Outlier;
    106    import org.jfree.chart.renderer.OutlierList;
    107    import org.jfree.chart.renderer.OutlierListCollection;
    108    import org.jfree.data.Range;
    109    import org.jfree.data.general.DatasetUtilities;
    110    import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
    111    import org.jfree.data.xy.XYDataset;
    112    import org.jfree.io.SerialUtilities;
    113    import org.jfree.ui.RectangleEdge;
    114    import org.jfree.util.PaintUtilities;
    115    import 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     */
    128    public 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    }