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