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