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