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