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