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