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