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