    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     * BoxAndWhiskerRenderer.java
    029     * --------------------------
    030     * (C) Copyright 2003-2008, by David Browning and Contributors.
    031     *
    032     * Original Author:  David Browning (for the Australian Institute of Marine
    033     *                   Science);
    034     * Contributor(s):   David Gilbert (for Object Refinery Limited);
    035     *                   Tim Bardzil;
    036     *                   Rob Van der Sanden (patches 1866446 and 1888422);
    037     *
    038     * Changes
    039     * -------
    040     * 21-Aug-2003 : Version 1, contributed by David Browning (for the Australian
    041     *               Institute of Marine Science);
    042     * 01-Sep-2003 : Incorporated outlier and farout symbols for low values
    043     *               also (DG);
    044     * 08-Sep-2003 : Changed ValueAxis API (DG);
    045     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
    046     * 07-Oct-2003 : Added renderer state (DG);
    047     * 12-Nov-2003 : Fixed casting bug reported by Tim Bardzil (DG);
    048     * 13-Nov-2003 : Added drawHorizontalItem() method contributed by Tim
    049     *               Bardzil (DG);
    050     * 25-Apr-2004 : Added fillBox attribute, equals() method and added
    051     *               serialization code (DG);
    052     * 29-Apr-2004 : Changed drawing of upper and lower shadows - see bug report
    053     *               944011 (DG);
    054     * 05-Nov-2004 : Modified drawItem() signature (DG);
    055     * 09-Mar-2005 : Override getLegendItem() method so that legend item shapes
    056     *               are shown as blocks (DG);
    057     * 20-Apr-2005 : Generate legend labels, tooltips and URLs (DG);
    058     * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG);
    059     * ------------- JFREECHART 1.0.x ---------------------------------------------
    060     * 12-Oct-2006 : Source reformatting and API doc updates (DG);
    061     * 12-Oct-2006 : Fixed bug 1572478, potential NullPointerException (DG);
    062     * 05-Feb-2006 : Added event notifications to a couple of methods (DG);
    063     * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
    064     * 11-May-2007 : Added check for visibility in getLegendItem() (DG);
    065     * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG);
    066     * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG);
    067     * 03-Jan-2008 : Check visibility of average marker before drawing it (DG);
    068     * 15-Jan-2008 : Add getMaximumBarWidth() and setMaximumBarWidth()
    069     *               methods (RVdS);
    070     * 14-Feb-2008 : Fix bar position for horizontal chart, see patch
    071     *               1888422 (RVdS);
    072     * 27-Mar-2008 : Boxes should use outlinePaint/Stroke settings (DG);
    073     *
    074     */
    075
    076    package org.jfree.chart.renderer.category;
    077
    078    import java.awt.Color;
    079    import java.awt.Graphics2D;
    080    import java.awt.Paint;
    081    import java.awt.Shape;
    082    import java.awt.Stroke;
    083    import java.awt.geom.Ellipse2D;
    084    import java.awt.geom.Line2D;
    085    import java.awt.geom.Point2D;
    086    import java.awt.geom.Rectangle2D;
    087    import java.io.IOException;
    088    import java.io.ObjectInputStream;
    089    import java.io.ObjectOutputStream;
    090    import java.io.Serializable;
    091    import java.util.ArrayList;
    092    import java.util.Collections;
    093    import java.util.Iterator;
    094    import java.util.List;
    095
    096    import org.jfree.chart.LegendItem;
    097    import org.jfree.chart.axis.CategoryAxis;
    098    import org.jfree.chart.axis.ValueAxis;
    099    import org.jfree.chart.entity.EntityCollection;
    100    import org.jfree.chart.event.RendererChangeEvent;
    101    import org.jfree.chart.plot.CategoryPlot;
    102    import org.jfree.chart.plot.PlotOrientation;
    103    import org.jfree.chart.plot.PlotRenderingInfo;
    104    import org.jfree.chart.renderer.Outlier;
    105    import org.jfree.chart.renderer.OutlierList;
    106    import org.jfree.chart.renderer.OutlierListCollection;
    107    import org.jfree.data.category.CategoryDataset;
    108    import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset;
    109    import org.jfree.io.SerialUtilities;
    110    import org.jfree.ui.RectangleEdge;
    111    import org.jfree.util.PaintUtilities;
    112    import org.jfree.util.PublicCloneable;
    113
    114    /**
    115     * A box-and-whisker renderer.  This renderer requires a
    116     * {@link BoxAndWhiskerCategoryDataset} and is for use with the
    117     * {@link CategoryPlot} class.
    118     */
    119    public class BoxAndWhiskerRenderer extends AbstractCategoryItemRenderer
    120            implements Cloneable, PublicCloneable, Serializable {
    121
    122        /** For serialization. */
    123        private static final long serialVersionUID = 632027470694481177L;
    124
    125        /** The color used to paint the median line and average marker. */
    126        private transient Paint artifactPaint;
    127
    128        /** A flag that controls whether or not the box is filled. */
    129        private boolean fillBox;
    130
    131        /** The margin between items (boxes) within a category. */
    132        private double itemMargin;
    133
    134        /**
    135         * The maximum bar width as percentage of the available space in the plot,
    136         * where 0.05 is five percent.
    137         */
    138        private double maximumBarWidth;
    139
    140        /**
    141         * Default constructor.
    142         */
    143        public BoxAndWhiskerRenderer() {
    144            this.artifactPaint = Color.black;
    145            this.fillBox = true;
    146            this.itemMargin = 0.20;
    147            this.maximumBarWidth = 1.0;
    148        }
    149
    150        /**
    151         * Returns the paint used to color the median and average markers.
    152         *
    153         * @return The paint used to draw the median and average markers (never
    154         *     <code>null</code>).
    155         *
    156         * @see #setArtifactPaint(Paint)
    157         */
    158        public Paint getArtifactPaint() {
    159            return this.artifactPaint;
    160        }
    161
    162        /**
    163         * Sets the paint used to color the median and average markers and sends
    164         * a {@link RendererChangeEvent} to all registered listeners.
    165         *
    166         * @param paint  the paint (<code>null</code> not permitted).
    167         *
    168         * @see #getArtifactPaint()
    169         */
    170        public void setArtifactPaint(Paint paint) {
    171            if (paint == null) {
    172                throw new IllegalArgumentException("Null 'paint' argument.");
    173            }
    174            this.artifactPaint = paint;
    175            fireChangeEvent();
    176        }
    177
    178        /**
    179         * Returns the flag that controls whether or not the box is filled.
    180         *
    181         * @return A boolean.
    182         *
    183         * @see #setFillBox(boolean)
    184         */
    185        public boolean getFillBox() {
    186            return this.fillBox;
    187        }
    188
    189        /**
    190         * Sets the flag that controls whether or not the box is filled and sends a
    191         * {@link RendererChangeEvent} to all registered listeners.
    192         *
    193         * @param flag  the flag.
    194         *
    195         * @see #getFillBox()
    196         */
    197        public void setFillBox(boolean flag) {
    198            this.fillBox = flag;
    199            fireChangeEvent();
    200        }
    201
    202        /**
    203         * Returns the item margin.  This is a percentage of the available space
    204         * that is allocated to the space between items in the chart.
    205         *
    206         * @return The margin.
    207         *
    208         * @see #setItemMargin(double)
    209         */
    210        public double getItemMargin() {
    211            return this.itemMargin;
    212        }
    213
    214        /**
    215         * Sets the item margin and sends a {@link RendererChangeEvent} to all
    216         * registered listeners.
    217         *
    218         * @param margin  the margin (a percentage).
    219         *
    220         * @see #getItemMargin()
    221         */
    222        public void setItemMargin(double margin) {
    223            this.itemMargin = margin;
    224            fireChangeEvent();
    225        }
    226
    227        /**
    228         * Returns the maximum bar width as a percentage of the available drawing
    229         * space.
    230         *
    231         * @return The maximum bar width.
    232         *
    233         * @see #setMaximumBarWidth(double)
    234         *
    235         * @since 1.0.10
    236         */
    237        public double getMaximumBarWidth() {
    238            return this.maximumBarWidth;
    239        }
    240
    241        /**
    242         * Sets the maximum bar width, which is specified as a percentage of the
    243         * available space for all bars, and sends a {@link RendererChangeEvent}
    244         * to all registered listeners.
    245         *
    246         * @param percent  the maximum Bar Width (a percentage).
    247         *
    248         * @see #getMaximumBarWidth()
    249         *
    250         * @since 1.0.10
    251         */
    252        public void setMaximumBarWidth(double percent) {
    253            this.maximumBarWidth = percent;
    254            fireChangeEvent();
    255        }
    256
    257        /**
    258         * Returns a legend item for a series.
    259         *
    260         * @param datasetIndex  the dataset index (zero-based).
    261         * @param series  the series index (zero-based).
    262         *
    263         * @return The legend item (possibly <code>null</code>).
    264         */
    265        public LegendItem getLegendItem(int datasetIndex, int series) {
    266
    267            CategoryPlot cp = getPlot();
    268            if (cp == null) {
    269                return null;
    270            }
    271
    272            // check that a legend item needs to be displayed...
    273            if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) {
    274                return null;
    275            }
    276
    277            CategoryDataset dataset = cp.getDataset(datasetIndex);
    278            String label = getLegendItemLabelGenerator().generateLabel(dataset,
    279                    series);
    280            String description = label;
    281            String toolTipText = null;
    282            if (getLegendItemToolTipGenerator() != null) {
    283                toolTipText = getLegendItemToolTipGenerator().generateLabel(
    284                        dataset, series);
    285            }
    286            String urlText = null;
    287            if (getLegendItemURLGenerator() != null) {
    288                urlText = getLegendItemURLGenerator().generateLabel(dataset,
    289                        series);
    290            }
    291            Shape shape = new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0);
    292            Paint paint = lookupSeriesPaint(series);
    293            Paint outlinePaint = lookupSeriesOutlinePaint(series);
    294            Stroke outlineStroke = lookupSeriesOutlineStroke(series);
    295            LegendItem result = new LegendItem(label, description, toolTipText,
    296                    urlText, shape, paint, outlineStroke, outlinePaint);
    297            result.setDataset(dataset);
    298            result.setDatasetIndex(datasetIndex);
    299            result.setSeriesKey(dataset.getRowKey(series));
    300            result.setSeriesIndex(series);
    301            return result;
    302
    303        }
    304
    305        /**
    306         * Initialises the renderer.  This method gets called once at the start of
    307         * the process of drawing a chart.
    308         *
    309         * @param g2  the graphics device.
    310         * @param dataArea  the area in which the data is to be plotted.
    311         * @param plot  the plot.
    312         * @param rendererIndex  the renderer index.
    313         * @param info  collects chart rendering information for return to caller.
    314         *
    315         * @return The renderer state.
    316         */
    317        public CategoryItemRendererState initialise(Graphics2D g2,
    318                                                    Rectangle2D dataArea,
    319                                                    CategoryPlot plot,
    320                                                    int rendererIndex,
    321                                                    PlotRenderingInfo info) {
    322
    323            CategoryItemRendererState state = super.initialise(g2, dataArea, plot,
    324                    rendererIndex, info);
    325
    326            // calculate the box width
    327            CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex);
    328            CategoryDataset dataset = plot.getDataset(rendererIndex);
    329            if (dataset != null) {
    330                int columns = dataset.getColumnCount();
    331                int rows = dataset.getRowCount();
    332                double space = 0.0;
    333                PlotOrientation orientation = plot.getOrientation();
    334                if (orientation == PlotOrientation.HORIZONTAL) {
    335                    space = dataArea.getHeight();
    336                }
    337                else if (orientation == PlotOrientation.VERTICAL) {
    338                    space = dataArea.getWidth();
    339                }
    340                double maxWidth = space * getMaximumBarWidth();
    341                double categoryMargin = 0.0;
    342                double currentItemMargin = 0.0;
    343                if (columns > 1) {
    344                    categoryMargin = domainAxis.getCategoryMargin();
    345                }
    346                if (rows > 1) {
    347                    currentItemMargin = getItemMargin();
    348                }
    349                double used = space * (1 - domainAxis.getLowerMargin()
    350                                         - domainAxis.getUpperMargin()
    351                                         - categoryMargin - currentItemMargin);
    352                if ((rows * columns) > 0) {
    353                    state.setBarWidth(Math.min(used / (dataset.getColumnCount()
    354                            * dataset.getRowCount()), maxWidth));
    355                }
    356                else {
    357                    state.setBarWidth(Math.min(used, maxWidth));
    358                }
    359            }
    360
    361            return state;
    362
    363        }
    364
    365        /**
    366         * Draw a single data item.
    367         *
    368         * @param g2  the graphics device.
    369         * @param state  the renderer state.
    370         * @param dataArea  the area in which the data is drawn.
    371         * @param plot  the plot.
    372         * @param domainAxis  the domain axis.
    373         * @param rangeAxis  the range axis.
    374         * @param dataset  the data (must be an instance of
    375         *                 {@link BoxAndWhiskerCategoryDataset}).
    376         * @param row  the row index (zero-based).
    377         * @param column  the column index (zero-based).
    378         * @param pass  the pass index.
    379         */
    380        public void drawItem(Graphics2D g2,
    381                             CategoryItemRendererState state,
    382                             Rectangle2D dataArea,
    383                             CategoryPlot plot,
    384                             CategoryAxis domainAxis,
    385                             ValueAxis rangeAxis,
    386                             CategoryDataset dataset,
    387                             int row,
    388                             int column,
    389                             int pass) {
    390
    391            if (!(dataset instanceof BoxAndWhiskerCategoryDataset)) {
    392                throw new IllegalArgumentException(
    393                        "BoxAndWhiskerRenderer.drawItem() : the data should be "
    394                        + "of type BoxAndWhiskerCategoryDataset only.");
    395            }
    396
    397            PlotOrientation orientation = plot.getOrientation();
    398
    399            if (orientation == PlotOrientation.HORIZONTAL) {
    400                drawHorizontalItem(g2, state, dataArea, plot, domainAxis,
    401                        rangeAxis, dataset, row, column);
    402            }
    403            else if (orientation == PlotOrientation.VERTICAL) {
    404                drawVerticalItem(g2, state, dataArea, plot, domainAxis,
    405                        rangeAxis, dataset, row, column);
    406            }
    407
    408        }
    409
    410        /**
    411         * Draws the visual representation of a single data item when the plot has
    412         * a horizontal orientation.
    413         *
    414         * @param g2  the graphics device.
    415         * @param state  the renderer state.
    416         * @param dataArea  the area within which the plot is being drawn.
    417         * @param plot  the plot (can be used to obtain standard color
    418         *              information etc).
    419         * @param domainAxis  the domain axis.
    420         * @param rangeAxis  the range axis.
    421         * @param dataset  the dataset (must be an instance of
    422         *                 {@link BoxAndWhiskerCategoryDataset}).
    423         * @param row  the row index (zero-based).
    424         * @param column  the column index (zero-based).
    425         */
    426        public void drawHorizontalItem(Graphics2D g2,
    427                                       CategoryItemRendererState state,
    428                                       Rectangle2D dataArea,
    429                                       CategoryPlot plot,
    430                                       CategoryAxis domainAxis,
    431                                       ValueAxis rangeAxis,
    432                                       CategoryDataset dataset,
    433                                       int row,
    434                                       int column) {
    435
    436            BoxAndWhiskerCategoryDataset bawDataset
    437                    = (BoxAndWhiskerCategoryDataset) dataset;
    438
    439            double categoryEnd = domainAxis.getCategoryEnd(column,
    440                    getColumnCount(), dataArea, plot.getDomainAxisEdge());
    441            double categoryStart = domainAxis.getCategoryStart(column,
    442                    getColumnCount(), dataArea, plot.getDomainAxisEdge());
    443            double categoryWidth = Math.abs(categoryEnd - categoryStart);
    444
    445            double yy = categoryStart;
    446            int seriesCount = getRowCount();
    447            int categoryCount = getColumnCount();
    448
    449            if (seriesCount > 1) {
    450                double seriesGap = dataArea.getHeight() * getItemMargin()
    451                                   / (categoryCount * (seriesCount - 1));
    452                double usedWidth = (state.getBarWidth() * seriesCount)
    453                                   + (seriesGap * (seriesCount - 1));
    454                // offset the start of the boxes if the total width used is smaller
    455                // than the category width
    456                double offset = (categoryWidth - usedWidth) / 2;
    457                yy = yy + offset + (row * (state.getBarWidth() + seriesGap));
    458            }
    459            else {
    460                // offset the start of the box if the box width is smaller than
    461                // the category width
    462                double offset = (categoryWidth - state.getBarWidth()) / 2;
    463                yy = yy + offset;
    464            }
    465
    466            g2.setPaint(getItemPaint(row, column));
    467            Stroke s = getItemStroke(row, column);
    468            g2.setStroke(s);
    469
    470            RectangleEdge location = plot.getRangeAxisEdge();
    471
    472            Number xQ1 = bawDataset.getQ1Value(row, column);
    473            Number xQ3 = bawDataset.getQ3Value(row, column);
    474            Number xMax = bawDataset.getMaxRegularValue(row, column);
    475            Number xMin = bawDataset.getMinRegularValue(row, column);
    476
    477            Shape box = null;
    478            if (xQ1 != null && xQ3 != null && xMax != null && xMin != null) {
    479
    480                double xxQ1 = rangeAxis.valueToJava2D(xQ1.doubleValue(), dataArea,
    481                        location);
    482                double xxQ3 = rangeAxis.valueToJava2D(xQ3.doubleValue(), dataArea,
    483                        location);
    484                double xxMax = rangeAxis.valueToJava2D(xMax.doubleValue(), dataArea,
    485                        location);
    486                double xxMin = rangeAxis.valueToJava2D(xMin.doubleValue(), dataArea,
    487                        location);
    488                double yymid = yy + state.getBarWidth() / 2.0;
    489
    490                // draw the upper shadow...
    491                g2.draw(new Line2D.Double(xxMax, yymid, xxQ3, yymid));
    492                g2.draw(new Line2D.Double(xxMax, yy, xxMax,
    493                        yy + state.getBarWidth()));
    494
    495                // draw the lower shadow...
    496                g2.draw(new Line2D.Double(xxMin, yymid, xxQ1, yymid));
    497                g2.draw(new Line2D.Double(xxMin, yy, xxMin,
    498                        yy + state.getBarWidth()));
    499
    500                // draw the box...
    501                box = new Rectangle2D.Double(Math.min(xxQ1, xxQ3), yy,
    502                        Math.abs(xxQ1 - xxQ3), state.getBarWidth());
    503                if (this.fillBox) {
    504                    g2.fill(box);
    505                }
    506                g2.setStroke(getItemOutlineStroke(row, column));
    507                g2.setPaint(getItemOutlinePaint(row, column));
    508                g2.draw(box);
    509            }
    510
    511            g2.setPaint(this.artifactPaint);
    512            double aRadius = 0;                 // average radius
    513
    514            // draw mean - SPECIAL AIMS REQUIREMENT...
    515            Number xMean = bawDataset.getMeanValue(row, column);
    516            if (xMean != null) {
    517                double xxMean = rangeAxis.valueToJava2D(xMean.doubleValue(),
    518                        dataArea, location);
    519                aRadius = state.getBarWidth() / 4;
    520                // here we check that the average marker will in fact be visible
    521                // before drawing it...
    522                if ((xxMean > (dataArea.getMinX() - aRadius))
    523                        && (xxMean < (dataArea.getMaxX() + aRadius))) {
    524                    Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xxMean
    525                            - aRadius, yy + aRadius, aRadius * 2, aRadius * 2);
    526                    g2.fill(avgEllipse);
    527                    g2.draw(avgEllipse);
    528                }
    529            }
    530
    531            // draw median...
    532            Number xMedian = bawDataset.getMedianValue(row, column);
    533            if (xMedian != null) {
    534                double xxMedian = rangeAxis.valueToJava2D(xMedian.doubleValue(),
    535                        dataArea, location);
    536                g2.draw(new Line2D.Double(xxMedian, yy, xxMedian,
    537                        yy + state.getBarWidth()));
    538            }
    539
    540            // collect entity and tool tip information...
    541            if (state.getInfo() != null && box != null) {
    542                EntityCollection entities = state.getEntityCollection();
    543                if (entities != null) {
    544                    addItemEntity(entities, dataset, row, column, box);
    545                }
    546            }
    547
    548        }
    549
    550        /**
    551         * Draws the visual representation of a single data item when the plot has
    552         * a vertical orientation.
    553         *
    554         * @param g2  the graphics device.
    555         * @param state  the renderer state.
    556         * @param dataArea  the area within which the plot is being drawn.
    557         * @param plot  the plot (can be used to obtain standard color information
    558         *              etc).
    559         * @param domainAxis  the domain axis.
    560         * @param rangeAxis  the range axis.
    561         * @param dataset  the dataset (must be an instance of
    562         *                 {@link BoxAndWhiskerCategoryDataset}).
    563         * @param row  the row index (zero-based).
    564         * @param column  the column index (zero-based).
    565         */
    566        public void drawVerticalItem(Graphics2D g2,
    567                                     CategoryItemRendererState state,
    568                                     Rectangle2D dataArea,
    569                                     CategoryPlot plot,
    570                                     CategoryAxis domainAxis,
    571                                     ValueAxis rangeAxis,
    572                                     CategoryDataset dataset,
    573                                     int row,
    574                                     int column) {
    575
    576            BoxAndWhiskerCategoryDataset bawDataset
    577                    = (BoxAndWhiskerCategoryDataset) dataset;
    578
    579            double categoryEnd = domainAxis.getCategoryEnd(column,
    580                    getColumnCount(), dataArea, plot.getDomainAxisEdge());
    581            double categoryStart = domainAxis.getCategoryStart(column,
    582                    getColumnCount(), dataArea, plot.getDomainAxisEdge());
    583            double categoryWidth = categoryEnd - categoryStart;
    584
    585            double xx = categoryStart;
    586            int seriesCount = getRowCount();
    587            int categoryCount = getColumnCount();
    588
    589            if (seriesCount > 1) {
    590                double seriesGap = dataArea.getWidth() * getItemMargin()
    591                                   / (categoryCount * (seriesCount - 1));
    592                double usedWidth = (state.getBarWidth() * seriesCount)
    593                                   + (seriesGap * (seriesCount - 1));
    594                // offset the start of the boxes if the total width used is smaller
    595                // than the category width
    596                double offset = (categoryWidth - usedWidth) / 2;
    597                xx = xx + offset + (row * (state.getBarWidth() + seriesGap));
    598            }
    599            else {
    600                // offset the start of the box if the box width is smaller than the
    601                // category width
    602                double offset = (categoryWidth - state.getBarWidth()) / 2;
    603                xx = xx + offset;
    604            }
    605
    606            double yyAverage = 0.0;
    607            double yyOutlier;
    608
    609            Paint itemPaint = getItemPaint(row, column);
    610            g2.setPaint(itemPaint);
    611            Stroke s = getItemStroke(row, column);
    612            g2.setStroke(s);
    613
    614            double aRadius = 0;                 // average radius
    615
    616            RectangleEdge location = plot.getRangeAxisEdge();
    617
    618            Number yQ1 = bawDataset.getQ1Value(row, column);
    619            Number yQ3 = bawDataset.getQ3Value(row, column);
    620            Number yMax = bawDataset.getMaxRegularValue(row, column);
    621            Number yMin = bawDataset.getMinRegularValue(row, column);
    622            Shape box = null;
    623            if (yQ1 != null && yQ3 != null && yMax != null && yMin != null) {
    624
    625                double yyQ1 = rangeAxis.valueToJava2D(yQ1.doubleValue(), dataArea,
    626                        location);
    627                double yyQ3 = rangeAxis.valueToJava2D(yQ3.doubleValue(), dataArea,
    628                        location);
    629                double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(),
    630                        dataArea, location);
    631                double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(),
    632                        dataArea, location);
    633                double xxmid = xx + state.getBarWidth() / 2.0;
    634
    635                // draw the upper shadow...
    636                g2.draw(new Line2D.Double(xxmid, yyMax, xxmid, yyQ3));
    637                g2.draw(new Line2D.Double(xx, yyMax, xx + state.getBarWidth(),
    638                        yyMax));
    639
    640                // draw the lower shadow...
    641                g2.draw(new Line2D.Double(xxmid, yyMin, xxmid, yyQ1));
    642                g2.draw(new Line2D.Double(xx, yyMin, xx + state.getBarWidth(),
    643                        yyMin));
    644
    645                // draw the body...
    646                box = new Rectangle2D.Double(xx, Math.min(yyQ1, yyQ3),
    647                        state.getBarWidth(), Math.abs(yyQ1 - yyQ3));
    648                if (this.fillBox) {
    649                    g2.fill(box);
    650                }
    651                g2.setStroke(getItemOutlineStroke(row, column));
    652                g2.setPaint(getItemOutlinePaint(row, column));
    653                g2.draw(box);
    654            }
    655
    656            g2.setPaint(this.artifactPaint);
    657
    658            // draw mean - SPECIAL AIMS REQUIREMENT...
    659            Number yMean = bawDataset.getMeanValue(row, column);
    660            if (yMean != null) {
    661                yyAverage = rangeAxis.valueToJava2D(yMean.doubleValue(),
    662                        dataArea, location);
    663                aRadius = state.getBarWidth() / 4;
    664                // here we check that the average marker will in fact be visible
    665                // before drawing it...
    666                if ((yyAverage > (dataArea.getMinY() - aRadius))
    667                        && (yyAverage < (dataArea.getMaxY() + aRadius))) {
    668                    Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx + aRadius,
    669                            yyAverage - aRadius, aRadius * 2, aRadius * 2);
    670                    g2.fill(avgEllipse);
    671                    g2.draw(avgEllipse);
    672                }
    673            }
    674
    675            // draw median...
    676            Number yMedian = bawDataset.getMedianValue(row, column);
    677            if (yMedian != null) {
    678                double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
    679                        dataArea, location);
    680                g2.draw(new Line2D.Double(xx, yyMedian, xx + state.getBarWidth(),
    681                        yyMedian));
    682            }
    683
    684            // draw yOutliers...
    685            double maxAxisValue = rangeAxis.valueToJava2D(
    686                    rangeAxis.getUpperBound(), dataArea, location) + aRadius;
    687            double minAxisValue = rangeAxis.valueToJava2D(
    688                    rangeAxis.getLowerBound(), dataArea, location) - aRadius;
    689
    690            g2.setPaint(itemPaint);
    691
    692            // draw outliers
    693            double oRadius = state.getBarWidth() / 3;    // outlier radius
    694            List outliers = new ArrayList();
    695            OutlierListCollection outlierListCollection
    696                    = new OutlierListCollection();
    697
    698            // From outlier array sort out which are outliers and put these into a
    699            // list If there are any farouts, set the flag on the
    700            // OutlierListCollection
    701            List yOutliers = bawDataset.getOutliers(row, column);
    702            if (yOutliers != null) {
    703                for (int i = 0; i < yOutliers.size(); i++) {
    704                    double outlier = ((Number) yOutliers.get(i)).doubleValue();
    705                    Number minOutlier = bawDataset.getMinOutlier(row, column);
    706                    Number maxOutlier = bawDataset.getMaxOutlier(row, column);
    707                    Number minRegular = bawDataset.getMinRegularValue(row, column);
    708                    Number maxRegular = bawDataset.getMaxRegularValue(row, column);
    709                    if (outlier > maxOutlier.doubleValue()) {
    710                        outlierListCollection.setHighFarOut(true);
    711                    }
    712                    else if (outlier < minOutlier.doubleValue()) {
    713                        outlierListCollection.setLowFarOut(true);
    714                    }
    715                    else if (outlier > maxRegular.doubleValue()) {
    716                        yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
    717                                location);
    718                        outliers.add(new Outlier(xx + state.getBarWidth() / 2.0,
    719                                yyOutlier, oRadius));
    720                    }
    721                    else if (outlier < minRegular.doubleValue()) {
    722                        yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
    723                                location);
    724                        outliers.add(new Outlier(xx + state.getBarWidth() / 2.0,
    725                                yyOutlier, oRadius));
    726                    }
    727                    Collections.sort(outliers);
    728                }
    729
    730                // Process outliers. Each outlier is either added to the
    731                // appropriate outlier list or a new outlier list is made
    732                for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
    733                    Outlier outlier = (Outlier) iterator.next();
    734                    outlierListCollection.add(outlier);
    735                }
    736
    737                for (Iterator iterator = outlierListCollection.iterator();
    738                         iterator.hasNext();) {
    739                    OutlierList list = (OutlierList) iterator.next();
    740                    Outlier outlier = list.getAveragedOutlier();
    741                    Point2D point = outlier.getPoint();
    742
    743                    if (list.isMultiple()) {
    744                        drawMultipleEllipse(point, state.getBarWidth(), oRadius,
    745                                g2);
    746                    }
    747                    else {
    748                        drawEllipse(point, oRadius, g2);
    749                    }
    750                }
    751
    752                // draw farout indicators
    753                if (outlierListCollection.isHighFarOut()) {
    754                    drawHighFarOut(aRadius / 2.0, g2,
    755                            xx + state.getBarWidth() / 2.0, maxAxisValue);
    756                }
    757
    758                if (outlierListCollection.isLowFarOut()) {
    759                    drawLowFarOut(aRadius / 2.0, g2,
    760                            xx + state.getBarWidth() / 2.0, minAxisValue);
    761                }
    762            }
    763            // collect entity and tool tip information...
    764            if (state.getInfo() != null && box != null) {
    765                EntityCollection entities = state.getEntityCollection();
    766                if (entities != null) {
    767                    addItemEntity(entities, dataset, row, column, box);
    768                }
    769            }
    770
    771        }
    772
    773        /**
    774         * Draws a dot to represent an outlier.
    775         *
    776         * @param point  the location.
    777         * @param oRadius  the radius.
    778         * @param g2  the graphics device.
    779         */
    780        private void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
    781            Ellipse2D dot = new Ellipse2D.Double(point.getX() + oRadius / 2,
    782                    point.getY(), oRadius, oRadius);
    783            g2.draw(dot);
    784        }
    785
    786        /**
    787         * Draws two dots to represent the average value of more than one outlier.
    788         *
    789         * @param point  the location
    790         * @param boxWidth  the box width.
    791         * @param oRadius  the radius.
    792         * @param g2  the graphics device.
    793         */
    794        private void drawMultipleEllipse(Point2D point, double boxWidth,
    795                                         double oRadius, Graphics2D g2)  {
    796
    797            Ellipse2D dot1 = new Ellipse2D.Double(point.getX() - (boxWidth / 2)
    798                    + oRadius, point.getY(), oRadius, oRadius);
    799            Ellipse2D dot2 = new Ellipse2D.Double(point.getX() + (boxWidth / 2),
    800                    point.getY(), oRadius, oRadius);
    801            g2.draw(dot1);
    802            g2.draw(dot2);
    803        }
    804
    805        /**
    806         * Draws a triangle to indicate the presence of far-out values.
    807         *
    808         * @param aRadius  the radius.
    809         * @param g2  the graphics device.
    810         * @param xx  the x coordinate.
    811         * @param m  the y coordinate.
    812         */
    813        private void drawHighFarOut(double aRadius, Graphics2D g2, double xx,
    814                                    double m) {
    815            double side = aRadius * 2;
    816            g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
    817            g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
    818            g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
    819        }
    820
    821        /**
    822         * Draws a triangle to indicate the presence of far-out values.
    823         *
    824         * @param aRadius  the radius.
    825         * @param g2  the graphics device.
    826         * @param xx  the x coordinate.
    827         * @param m  the y coordinate.
    828         */
    829        private void drawLowFarOut(double aRadius, Graphics2D g2, double xx,
    830                                   double m) {
    831            double side = aRadius * 2;
    832            g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
    833            g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
    834            g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
    835        }
    836
    837        /**
    838         * Tests this renderer for equality with an arbitrary object.
    839         *
    840         * @param obj  the object (<code>null</code> permitted).
    841         *
    842         * @return <code>true</code> or <code>false</code>.
    843         */
    844        public boolean equals(Object obj) {
    845            if (obj == this) {
    846                return true;
    847            }
    848            if (!(obj instanceof BoxAndWhiskerRenderer)) {
    849                return false;
    850            }
    851            if (!super.equals(obj)) {
    852                return false;
    853            }
    854            BoxAndWhiskerRenderer that = (BoxAndWhiskerRenderer) obj;
    855            if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) {
    856                return false;
    857            }
    858            if (this.fillBox != that.fillBox) {
    859                return false;
    860            }
    861            if (this.itemMargin != that.itemMargin) {
    862                return false;
    863            }
    864            if (this.maximumBarWidth != that.maximumBarWidth) {
    865                return false;
    866            }
    867            return true;
    868        }
    869
    870        /**
    871         * Provides serialization support.
    872         *
    873         * @param stream  the output stream.
    874         *
    875         * @throws IOException  if there is an I/O error.
    876         */
    877        private void writeObject(ObjectOutputStream stream) throws IOException {
    878            stream.defaultWriteObject();
    879            SerialUtilities.writePaint(this.artifactPaint, stream);
    880        }
    881
    882        /**
    883         * Provides serialization support.
    884         *
    885         * @param stream  the input stream.
    886         *
    887         * @throws IOException  if there is an I/O error.
    888         * @throws ClassNotFoundException  if there is a classpath problem.
    889         */
    890        private void readObject(ObjectInputStream stream)
    891                throws IOException, ClassNotFoundException {
    892            stream.defaultReadObject();
    893            this.artifactPaint = SerialUtilities.readPaint(stream);
    894        }
    895
    896    }