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