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