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