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