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