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