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