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