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
079package org.jfree.chart.renderer.category;
080
081import java.awt.Color;
082import java.awt.Graphics2D;
083import java.awt.Paint;
084import java.awt.Shape;
085import java.awt.Stroke;
086import java.awt.geom.Ellipse2D;
087import java.awt.geom.Line2D;
088import java.awt.geom.Point2D;
089import java.awt.geom.Rectangle2D;
090import java.io.IOException;
091import java.io.ObjectInputStream;
092import java.io.ObjectOutputStream;
093import java.io.Serializable;
094import java.util.ArrayList;
095import java.util.Collections;
096import java.util.Iterator;
097import java.util.List;
098
099import org.jfree.chart.LegendItem;
100import org.jfree.chart.axis.CategoryAxis;
101import org.jfree.chart.axis.ValueAxis;
102import org.jfree.chart.entity.EntityCollection;
103import org.jfree.chart.event.RendererChangeEvent;
104import org.jfree.chart.plot.CategoryPlot;
105import org.jfree.chart.plot.PlotOrientation;
106import org.jfree.chart.plot.PlotRenderingInfo;
107import org.jfree.chart.renderer.Outlier;
108import org.jfree.chart.renderer.OutlierList;
109import org.jfree.chart.renderer.OutlierListCollection;
110import org.jfree.data.Range;
111import org.jfree.data.category.CategoryDataset;
112import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset;
113import org.jfree.io.SerialUtilities;
114import org.jfree.ui.RectangleEdge;
115import org.jfree.util.PaintUtilities;
116import 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 */
128public 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}