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