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    }