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