    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2008, 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-2008, 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     *
    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.event.RendererChangeEvent;
    098    import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator;
    099    import org.jfree.chart.plot.CrosshairState;
    100    import org.jfree.chart.plot.PlotOrientation;
    101    import org.jfree.chart.plot.PlotRenderingInfo;
    102    import org.jfree.chart.plot.XYPlot;
    103    import org.jfree.chart.renderer.Outlier;
    104    import org.jfree.chart.renderer.OutlierList;
    105    import org.jfree.chart.renderer.OutlierListCollection;
    106    import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
    107    import org.jfree.data.xy.XYDataset;
    108    import org.jfree.io.SerialUtilities;
    109    import org.jfree.ui.RectangleEdge;
    110    import org.jfree.util.PaintUtilities;
    111    import org.jfree.util.PublicCloneable;
    112
    113    /**
    114     * A renderer that draws box-and-whisker items on an {@link XYPlot}.  This
    115     * renderer requires a {@link BoxAndWhiskerXYDataset}).
    116     * <P>
    117     * This renderer does not include any code to calculate the crosshair point.
    118     */
    119    public class XYBoxAndWhiskerRenderer extends AbstractXYItemRenderer
    120                                         implements XYItemRenderer,
    121                                                    Cloneable,
    122                                                    PublicCloneable,
    123                                                    Serializable {
    124
    125        /** For serialization. */
    126        private static final long serialVersionUID = -8020170108532232324L;
    127
    128        /** The box width. */
    129        private double boxWidth;
    130
    131        /** The paint used to fill the box. */
    132        private transient Paint boxPaint;
    133
    134        /** A flag that controls whether or not the box is filled. */
    135        private boolean fillBox;
    136
    137        /**
    138         * The paint used to draw various artifacts such as outliers, farout
    139         * symbol, average ellipse and median line.
    140         */
    141        private transient Paint artifactPaint = Color.black;
    142
    143        /**
    144         * Creates a new renderer for box and whisker charts.
    145         */
    146        public XYBoxAndWhiskerRenderer() {
    147            this(-1.0);
    148        }
    149
    150        /**
    151         * Creates a new renderer for box and whisker charts.
    152         * <P>
    153         * Use -1 for the box width if you prefer the width to be calculated
    154         * automatically.
    155         *
    156         * @param boxWidth  the box width.
    157         */
    158        public XYBoxAndWhiskerRenderer(double boxWidth) {
    159            super();
    160            this.boxWidth = boxWidth;
    161            this.boxPaint = Color.green;
    162            this.fillBox = true;
    163            setBaseToolTipGenerator(new BoxAndWhiskerXYToolTipGenerator());
    164        }
    165
    166        /**
    167         * Returns the width of each box.
    168         *
    169         * @return The box width.
    170         *
    171         * @see #setBoxWidth(double)
    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         * @see #getBoxWidth()
    187         */
    188        public void setBoxWidth(double width) {
    189            if (width != this.boxWidth) {
    190                this.boxWidth = width;
    191                fireChangeEvent();
    192            }
    193        }
    194
    195        /**
    196         * Returns the paint used to fill boxes.
    197         *
    198         * @return The paint (possibly <code>null</code>).
    199         *
    200         * @see #setBoxPaint(Paint)
    201         */
    202        public Paint getBoxPaint() {
    203            return this.boxPaint;
    204        }
    205
    206        /**
    207         * Sets the paint used to fill boxes and sends a {@link RendererChangeEvent}
    208         * to all registered listeners.
    209         *
    210         * @param paint  the paint (<code>null</code> permitted).
    211         *
    212         * @see #getBoxPaint()
    213         */
    214        public void setBoxPaint(Paint paint) {
    215            this.boxPaint = paint;
    216            fireChangeEvent();
    217        }
    218
    219        /**
    220         * Returns the flag that controls whether or not the box is filled.
    221         *
    222         * @return A boolean.
    223         *
    224         * @see #setFillBox(boolean)
    225         */
    226        public boolean getFillBox() {
    227            return this.fillBox;
    228        }
    229
    230        /**
    231         * Sets the flag that controls whether or not the box is filled and sends a
    232         * {@link RendererChangeEvent} to all registered listeners.
    233         *
    234         * @param flag  the flag.
    235         *
    236         * @see #setFillBox(boolean)
    237         */
    238        public void setFillBox(boolean flag) {
    239            this.fillBox = flag;
    240            fireChangeEvent();
    241        }
    242
    243        /**
    244         * Returns the paint used to paint the various artifacts such as outliers,
    245         * farout symbol, median line and the averages ellipse.
    246         *
    247         * @return The paint (never <code>null</code>).
    248         *
    249         * @see #setArtifactPaint(Paint)
    250         */
    251        public Paint getArtifactPaint() {
    252            return this.artifactPaint;
    253        }
    254
    255        /**
    256         * Sets the paint used to paint the various artifacts such as outliers,
    257         * farout symbol, median line and the averages ellipse, and sends a
    258         * {@link RendererChangeEvent} to all registered listeners.
    259         *
    260         * @param paint  the paint (<code>null</code> not permitted).
    261         *
    262         * @see #getArtifactPaint()
    263         */
    264        public void setArtifactPaint(Paint paint) {
    265            if (paint == null) {
    266                throw new IllegalArgumentException("Null 'paint' argument.");
    267            }
    268            this.artifactPaint = paint;
    269            fireChangeEvent();
    270        }
    271
    272        /**
    273         * Draws the visual representation of a single data item.
    274         *
    275         * @param g2  the graphics device.
    276         * @param state  the renderer state.
    277         * @param dataArea  the area within which the plot is being drawn.
    278         * @param info  collects info about the drawing.
    279         * @param plot  the plot (can be used to obtain standard color
    280         *              information etc).
    281         * @param domainAxis  the domain axis.
    282         * @param rangeAxis  the range axis.
    283         * @param dataset  the dataset.
    284         * @param series  the series index (zero-based).
    285         * @param item  the item index (zero-based).
    286         * @param crosshairState  crosshair information for the plot
    287         *                        (<code>null</code> permitted).
    288         * @param pass  the pass index.
    289         */
    290        public void drawItem(Graphics2D g2,
    291                             XYItemRendererState state,
    292                             Rectangle2D dataArea,
    293                             PlotRenderingInfo info,
    294                             XYPlot plot,
    295                             ValueAxis domainAxis,
    296                             ValueAxis rangeAxis,
    297                             XYDataset dataset,
    298                             int series,
    299                             int item,
    300                             CrosshairState crosshairState,
    301                             int pass) {
    302
    303            PlotOrientation orientation = plot.getOrientation();
    304
    305            if (orientation == PlotOrientation.HORIZONTAL) {
    306                drawHorizontalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
    307                        dataset, series, item, crosshairState, pass);
    308            }
    309            else if (orientation == PlotOrientation.VERTICAL) {
    310                drawVerticalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
    311                        dataset, series, item, crosshairState, pass);
    312            }
    313
    314        }
    315
    316        /**
    317         * Draws the visual representation of a single data item.
    318         *
    319         * @param g2  the graphics device.
    320         * @param dataArea  the area within which the plot is being drawn.
    321         * @param info  collects info about the drawing.
    322         * @param plot  the plot (can be used to obtain standard color
    323         *              information etc).
    324         * @param domainAxis  the domain axis.
    325         * @param rangeAxis  the range axis.
    326         * @param dataset  the dataset.
    327         * @param series  the series index (zero-based).
    328         * @param item  the item index (zero-based).
    329         * @param crosshairState  crosshair information for the plot
    330         *                        (<code>null</code> permitted).
    331         * @param pass  the pass index.
    332         */
    333        public void drawHorizontalItem(Graphics2D g2,
    334                                       Rectangle2D dataArea,
    335                                       PlotRenderingInfo info,
    336                                       XYPlot plot,
    337                                       ValueAxis domainAxis,
    338                                       ValueAxis rangeAxis,
    339                                       XYDataset dataset,
    340                                       int series,
    341                                       int item,
    342                                       CrosshairState crosshairState,
    343                                       int pass) {
    344
    345            // setup for collecting optional entity info...
    346            EntityCollection entities = null;
    347            if (info != null) {
    348                entities = info.getOwner().getEntityCollection();
    349            }
    350
    351            BoxAndWhiskerXYDataset boxAndWhiskerData
    352                    = (BoxAndWhiskerXYDataset) dataset;
    353
    354            Number x = boxAndWhiskerData.getX(series, item);
    355            Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
    356            Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
    357            Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
    358            Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
    359            Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
    360            Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
    361
    362            double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea,
    363                    plot.getDomainAxisEdge());
    364
    365            RectangleEdge location = plot.getRangeAxisEdge();
    366            double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea,
    367                    location);
    368            double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea,
    369                    location);
    370            double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
    371                    dataArea, location);
    372            double yyAverage = 0.0;
    373            if (yAverage != null) {
    374                yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(),
    375                        dataArea, location);
    376            }
    377            double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(),
    378                    dataArea, location);
    379            double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(),
    380                    dataArea, location);
    381
    382            double exactBoxWidth = getBoxWidth();
    383            double width = exactBoxWidth;
    384            double dataAreaX = dataArea.getHeight();
    385            double maxBoxPercent = 0.1;
    386            double maxBoxWidth = dataAreaX * maxBoxPercent;
    387            if (exactBoxWidth <= 0.0) {
    388                int itemCount = boxAndWhiskerData.getItemCount(series);
    389                exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
    390                if (exactBoxWidth < 3) {
    391                    width = 3;
    392                }
    393                else if (exactBoxWidth > maxBoxWidth) {
    394                    width = maxBoxWidth;
    395                }
    396                else {
    397                    width = exactBoxWidth;
    398                }
    399            }
    400
    401            Paint p = getBoxPaint();
    402            if (p != null) {
    403                g2.setPaint(p);
    404            }
    405            Stroke s = getItemStroke(series, item);
    406            g2.setStroke(s);
    407
    408            // draw the upper shadow
    409            g2.draw(new Line2D.Double(yyMax, xx, yyQ3Median, xx));
    410            g2.draw(new Line2D.Double(yyMax, xx - width / 2, yyMax,
    411                    xx + width / 2));
    412
    413            // draw the lower shadow
    414            g2.draw(new Line2D.Double(yyMin, xx, yyQ1Median, xx));
    415            g2.draw(new Line2D.Double(yyMin, xx - width / 2, yyMin,
    416                    xx + width / 2));
    417
    418            // draw the body
    419            Shape box = null;
    420            if (yyQ1Median < yyQ3Median) {
    421                box = new Rectangle2D.Double(yyQ1Median, xx - width / 2,
    422                        yyQ3Median - yyQ1Median, width);
    423            }
    424            else {
    425                box = new Rectangle2D.Double(yyQ3Median, xx - width / 2,
    426                        yyQ1Median - yyQ3Median, width);
    427            }
    428            if (getBoxPaint() != null) {
    429                g2.setPaint(getBoxPaint());
    430            }
    431            if (this.fillBox) {
    432                g2.fill(box);
    433            }
    434            g2.draw(box);
    435
    436            // draw median
    437            g2.setPaint(getArtifactPaint());
    438            g2.draw(new Line2D.Double(yyMedian,
    439                    xx - width / 2, yyMedian, xx + width / 2));
    440
    441            // draw average - SPECIAL AIMS REQUIREMENT
    442            if (yAverage != null) {
    443                double aRadius = width / 4;
    444                // here we check that the average marker will in fact be visible
    445                // before drawing it...
    446                if ((yyAverage > (dataArea.getMinX() - aRadius))
    447                        && (yyAverage < (dataArea.getMaxX() + aRadius))) {
    448                    Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
    449                            yyAverage - aRadius, xx - aRadius, aRadius * 2,
    450                            aRadius * 2);
    451                    g2.fill(avgEllipse);
    452                    g2.draw(avgEllipse);
    453                }
    454            }
    455
    456            // FIXME: draw outliers
    457
    458            // add an entity for the item...
    459            if (entities != null && box.intersects(dataArea)) {
    460                addEntity(entities, box, dataset, series, item, yyAverage, xx);
    461            }
    462
    463        }
    464
    465        /**
    466         * Draws the visual representation of a single data item.
    467         *
    468         * @param g2  the graphics device.
    469         * @param dataArea  the area within which the plot is being drawn.
    470         * @param info  collects info about the drawing.
    471         * @param plot  the plot (can be used to obtain standard color
    472         *              information etc).
    473         * @param domainAxis  the domain axis.
    474         * @param rangeAxis  the range axis.
    475         * @param dataset  the dataset.
    476         * @param series  the series index (zero-based).
    477         * @param item  the item index (zero-based).
    478         * @param crosshairState  crosshair information for the plot
    479         *                        (<code>null</code> permitted).
    480         * @param pass  the pass index.
    481         */
    482        public void drawVerticalItem(Graphics2D g2,
    483                                     Rectangle2D dataArea,
    484                                     PlotRenderingInfo info,
    485                                     XYPlot plot,
    486                                     ValueAxis domainAxis,
    487                                     ValueAxis rangeAxis,
    488                                     XYDataset dataset,
    489                                     int series,
    490                                     int item,
    491                                     CrosshairState crosshairState,
    492                                     int pass) {
    493
    494            // setup for collecting optional entity info...
    495            EntityCollection entities = null;
    496            if (info != null) {
    497                entities = info.getOwner().getEntityCollection();
    498            }
    499
    500            BoxAndWhiskerXYDataset boxAndWhiskerData
    501                = (BoxAndWhiskerXYDataset) dataset;
    502
    503            Number x = boxAndWhiskerData.getX(series, item);
    504            Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
    505            Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
    506            Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
    507            Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
    508            Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
    509            Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
    510            List yOutliers = boxAndWhiskerData.getOutliers(series, item);
    511
    512            double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea,
    513                    plot.getDomainAxisEdge());
    514
    515            RectangleEdge location = plot.getRangeAxisEdge();
    516            double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea,
    517                    location);
    518            double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea,
    519                    location);
    520            double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
    521                    dataArea, location);
    522            double yyAverage = 0.0;
    523            if (yAverage != null) {
    524                yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(),
    525                        dataArea, location);
    526            }
    527            double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(),
    528                    dataArea, location);
    529            double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(),
    530                    dataArea, location);
    531            double yyOutlier;
    532
    533
    534            double exactBoxWidth = getBoxWidth();
    535            double width = exactBoxWidth;
    536            double dataAreaX = dataArea.getMaxX() - dataArea.getMinX();
    537            double maxBoxPercent = 0.1;
    538            double maxBoxWidth = dataAreaX * maxBoxPercent;
    539            if (exactBoxWidth <= 0.0) {
    540                int itemCount = boxAndWhiskerData.getItemCount(series);
    541                exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
    542                if (exactBoxWidth < 3) {
    543                    width = 3;
    544                }
    545                else if (exactBoxWidth > maxBoxWidth) {
    546                    width = maxBoxWidth;
    547                }
    548                else {
    549                    width = exactBoxWidth;
    550                }
    551            }
    552
    553            Paint p = getBoxPaint();
    554            if (p != null) {
    555                g2.setPaint(p);
    556            }
    557            Stroke s = getItemStroke(series, item);
    558
    559            g2.setStroke(s);
    560
    561            // draw the upper shadow
    562            g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median));
    563            g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2,
    564                    yyMax));
    565
    566            // draw the lower shadow
    567            g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median));
    568            g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2,
    569                    yyMin));
    570
    571            // draw the body
    572            Shape box = null;
    573            if (yyQ1Median > yyQ3Median) {
    574                box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width,
    575                        yyQ1Median - yyQ3Median);
    576            }
    577            else {
    578                box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width,
    579                        yyQ3Median - yyQ1Median);
    580            }
    581            if (this.fillBox) {
    582                g2.fill(box);
    583            }
    584            g2.draw(box);
    585
    586            // draw median
    587            g2.setPaint(getArtifactPaint());
    588            g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2,
    589                    yyMedian));
    590
    591            double aRadius = 0;                 // average radius
    592            double oRadius = width / 3;    // outlier radius
    593
    594            // draw average - SPECIAL AIMS REQUIREMENT
    595            if (yAverage != null) {
    596                aRadius = width / 4;
    597                // here we check that the average marker will in fact be visible
    598                // before drawing it...
    599                if ((yyAverage > (dataArea.getMinY() - aRadius))
    600                        && (yyAverage < (dataArea.getMaxY() + aRadius))) {
    601                    Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius,
    602                            yyAverage - aRadius, aRadius * 2, aRadius * 2);
    603                    g2.fill(avgEllipse);
    604                    g2.draw(avgEllipse);
    605                }
    606            }
    607
    608            List outliers = new ArrayList();
    609            OutlierListCollection outlierListCollection
    610                    = new OutlierListCollection();
    611
    612            /* From outlier array sort out which are outliers and put these into
    613             * an arraylist. If there are any farouts, set the flag on the
    614             * OutlierListCollection
    615             */
    616
    617            for (int i = 0; i < yOutliers.size(); i++) {
    618                double outlier = ((Number) yOutliers.get(i)).doubleValue();
    619                if (outlier > boxAndWhiskerData.getMaxOutlier(series,
    620                        item).doubleValue()) {
    621                    outlierListCollection.setHighFarOut(true);
    622                }
    623                else if (outlier < boxAndWhiskerData.getMinOutlier(series,
    624                        item).doubleValue()) {
    625                    outlierListCollection.setLowFarOut(true);
    626                }
    627                else if (outlier > boxAndWhiskerData.getMaxRegularValue(series,
    628                        item).doubleValue()) {
    629                    yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
    630                            location);
    631                    outliers.add(new Outlier(xx, yyOutlier, oRadius));
    632                }
    633                else if (outlier < boxAndWhiskerData.getMinRegularValue(series,
    634                        item).doubleValue()) {
    635                    yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
    636                            location);
    637                    outliers.add(new Outlier(xx, yyOutlier, oRadius));
    638                }
    639                Collections.sort(outliers);
    640            }
    641
    642            // Process outliers. Each outlier is either added to the appropriate
    643            // outlier list or a new outlier list is made
    644            for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
    645                Outlier outlier = (Outlier) iterator.next();
    646                outlierListCollection.add(outlier);
    647            }
    648
    649            // draw yOutliers
    650            double maxAxisValue = rangeAxis.valueToJava2D(rangeAxis.getUpperBound(),
    651                    dataArea, location) + aRadius;
    652            double minAxisValue = rangeAxis.valueToJava2D(rangeAxis.getLowerBound(),
    653                    dataArea, location) - aRadius;
    654
    655            // draw outliers
    656            for (Iterator iterator = outlierListCollection.iterator();
    657                    iterator.hasNext();) {
    658                OutlierList list = (OutlierList) iterator.next();
    659                Outlier outlier = list.getAveragedOutlier();
    660                Point2D point = outlier.getPoint();
    661
    662                if (list.isMultiple()) {
    663                    drawMultipleEllipse(point, width, oRadius, g2);
    664                }
    665                else {
    666                    drawEllipse(point, oRadius, g2);
    667                }
    668            }
    669
    670            // draw farout
    671            if (outlierListCollection.isHighFarOut()) {
    672                drawHighFarOut(aRadius, g2, xx, maxAxisValue);
    673            }
    674
    675            if (outlierListCollection.isLowFarOut()) {
    676                drawLowFarOut(aRadius, g2, xx, minAxisValue);
    677            }
    678
    679            // add an entity for the item...
    680            if (entities != null && box.intersects(dataArea)) {
    681                addEntity(entities, box, dataset, series, item, xx, yyAverage);
    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    }