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