001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jfreechart/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it 
010     * under the terms of the GNU Lesser General Public License as published by 
011     * the Free Software Foundation; either version 2.1 of the License, or 
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but 
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022     * USA.  
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     *
027     * ----------------------------
028     * XYBoxAndWhiskerRenderer.java
029     * ----------------------------
030     * (C) Copyright 2003, 2004, by David Browning and Contributors.
031     *
032     * Original Author:  David Browning (for Australian Institute of Marine 
033     *                   Science);
034     * Contributor(s):   David Gilbert (for Object Refinery Limited);
035     *
036     * $Id: XYBoxAndWhiskerRenderer.java,v 1.6.2.3 2005/10/25 20:56:21 mungady Exp $
037     *
038     * Changes
039     * -------
040     * 05-Aug-2003 : Version 1, contributed by David Browning.  Based on code in the
041     *               CandlestickRenderer class.  Additional modifications by David 
042     *               Gilbert to make the code work with 0.9.10 changes (DG);
043     * 08-Aug-2003 : Updated some of the Javadoc
044     *               Allowed BoxAndwhiskerDataset Average value to be null - the 
045     *               average value is an AIMS requirement
046     *               Allow the outlier and farout coefficients to be set - though 
047     *               at the moment this only affects the calculation of farouts.
048     *               Added artifactPaint variable and setter/getter
049     * 12-Aug-2003   Rewrote code to sort out and process outliers to take 
050     *               advantage of changes in DefaultBoxAndWhiskerDataset
051     *               Added a limit of 10% for width of box should no width be 
052     *               specified...maybe this should be setable???
053     * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
054     * 08-Sep-2003 : Changed ValueAxis API (DG);
055     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
056     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
057     * 23-Apr-2004 : Added fillBox attribute, extended equals() method and fixed 
058     *               serialization issue (DG);
059     * 29-Apr-2004 : Fixed problem with drawing upper and lower shadows - bug id 
060     *               944011 (DG);
061     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
062     *               getYValue() (DG);
063     * 01-Oct-2004 : Renamed 'paint' --> 'boxPaint' to avoid conflict with 
064     *               inherited attribute (DG);
065     * 10-Jun-2005 : Updated equals() to handle GradientPaint (DG);
066     * 06-Oct-2005 : Removed setPaint() call in drawItem(), it is causing a 
067     *               loop (DG);
068     *
069     * DO NOT USE drawHorizontalItem() - IT IS INCOMPLETE
070     * TO EXPERIMENT, USE drawVerticalItem()
071     */
072    
073    package org.jfree.chart.renderer.xy;
074    
075    import java.awt.Color;
076    import java.awt.Graphics2D;
077    import java.awt.Paint;
078    import java.awt.Shape;
079    import java.awt.Stroke;
080    import java.awt.geom.Ellipse2D;
081    import java.awt.geom.Line2D;
082    import java.awt.geom.Point2D;
083    import java.awt.geom.Rectangle2D;
084    import java.io.IOException;
085    import java.io.ObjectInputStream;
086    import java.io.ObjectOutputStream;
087    import java.io.Serializable;
088    import java.util.ArrayList;
089    import java.util.Collections;
090    import java.util.Iterator;
091    import java.util.List;
092    
093    import org.jfree.chart.axis.ValueAxis;
094    import org.jfree.chart.entity.EntityCollection;
095    import org.jfree.chart.entity.XYItemEntity;
096    import org.jfree.chart.event.RendererChangeEvent;
097    import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator;
098    import org.jfree.chart.labels.XYToolTipGenerator;
099    import org.jfree.chart.plot.CrosshairState;
100    import org.jfree.chart.plot.PlotOrientation;
101    import org.jfree.chart.plot.PlotRenderingInfo;
102    import org.jfree.chart.plot.XYPlot;
103    import org.jfree.chart.renderer.Outlier;
104    import org.jfree.chart.renderer.OutlierList;
105    import org.jfree.chart.renderer.OutlierListCollection;
106    import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
107    import org.jfree.data.xy.XYDataset;
108    import org.jfree.io.SerialUtilities;
109    import org.jfree.ui.RectangleEdge;
110    import org.jfree.util.PaintUtilities;
111    import org.jfree.util.PublicCloneable;
112    
113    /**
114     * A renderer that draws box-and-whisker items on an {@link XYPlot}.  This 
115     * renderer requires a {@link BoxAndWhiskerXYDataset}).
116     * <P>
117     * This renderer does not include any code to calculate the crosshair point.
118     *
119     * @author David Browning
120     */
121    public class XYBoxAndWhiskerRenderer extends AbstractXYItemRenderer 
122                                         implements XYItemRenderer, 
123                                                    Cloneable,
124                                                    PublicCloneable,
125                                                    Serializable {
126    
127        /** For serialization. */
128        private static final long serialVersionUID = -8020170108532232324L;
129        
130        /** The box width. */
131        private double boxWidth;
132    
133        /** The paint used to fill the box. */
134        private transient Paint boxPaint;
135    
136        /** A flag that controls whether or not the box is filled. */
137        private boolean fillBox;
138        
139        /** 
140         * The paint used to draw various artifacts such as outliers, farout 
141         * symbol, average ellipse and median line. 
142         */
143        private transient Paint artifactPaint = Color.black;
144    
145        /**
146         * Creates a new renderer for box and whisker charts.
147         */
148        public XYBoxAndWhiskerRenderer() {
149            this(-1.0);
150        }
151    
152        /**
153         * Creates a new renderer for box and whisker charts.
154         * <P>
155         * Use -1 for the box width if you prefer the width to be calculated 
156         * automatically.
157         *
158         * @param boxWidth  the box width.
159         */
160        public XYBoxAndWhiskerRenderer(double boxWidth) {
161            super();
162            this.boxWidth = boxWidth;
163            this.boxPaint = Color.green;
164            this.fillBox = true;
165            setToolTipGenerator(new BoxAndWhiskerXYToolTipGenerator());
166        }
167    
168        /**
169         * Returns the width of each box.
170         *
171         * @return The box width.
172         */
173        public double getBoxWidth() {
174            return this.boxWidth;
175        }
176    
177        /**
178         * Sets the box width.
179         * <P>
180         * If you set the width to a negative value, the renderer will calculate
181         * the box width automatically based on the space available on the chart.
182         *
183         * @param width  the width.
184         */
185        public void setBoxWidth(double width) {
186            if (width != this.boxWidth) {
187                this.boxWidth = width;
188                notifyListeners(new RendererChangeEvent(this));
189            }
190        }
191    
192        /**
193         * Returns the paint used to fill boxes.
194         *
195         * @return The paint (possibly <code>null</code>).
196         */
197        public Paint getBoxPaint() {
198            return this.boxPaint;
199        }
200    
201        /**
202         * Sets the paint used to fill boxes and sends a {@link RendererChangeEvent}
203         * to all registered listeners.
204         *
205         * @param paint  the paint (<code>null</code> permitted).
206         */
207        public void setBoxPaint(Paint paint) {
208            this.boxPaint = paint;
209            notifyListeners(new RendererChangeEvent(this));
210        }
211        
212        /**
213         * Returns the flag that controls whether or not the box is filled.
214         * 
215         * @return A boolean.
216         */
217        public boolean getFillBox() {
218            return this.fillBox;   
219        }
220        
221        /**
222         * Sets the flag that controls whether or not the box is filled and sends a 
223         * {@link RendererChangeEvent} to all registered listeners.
224         * 
225         * @param flag  the flag.
226         */
227        public void setFillBox(boolean flag) {
228            this.fillBox = flag;
229            notifyListeners(new RendererChangeEvent(this));
230        }
231    
232        /**
233         * Returns the paint used to paint the various artifacts such as outliers, 
234         * farout symbol, median line and the averages ellipse.
235         *
236         * @return The paint.
237         */
238        public Paint getArtifactPaint() {
239            return this.artifactPaint;
240        }
241    
242        /**
243         * Sets the paint used to paint the various artifacts such as outliers, 
244         * farout symbol, median line and the averages ellipse.
245         * 
246         * @param artifactPaint  the paint.
247         */
248        public void setArtifactPaint(Paint artifactPaint) {
249            this.artifactPaint = artifactPaint;
250        }
251    
252        /**
253         * Draws the visual representation of a single data item.
254         *
255         * @param g2  the graphics device.
256         * @param state  the renderer state.
257         * @param dataArea  the area within which the plot is being drawn.
258         * @param info  collects info about the drawing.
259         * @param plot  the plot (can be used to obtain standard color 
260         *              information etc).
261         * @param domainAxis  the domain axis.
262         * @param rangeAxis  the range axis.
263         * @param dataset  the dataset.
264         * @param series  the series index (zero-based).
265         * @param item  the item index (zero-based).
266         * @param crosshairState  crosshair information for the plot 
267         *                        (<code>null</code> permitted).
268         * @param pass  the pass index.
269         */
270        public void drawItem(Graphics2D g2, 
271                             XYItemRendererState state,
272                             Rectangle2D dataArea,
273                             PlotRenderingInfo info,
274                             XYPlot plot, 
275                             ValueAxis domainAxis, 
276                             ValueAxis rangeAxis,
277                             XYDataset dataset, 
278                             int series, 
279                             int item,
280                             CrosshairState crosshairState,
281                             int pass) {
282    
283            PlotOrientation orientation = plot.getOrientation();
284    
285            if (orientation == PlotOrientation.HORIZONTAL) {
286                drawHorizontalItem(
287                    g2, dataArea, info, plot, domainAxis, rangeAxis,
288                    dataset, series, item, crosshairState, pass
289                );
290            }
291            else if (orientation == PlotOrientation.VERTICAL) {
292                drawVerticalItem(
293                    g2, dataArea, info, plot, domainAxis, rangeAxis,
294                    dataset, series, item, crosshairState, pass
295                );
296            }
297    
298        }
299    
300        /**
301         * Draws the visual representation of a single data item.
302         *
303         * @param g2  the graphics device.
304         * @param dataArea  the area within which the plot is being drawn.
305         * @param info  collects info about the drawing.
306         * @param plot  the plot (can be used to obtain standard color 
307         *              information etc).
308         * @param domainAxis  the domain axis.
309         * @param rangeAxis  the range axis.
310         * @param dataset  the dataset.
311         * @param series  the series index (zero-based).
312         * @param item  the item index (zero-based).
313         * @param crosshairState  crosshair information for the plot 
314         *                        (<code>null</code> permitted).
315         * @param pass  the pass index.
316         */
317        public void drawHorizontalItem(Graphics2D g2, 
318                                       Rectangle2D dataArea,
319                                       PlotRenderingInfo info,
320                                       XYPlot plot, 
321                                       ValueAxis domainAxis, 
322                                       ValueAxis rangeAxis,
323                                       XYDataset dataset, 
324                                       int series, 
325                                       int item,
326                                       CrosshairState crosshairState,
327                                       int pass) {
328    
329            // setup for collecting optional entity info...
330            EntityCollection entities = null;
331            if (info != null) {
332                entities = info.getOwner().getEntityCollection();
333            }
334    
335            BoxAndWhiskerXYDataset boxAndWhiskerData 
336                = (BoxAndWhiskerXYDataset) dataset;
337    
338            Number x = boxAndWhiskerData.getX(series, item);
339            Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
340            Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
341            Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
342            Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
343    
344            double xx = domainAxis.valueToJava2D(
345                x.doubleValue(), dataArea, plot.getDomainAxisEdge()
346            );
347    
348            RectangleEdge location = plot.getRangeAxisEdge();
349            double yyMax = rangeAxis.valueToJava2D(
350                yMax.doubleValue(), dataArea, location
351            );
352            double yyMin = rangeAxis.valueToJava2D(
353                yMin.doubleValue(), dataArea, location
354            );
355    
356            double yyQ1Median = rangeAxis.valueToJava2D(
357                yQ1Median.doubleValue(), dataArea, location
358            );
359            double yyQ3Median = rangeAxis.valueToJava2D(
360                yQ3Median.doubleValue(), dataArea, location
361            );
362    
363            double exactCandleWidth = getBoxWidth();
364            double thisCandleWidth = exactCandleWidth;
365            if (exactCandleWidth <= 0.0) {
366                int itemCount = boxAndWhiskerData.getItemCount(series);
367                exactCandleWidth = (dataArea.getHeight()) / itemCount * 4.5 / 7;
368                if (exactCandleWidth < 1) {
369                    exactCandleWidth = 1;
370                }
371                thisCandleWidth = exactCandleWidth;
372                if (thisCandleWidth < 3) {
373                    thisCandleWidth = 3;
374                }
375            }
376    
377            Stroke s = getItemStroke(series, item);
378    
379            g2.setStroke(s);
380    
381            // draw the upper shadow
382            if ((yyMax > yyQ1Median) && (yyMax > yyQ3Median)) {
383                g2.draw(
384                    new Line2D.Double(yyMax, xx, Math.max(yyQ1Median, yyQ3Median), 
385                            xx)
386                );
387            }
388    
389            // draw the lower shadow
390            if ((yyMin < yyQ1Median) && (yyMin < yyQ3Median)) {
391                g2.draw(
392                    new Line2D.Double(yyMin, xx, Math.min(yyQ1Median, yyQ3Median), 
393                            xx)
394                );
395            }
396    
397    
398            // draw the body
399            Shape box = null;
400            if (yyQ1Median < yyQ3Median) {
401                box = new Rectangle2D.Double(
402                    yyQ1Median, xx - thisCandleWidth / 2, yyQ3Median - yyQ1Median, 
403                    thisCandleWidth
404                );
405            }
406            else {
407                box = new Rectangle2D.Double(
408                    yyQ3Median, xx - thisCandleWidth / 2, yyQ1Median - yyQ3Median, 
409                    thisCandleWidth
410                );
411                if (getBoxPaint() != null) {
412                    g2.setPaint(getBoxPaint());
413                }
414                if (this.fillBox) {
415                    g2.fill(box);   
416                }
417                g2.draw(box);
418            }
419    
420            // add an entity for the item...
421            if (entities != null) {
422                String tip = null;
423                XYToolTipGenerator generator = getToolTipGenerator(series, item);
424                if (generator != null) {
425                    tip = generator.generateToolTip(dataset, series, item);
426                }
427                String url = null;
428                if (getURLGenerator() != null) {
429                    url = getURLGenerator().generateURL(dataset, series, item);
430                }
431                XYItemEntity entity = new XYItemEntity(box, dataset, series, item, 
432                        tip, url);
433                entities.add(entity);
434            }
435    
436        }
437    
438        /**
439         * Draws the visual representation of a single data item.
440         *
441         * @param g2  the graphics device.
442         * @param dataArea  the area within which the plot is being drawn.
443         * @param info  collects info about the drawing.
444         * @param plot  the plot (can be used to obtain standard color 
445         *              information etc).
446         * @param domainAxis  the domain axis.
447         * @param rangeAxis  the range axis.
448         * @param dataset  the dataset.
449         * @param series  the series index (zero-based).
450         * @param item  the item index (zero-based).
451         * @param crosshairState  crosshair information for the plot 
452         *                        (<code>null</code> permitted).
453         * @param pass  the pass index.
454         */
455        public void drawVerticalItem(Graphics2D g2, 
456                                     Rectangle2D dataArea,
457                                     PlotRenderingInfo info,
458                                     XYPlot plot, 
459                                     ValueAxis domainAxis, 
460                                     ValueAxis rangeAxis,
461                                     XYDataset dataset, 
462                                     int series, 
463                                     int item,
464                                     CrosshairState crosshairState,
465                                     int pass) {
466    
467            // setup for collecting optional entity info...
468            EntityCollection entities = null;
469            if (info != null) {
470                entities = info.getOwner().getEntityCollection();
471            }
472    
473            BoxAndWhiskerXYDataset boxAndWhiskerData 
474                = (BoxAndWhiskerXYDataset) dataset;
475    
476            Number x = boxAndWhiskerData.getX(series, item);
477            Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
478            Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
479            Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
480            Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
481            Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
482            Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
483            List yOutliers = boxAndWhiskerData.getOutliers(series, item);
484    
485            double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea, 
486                    plot.getDomainAxisEdge());
487    
488            RectangleEdge location = plot.getRangeAxisEdge();
489            double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea, 
490                    location);
491            double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea, 
492                    location);
493            double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(), 
494                    dataArea, location);
495            double yyAverage = 0.0;
496            if (yAverage != null) {
497                yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(), 
498                        dataArea, location);
499            }
500            double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(), 
501                    dataArea, location);
502            double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(), 
503                    dataArea, location);
504            double yyOutlier;
505    
506    
507            double exactBoxWidth = getBoxWidth();
508            double width = exactBoxWidth;
509            double dataAreaX = dataArea.getMaxX() - dataArea.getMinX();
510            double maxBoxPercent = 0.1;
511            double maxBoxWidth = dataAreaX * maxBoxPercent;
512            if (exactBoxWidth <= 0.0) {
513                int itemCount = boxAndWhiskerData.getItemCount(series);
514                exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
515                if (exactBoxWidth < 3) {
516                    width = 3;
517                } 
518                else if (exactBoxWidth > maxBoxWidth) {
519                    width = maxBoxWidth;
520                } 
521                else {
522                    width = exactBoxWidth;
523                }
524            }
525    
526            Paint p = getBoxPaint();
527            if (p != null) {
528                g2.setPaint(p);
529            }
530            Stroke s = getItemStroke(series, item);
531    
532            g2.setStroke(s);
533    
534            // draw the upper shadow
535            g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median));
536            g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2, 
537                    yyMax));
538    
539            // draw the lower shadow
540            g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median));
541            g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2, 
542                    yyMin));
543            
544            // draw the body
545            Shape box = null;
546            if (yyQ1Median > yyQ3Median) {
547                box = new Rectangle2D.Double(
548                    xx - width / 2, yyQ3Median, width, yyQ1Median - yyQ3Median
549                );
550            }
551            else {
552                box = new Rectangle2D.Double(
553                    xx - width / 2, yyQ1Median, width, yyQ3Median - yyQ1Median
554                );
555            }
556            if (this.fillBox) {
557                g2.fill(box);   
558            }
559            g2.draw(box);
560    
561            // draw median
562            g2.setPaint(getArtifactPaint());
563            g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2, 
564                    yyMedian));
565    
566            double aRadius = 0;                 // average radius
567            double oRadius = width / 3;    // outlier radius
568    
569            // draw average - SPECIAL AIMS REQUIREMENT
570            if (yAverage != null) {
571                aRadius = width / 4;
572                Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
573                    xx - aRadius, yyAverage - aRadius, aRadius * 2, aRadius * 2
574                );
575                g2.fill(avgEllipse);
576                g2.draw(avgEllipse);
577            }
578    
579            List outliers = new ArrayList();
580            OutlierListCollection outlierListCollection 
581                = new OutlierListCollection();
582    
583            /* From outlier array sort out which are outliers and put these into 
584             * an arraylist. If there are any farouts, set the flag on the 
585             * OutlierListCollection
586             */
587    
588            for (int i = 0; i < yOutliers.size(); i++) {
589                double outlier = ((Number) yOutliers.get(i)).doubleValue();
590                if (outlier > boxAndWhiskerData.getMaxOutlier(series, 
591                        item).doubleValue()) {
592                    outlierListCollection.setHighFarOut(true);
593                } 
594                else if (outlier < boxAndWhiskerData.getMinOutlier(series, 
595                        item).doubleValue()) {
596                    outlierListCollection.setLowFarOut(true);
597                } 
598                else if (outlier > boxAndWhiskerData.getMaxRegularValue(series, 
599                        item).doubleValue()) {
600                    yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 
601                            location);
602                    outliers.add(new Outlier(xx, yyOutlier, oRadius));
603                }
604                else if (outlier < boxAndWhiskerData.getMinRegularValue(series, 
605                        item).doubleValue()) {
606                    yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea, 
607                            location);
608                    outliers.add(new Outlier(xx, yyOutlier, oRadius));
609                }
610                Collections.sort(outliers);
611            }
612    
613            // Process outliers. Each outlier is either added to the appropriate 
614            // outlier list or a new outlier list is made
615            for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
616                Outlier outlier = (Outlier) iterator.next();
617                outlierListCollection.add(outlier);
618            }
619    
620            // draw yOutliers
621            double maxAxisValue = rangeAxis.valueToJava2D(
622                rangeAxis.getUpperBound(), dataArea, location
623            ) + aRadius;
624            double minAxisValue = rangeAxis.valueToJava2D(
625                rangeAxis.getLowerBound(), dataArea, location
626            ) - aRadius;
627    
628            // draw outliers
629            for (Iterator iterator = outlierListCollection.iterator(); 
630                    iterator.hasNext();) {
631                OutlierList list = (OutlierList) iterator.next();
632                Outlier outlier = list.getAveragedOutlier();
633                Point2D point = outlier.getPoint();
634    
635                if (list.isMultiple()) {
636                    drawMultipleEllipse(point, width, oRadius, g2);
637                } 
638                else {
639                    drawEllipse(point, oRadius, g2);
640                }
641            }
642    
643            // draw farout
644            if (outlierListCollection.isHighFarOut()) {
645                drawHighFarOut(aRadius, g2, xx, maxAxisValue);
646            }
647    
648            if (outlierListCollection.isLowFarOut()) {
649                drawLowFarOut(aRadius, g2, xx, minAxisValue);
650            }
651            
652            // add an entity for the item...
653            if (entities != null) {
654                String tip = null;
655                XYToolTipGenerator generator = getToolTipGenerator(series, item);
656                if (generator != null) {
657                    tip = generator.generateToolTip(dataset, series, item);
658                }
659                String url = null;
660                if (getURLGenerator() != null) {
661                    url = getURLGenerator().generateURL(dataset, series, item);
662                }
663                XYItemEntity entity = new XYItemEntity(box, dataset, series, item, 
664                        tip, url);
665                entities.add(entity);
666            }
667    
668        }
669    
670        /**
671         * Draws an ellipse to represent an outlier.
672         * 
673         * @param point  the location.
674         * @param oRadius  the radius.
675         * @param g2  the graphics device.
676         */
677        protected void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
678            Ellipse2D.Double dot = new Ellipse2D.Double(
679                point.getX() + oRadius / 2, point.getY(), oRadius, oRadius
680            );
681            g2.draw(dot);
682        }
683    
684        /**
685         * Draws two ellipses to represent overlapping outliers.
686         * 
687         * @param point  the location.
688         * @param boxWidth  the box width.
689         * @param oRadius  the radius.
690         * @param g2  the graphics device.
691         */
692        protected void drawMultipleEllipse(Point2D point, double boxWidth, 
693                                           double oRadius, Graphics2D g2) {
694                                             
695            Ellipse2D.Double dot1 = new Ellipse2D.Double(
696                point.getX() - (boxWidth / 2) + oRadius, point.getY(), oRadius, 
697                oRadius
698            );
699            Ellipse2D.Double dot2 = new Ellipse2D.Double(
700                point.getX() + (boxWidth / 2), point.getY(), oRadius, oRadius
701            );
702            g2.draw(dot1);
703            g2.draw(dot2);
704            
705        }
706    
707        /**
708         * Draws a triangle to indicate the presence of far out values.
709         * 
710         * @param aRadius  the radius.
711         * @param g2  the graphics device.
712         * @param xx  the x value.
713         * @param m  the max y value.
714         */
715        protected void drawHighFarOut(double aRadius, Graphics2D g2, double xx, 
716                double m) {
717            double side = aRadius * 2;
718            g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
719            g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
720            g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
721        }
722    
723        /**
724         * Draws a triangle to indicate the presence of far out values.
725         * 
726         * @param aRadius  the radius.
727         * @param g2  the graphics device.
728         * @param xx  the x value.
729         * @param m  the min y value.
730         */
731        protected void drawLowFarOut(double aRadius, Graphics2D g2, double xx, 
732                double m) {
733            double side = aRadius * 2;
734            g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
735            g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
736            g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
737        }
738    
739        /**
740         * Tests this renderer for equality with another object.
741         *
742         * @param obj  the object (<code>null</code> permitted).
743         *
744         * @return <code>true</code> or <code>false</code>.
745         */
746        public boolean equals(Object obj) {
747            if (obj == this) {
748                return true;
749            }
750            if (!(obj instanceof XYBoxAndWhiskerRenderer)) {
751                return false;
752            }
753            if (!super.equals(obj)) {
754                return false;
755            }
756            XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj;
757            if (this.boxWidth != that.getBoxWidth()) {
758                return false;
759            }
760            if (!PaintUtilities.equal(this.boxPaint, that.boxPaint)) {
761                return false;
762            }
763            if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) {
764                return false;
765            }
766            if (this.fillBox != that.fillBox) {
767                return false;
768            }
769            return true;
770    
771        }
772    
773        /**
774         * Provides serialization support.
775         *
776         * @param stream  the output stream.
777         *
778         * @throws IOException  if there is an I/O error.
779         */
780        private void writeObject(ObjectOutputStream stream) throws IOException {
781    
782            stream.defaultWriteObject();
783            SerialUtilities.writePaint(this.boxPaint, stream);
784            SerialUtilities.writePaint(this.artifactPaint, stream);
785    
786        }
787    
788        /**
789         * Provides serialization support.
790         *
791         * @param stream  the input stream.
792         *
793         * @throws IOException  if there is an I/O error.
794         * @throws ClassNotFoundException  if there is a classpath problem.
795         */
796        private void readObject(ObjectInputStream stream) 
797            throws IOException, ClassNotFoundException {
798    
799            stream.defaultReadObject();
800            this.boxPaint = SerialUtilities.readPaint(stream);
801            this.artifactPaint = SerialUtilities.readPaint(stream);
802    
803        }
804    
805        /**
806         * Returns a clone of the renderer.
807         * 
808         * @return A clone.
809         * 
810         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
811         */
812        public Object clone() throws CloneNotSupportedException {
813            return super.clone();
814        }
815    
816    }