001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2021, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ----------------------------
028 * XYBoxAndWhiskerRenderer.java
029 * ----------------------------
030 * (C) Copyright 2003-2021, 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 */
037
038package org.jfree.chart.renderer.xy;
039
040import java.awt.Color;
041import java.awt.Graphics2D;
042import java.awt.Paint;
043import java.awt.Shape;
044import java.awt.Stroke;
045import java.awt.geom.Ellipse2D;
046import java.awt.geom.Line2D;
047import java.awt.geom.Point2D;
048import java.awt.geom.Rectangle2D;
049import java.io.IOException;
050import java.io.ObjectInputStream;
051import java.io.ObjectOutputStream;
052import java.io.Serializable;
053import java.util.ArrayList;
054import java.util.Collections;
055import java.util.Iterator;
056import java.util.List;
057
058import org.jfree.chart.axis.ValueAxis;
059import org.jfree.chart.entity.EntityCollection;
060import org.jfree.chart.event.RendererChangeEvent;
061import org.jfree.chart.labels.BoxAndWhiskerXYToolTipGenerator;
062import org.jfree.chart.plot.CrosshairState;
063import org.jfree.chart.plot.PlotOrientation;
064import org.jfree.chart.plot.PlotRenderingInfo;
065import org.jfree.chart.plot.XYPlot;
066import org.jfree.chart.renderer.Outlier;
067import org.jfree.chart.renderer.OutlierList;
068import org.jfree.chart.renderer.OutlierListCollection;
069import org.jfree.chart.ui.RectangleEdge;
070import org.jfree.chart.util.PaintUtils;
071import org.jfree.chart.util.Args;
072import org.jfree.chart.util.PublicCloneable;
073import org.jfree.chart.util.SerialUtils;
074import org.jfree.data.Range;
075import org.jfree.data.statistics.BoxAndWhiskerXYDataset;
076import org.jfree.data.xy.XYDataset;
077
078/**
079 * A renderer that draws box-and-whisker items on an {@link XYPlot}.  This
080 * renderer requires a {@link BoxAndWhiskerXYDataset}).  The example shown here
081 * is generated by the{@code BoxAndWhiskerChartDemo2.java} program
082 * included in the JFreeChart demo collection:
083 * <br><br>
084 * <img src="doc-files/XYBoxAndWhiskerRendererSample.png"
085 * alt="XYBoxAndWhiskerRendererSample.png">
086 * <P>
087 * This renderer does not include any code to calculate the crosshair point.
088 */
089public class XYBoxAndWhiskerRenderer extends AbstractXYItemRenderer
090        implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
091
092    /** For serialization. */
093    private static final long serialVersionUID = -8020170108532232324L;
094
095    /** The box width. */
096    private double boxWidth;
097
098    /** The paint used to fill the box. */
099    private transient Paint boxPaint;
100
101    /** A flag that controls whether or not the box is filled. */
102    private boolean fillBox;
103
104    /**
105     * The paint used to draw various artifacts such as outliers, farout
106     * symbol, average ellipse and median line.
107     */
108    private transient Paint artifactPaint = Color.BLACK;
109
110    /**
111     * Creates a new renderer for box and whisker charts.
112     */
113    public XYBoxAndWhiskerRenderer() {
114        this(-1.0);
115    }
116
117    /**
118     * Creates a new renderer for box and whisker charts.
119     * <P>
120     * Use -1 for the box width if you prefer the width to be calculated
121     * automatically.
122     *
123     * @param boxWidth  the box width.
124     */
125    public XYBoxAndWhiskerRenderer(double boxWidth) {
126        super();
127        this.boxWidth = boxWidth;
128        this.boxPaint = Color.GREEN;
129        this.fillBox = true;
130        setDefaultToolTipGenerator(new BoxAndWhiskerXYToolTipGenerator());
131    }
132
133    /**
134     * Returns the width of each box.
135     *
136     * @return The box width.
137     *
138     * @see #setBoxWidth(double)
139     */
140    public double getBoxWidth() {
141        return this.boxWidth;
142    }
143
144    /**
145     * Sets the box width and sends a {@link RendererChangeEvent} to all
146     * registered listeners.
147     * <P>
148     * If you set the width to a negative value, the renderer will calculate
149     * the box width automatically based on the space available on the chart.
150     *
151     * @param width  the width.
152     *
153     * @see #getBoxWidth()
154     */
155    public void setBoxWidth(double width) {
156        if (width != this.boxWidth) {
157            this.boxWidth = width;
158            fireChangeEvent();
159        }
160    }
161
162    /**
163     * Returns the paint used to fill boxes.
164     *
165     * @return The paint (possibly {@code null}).
166     *
167     * @see #setBoxPaint(Paint)
168     */
169    public Paint getBoxPaint() {
170        return this.boxPaint;
171    }
172
173    /**
174     * Sets the paint used to fill boxes and sends a {@link RendererChangeEvent}
175     * to all registered listeners.
176     *
177     * @param paint  the paint ({@code null} permitted).
178     *
179     * @see #getBoxPaint()
180     */
181    public void setBoxPaint(Paint paint) {
182        this.boxPaint = paint;
183        fireChangeEvent();
184    }
185
186    /**
187     * Returns the flag that controls whether or not the box is filled.
188     *
189     * @return A boolean.
190     *
191     * @see #setFillBox(boolean)
192     */
193    public boolean getFillBox() {
194        return this.fillBox;
195    }
196
197    /**
198     * Sets the flag that controls whether or not the box is filled and sends a
199     * {@link RendererChangeEvent} to all registered listeners.
200     *
201     * @param flag  the flag.
202     *
203     * @see #setFillBox(boolean)
204     */
205    public void setFillBox(boolean flag) {
206        this.fillBox = flag;
207        fireChangeEvent();
208    }
209
210    /**
211     * Returns the paint used to paint the various artifacts such as outliers,
212     * farout symbol, median line and the averages ellipse.
213     *
214     * @return The paint (never {@code null}).
215     *
216     * @see #setArtifactPaint(Paint)
217     */
218    public Paint getArtifactPaint() {
219        return this.artifactPaint;
220    }
221
222    /**
223     * Sets the paint used to paint the various artifacts such as outliers,
224     * farout symbol, median line and the averages ellipse, and sends a
225     * {@link RendererChangeEvent} to all registered listeners.
226     *
227     * @param paint  the paint ({@code null} not permitted).
228     *
229     * @see #getArtifactPaint()
230     */
231    public void setArtifactPaint(Paint paint) {
232        Args.nullNotPermitted(paint, "paint");
233        this.artifactPaint = paint;
234        fireChangeEvent();
235    }
236
237    /**
238     * Returns the range of values the renderer requires to display all the
239     * items from the specified dataset.
240     *
241     * @param dataset  the dataset ({@code null} permitted).
242     *
243     * @return The range ({@code null} if the dataset is {@code null}
244     *         or empty).
245     *
246     * @see #findDomainBounds(XYDataset)
247     */
248    @Override
249    public Range findRangeBounds(XYDataset dataset) {
250        return findRangeBounds(dataset, true);
251    }
252
253    /**
254     * Returns the box paint or, if this is {@code null}, the item
255     * paint.
256     *
257     * @param series  the series index.
258     * @param item  the item index.
259     *
260     * @return The paint used to fill the box for the specified item (never
261     *         {@code null}).
262     */
263    protected Paint lookupBoxPaint(int series, int item) {
264        Paint p = getBoxPaint();
265        if (p != null) {
266            return p;
267        }
268        else {
269            // TODO: could change this to itemFillPaint().  For backwards
270            // compatibility, it might require a useFillPaint flag.
271            return getItemPaint(series, item);
272        }
273    }
274
275    /**
276     * Draws the visual representation of a single data item.
277     *
278     * @param g2  the graphics device.
279     * @param state  the renderer state.
280     * @param dataArea  the area within which the plot is being drawn.
281     * @param info  collects info about the drawing.
282     * @param plot  the plot (can be used to obtain standard color
283     *              information etc).
284     * @param domainAxis  the domain axis.
285     * @param rangeAxis  the range axis.
286     * @param dataset  the dataset (must be an instance of
287     *                 {@link BoxAndWhiskerXYDataset}).
288     * @param series  the series index (zero-based).
289     * @param item  the item index (zero-based).
290     * @param crosshairState  crosshair information for the plot
291     *                        ({@code null} permitted).
292     * @param pass  the pass index.
293     */
294    @Override
295    public void drawItem(Graphics2D g2, XYItemRendererState state,
296            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
297            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
298            int series, int item, CrosshairState crosshairState, int pass) {
299
300        PlotOrientation orientation = plot.getOrientation();
301
302        if (orientation == PlotOrientation.HORIZONTAL) {
303            drawHorizontalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
304                    dataset, series, item, crosshairState, pass);
305        }
306        else if (orientation == PlotOrientation.VERTICAL) {
307            drawVerticalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
308                    dataset, series, item, crosshairState, pass);
309        }
310
311    }
312
313    /**
314     * Draws the visual representation of a single data item.
315     *
316     * @param g2  the graphics device.
317     * @param dataArea  the area within which the plot is being drawn.
318     * @param info  collects info about the drawing.
319     * @param plot  the plot (can be used to obtain standard color
320     *              information etc).
321     * @param domainAxis  the domain axis.
322     * @param rangeAxis  the range axis.
323     * @param dataset  the dataset (must be an instance of
324     *                 {@link BoxAndWhiskerXYDataset}).
325     * @param series  the series index (zero-based).
326     * @param item  the item index (zero-based).
327     * @param crosshairState  crosshair information for the plot
328     *                        ({@code null} permitted).
329     * @param pass  the pass index.
330     */
331    public void drawHorizontalItem(Graphics2D g2, Rectangle2D dataArea,
332            PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis,
333            ValueAxis rangeAxis, XYDataset dataset, int series,
334            int item, CrosshairState crosshairState, int pass) {
335
336        // setup for collecting optional entity info...
337        EntityCollection entities = null;
338        if (info != null) {
339            entities = info.getOwner().getEntityCollection();
340        }
341
342        BoxAndWhiskerXYDataset boxAndWhiskerData
343                = (BoxAndWhiskerXYDataset) dataset;
344
345        Number x = boxAndWhiskerData.getX(series, item);
346        Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
347        Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
348        Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
349        Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
350        Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
351        Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
352
353        double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea,
354                plot.getDomainAxisEdge());
355
356        RectangleEdge location = plot.getRangeAxisEdge();
357        double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea,
358                location);
359        double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea,
360                location);
361        double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
362                dataArea, location);
363        double yyAverage = 0.0;
364        if (yAverage != null) {
365            yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(),
366                    dataArea, location);
367        }
368        double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(),
369                dataArea, location);
370        double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(),
371                dataArea, location);
372
373        double exactBoxWidth = getBoxWidth();
374        double width = exactBoxWidth;
375        double dataAreaX = dataArea.getHeight();
376        double maxBoxPercent = 0.1;
377        double maxBoxWidth = dataAreaX * maxBoxPercent;
378        if (exactBoxWidth <= 0.0) {
379            int itemCount = boxAndWhiskerData.getItemCount(series);
380            exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
381            if (exactBoxWidth < 3) {
382                width = 3;
383            }
384            else if (exactBoxWidth > maxBoxWidth) {
385                width = maxBoxWidth;
386            }
387            else {
388                width = exactBoxWidth;
389            }
390        }
391
392        g2.setPaint(getItemPaint(series, item));
393        Stroke s = getItemStroke(series, item);
394        g2.setStroke(s);
395
396        // draw the upper shadow
397        g2.draw(new Line2D.Double(yyMax, xx, yyQ3Median, xx));
398        g2.draw(new Line2D.Double(yyMax, xx - width / 2, yyMax,
399                xx + width / 2));
400
401        // draw the lower shadow
402        g2.draw(new Line2D.Double(yyMin, xx, yyQ1Median, xx));
403        g2.draw(new Line2D.Double(yyMin, xx - width / 2, yyMin,
404                xx + width / 2));
405
406        // draw the body
407        Shape box;
408        if (yyQ1Median < yyQ3Median) {
409            box = new Rectangle2D.Double(yyQ1Median, xx - width / 2,
410                    yyQ3Median - yyQ1Median, width);
411        }
412        else {
413            box = new Rectangle2D.Double(yyQ3Median, xx - width / 2,
414                    yyQ1Median - yyQ3Median, width);
415        }
416        if (this.fillBox) {
417            g2.setPaint(lookupBoxPaint(series, item));
418            g2.fill(box);
419        }
420        g2.setStroke(getItemOutlineStroke(series, item));
421        g2.setPaint(getItemOutlinePaint(series, item));
422        g2.draw(box);
423
424        // draw median
425        g2.setPaint(getArtifactPaint());
426        g2.draw(new Line2D.Double(yyMedian,
427                xx - width / 2, yyMedian, xx + width / 2));
428
429        // draw average - SPECIAL AIMS REQUIREMENT
430        if (yAverage != null) {
431            double aRadius = width / 4;
432            // here we check that the average marker will in fact be visible
433            // before drawing it...
434            if ((yyAverage > (dataArea.getMinX() - aRadius))
435                    && (yyAverage < (dataArea.getMaxX() + aRadius))) {
436                Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
437                        yyAverage - aRadius, xx - aRadius, aRadius * 2,
438                        aRadius * 2);
439                g2.fill(avgEllipse);
440                g2.draw(avgEllipse);
441            }
442        }
443
444        // FIXME: draw outliers
445
446        // add an entity for the item...
447        if (entities != null && box.intersects(dataArea)) {
448            addEntity(entities, box, dataset, series, item, yyAverage, xx);
449        }
450
451    }
452
453    /**
454     * Draws the visual representation of a single data item.
455     *
456     * @param g2  the graphics device.
457     * @param dataArea  the area within which the plot is being drawn.
458     * @param info  collects info about the drawing.
459     * @param plot  the plot (can be used to obtain standard color
460     *              information etc).
461     * @param domainAxis  the domain axis.
462     * @param rangeAxis  the range axis.
463     * @param dataset  the dataset (must be an instance of
464     *                 {@link BoxAndWhiskerXYDataset}).
465     * @param series  the series index (zero-based).
466     * @param item  the item index (zero-based).
467     * @param crosshairState  crosshair information for the plot
468     *                        ({@code null} permitted).
469     * @param pass  the pass index.
470     */
471    public void drawVerticalItem(Graphics2D g2, Rectangle2D dataArea,
472            PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis,
473            ValueAxis rangeAxis, XYDataset dataset, int series,
474            int item, CrosshairState crosshairState, int pass) {
475
476        // setup for collecting optional entity info...
477        EntityCollection entities = null;
478        if (info != null) {
479            entities = info.getOwner().getEntityCollection();
480        }
481
482        BoxAndWhiskerXYDataset boxAndWhiskerData
483            = (BoxAndWhiskerXYDataset) dataset;
484
485        Number x = boxAndWhiskerData.getX(series, item);
486        Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
487        Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
488        Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
489        Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
490        Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
491        Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
492        List yOutliers = boxAndWhiskerData.getOutliers(series, item);
493        // yOutliers can be null, but we'd prefer it to be an empty list in
494        // that case...
495        if (yOutliers == null) {
496            yOutliers = Collections.EMPTY_LIST;
497        }
498
499        double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea,
500                plot.getDomainAxisEdge());
501
502        RectangleEdge location = plot.getRangeAxisEdge();
503        double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea,
504                location);
505        double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea,
506                location);
507        double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
508                dataArea, location);
509        double yyAverage = 0.0;
510        if (yAverage != null) {
511            yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(),
512                    dataArea, location);
513        }
514        double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(),
515                dataArea, location);
516        double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(),
517                dataArea, location);
518        double yyOutlier;
519
520        double exactBoxWidth = getBoxWidth();
521        double width = exactBoxWidth;
522        double dataAreaX = dataArea.getMaxX() - dataArea.getMinX();
523        double maxBoxPercent = 0.1;
524        double maxBoxWidth = dataAreaX * maxBoxPercent;
525        if (exactBoxWidth <= 0.0) {
526            int itemCount = boxAndWhiskerData.getItemCount(series);
527            exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
528            if (exactBoxWidth < 3) {
529                width = 3;
530            }
531            else if (exactBoxWidth > maxBoxWidth) {
532                width = maxBoxWidth;
533            }
534            else {
535                width = exactBoxWidth;
536            }
537        }
538
539        g2.setPaint(getItemPaint(series, item));
540        Stroke s = getItemStroke(series, item);
541        g2.setStroke(s);
542
543        // draw the upper shadow
544        g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median));
545        g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2,
546                yyMax));
547
548        // draw the lower shadow
549        g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median));
550        g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2,
551                yyMin));
552
553        // draw the body
554        Shape box;
555        if (yyQ1Median > yyQ3Median) {
556            box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width,
557                    yyQ1Median - yyQ3Median);
558        }
559        else {
560            box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width,
561                    yyQ3Median - yyQ1Median);
562        }
563        if (this.fillBox) {
564            g2.setPaint(lookupBoxPaint(series, item));
565            g2.fill(box);
566        }
567        g2.setStroke(getItemOutlineStroke(series, item));
568        g2.setPaint(getItemOutlinePaint(series, item));
569        g2.draw(box);
570
571        // draw median
572        g2.setPaint(getArtifactPaint());
573        g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2,
574                yyMedian));
575
576        double aRadius = 0;                 // average radius
577        double oRadius = width / 3;    // outlier radius
578
579        // draw average - SPECIAL AIMS REQUIREMENT
580        if (yAverage != null) {
581            aRadius = width / 4;
582            // here we check that the average marker will in fact be visible
583            // before drawing it...
584            if ((yyAverage > (dataArea.getMinY() - aRadius))
585                    && (yyAverage < (dataArea.getMaxY() + aRadius))) {
586                Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius,
587                        yyAverage - aRadius, aRadius * 2, aRadius * 2);
588                g2.fill(avgEllipse);
589                g2.draw(avgEllipse);
590            }
591        }
592
593        List outliers = new ArrayList();
594        OutlierListCollection outlierListCollection
595                = new OutlierListCollection();
596
597        /* From outlier array sort out which are outliers and put these into
598         * an arraylist. If there are any farouts, set the flag on the
599         * OutlierListCollection
600         */
601        for (int i = 0; i < yOutliers.size(); i++) {
602            double outlier = ((Number) yOutliers.get(i)).doubleValue();
603            if (outlier > boxAndWhiskerData.getMaxOutlier(series,
604                    item).doubleValue()) {
605                outlierListCollection.setHighFarOut(true);
606            }
607            else if (outlier < boxAndWhiskerData.getMinOutlier(series,
608                    item).doubleValue()) {
609                outlierListCollection.setLowFarOut(true);
610            }
611            else if (outlier > boxAndWhiskerData.getMaxRegularValue(series,
612                    item).doubleValue()) {
613                yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
614                        location);
615                outliers.add(new Outlier(xx, yyOutlier, oRadius));
616            }
617            else if (outlier < boxAndWhiskerData.getMinRegularValue(series,
618                    item).doubleValue()) {
619                yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
620                        location);
621                outliers.add(new Outlier(xx, yyOutlier, oRadius));
622            }
623            Collections.sort(outliers);
624        }
625
626        // Process outliers. Each outlier is either added to the appropriate
627        // outlier list or a new outlier list is made
628        for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
629            Outlier outlier = (Outlier) iterator.next();
630            outlierListCollection.add(outlier);
631        }
632
633        // draw yOutliers
634        double maxAxisValue = rangeAxis.valueToJava2D(rangeAxis.getUpperBound(),
635                dataArea, location) + aRadius;
636        double minAxisValue = rangeAxis.valueToJava2D(rangeAxis.getLowerBound(),
637                dataArea, location) - aRadius;
638
639        // draw outliers
640        for (Iterator iterator = outlierListCollection.iterator();
641                iterator.hasNext();) {
642            OutlierList list = (OutlierList) iterator.next();
643            Outlier outlier = list.getAveragedOutlier();
644            Point2D point = outlier.getPoint();
645
646            if (list.isMultiple()) {
647                drawMultipleEllipse(point, width, oRadius, g2);
648            }
649            else {
650                drawEllipse(point, oRadius, g2);
651            }
652        }
653
654        // draw farout
655        if (outlierListCollection.isHighFarOut()) {
656            drawHighFarOut(aRadius, g2, xx, maxAxisValue);
657        }
658
659        if (outlierListCollection.isLowFarOut()) {
660            drawLowFarOut(aRadius, g2, xx, minAxisValue);
661        }
662
663        // add an entity for the item...
664        if (entities != null && box.intersects(dataArea)) {
665            addEntity(entities, box, dataset, series, item, xx, yyAverage);
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(point.getX() + oRadius / 2,
679                point.getY(), oRadius, oRadius);
680        g2.draw(dot);
681    }
682
683    /**
684     * Draws two ellipses to represent overlapping outliers.
685     *
686     * @param point  the location.
687     * @param boxWidth  the box width.
688     * @param oRadius  the radius.
689     * @param g2  the graphics device.
690     */
691    protected void drawMultipleEllipse(Point2D point, double boxWidth,
692                                       double oRadius, Graphics2D g2) {
693
694        Ellipse2D.Double dot1 = new Ellipse2D.Double(point.getX()
695                - (boxWidth / 2) + oRadius, point.getY(), oRadius, oRadius);
696        Ellipse2D.Double dot2 = new Ellipse2D.Double(point.getX()
697                + (boxWidth / 2), point.getY(), oRadius, oRadius);
698        g2.draw(dot1);
699        g2.draw(dot2);
700
701    }
702
703    /**
704     * Draws a triangle to indicate the presence of far out values.
705     *
706     * @param aRadius  the radius.
707     * @param g2  the graphics device.
708     * @param xx  the x value.
709     * @param m  the max y value.
710     */
711    protected void drawHighFarOut(double aRadius, Graphics2D g2, double xx,
712            double m) {
713        double side = aRadius * 2;
714        g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
715        g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
716        g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
717    }
718
719    /**
720     * Draws a triangle to indicate the presence of far out values.
721     *
722     * @param aRadius  the radius.
723     * @param g2  the graphics device.
724     * @param xx  the x value.
725     * @param m  the min y value.
726     */
727    protected void drawLowFarOut(double aRadius, Graphics2D g2, double xx,
728            double m) {
729        double side = aRadius * 2;
730        g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
731        g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
732        g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
733    }
734
735    /**
736     * Tests this renderer for equality with another object.
737     *
738     * @param obj  the object ({@code null} permitted).
739     *
740     * @return {@code true} or {@code false}.
741     */
742    @Override
743    public boolean equals(Object obj) {
744        if (obj == this) {
745            return true;
746        }
747        if (!(obj instanceof XYBoxAndWhiskerRenderer)) {
748            return false;
749        }
750        if (!super.equals(obj)) {
751            return false;
752        }
753        XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj;
754        if (this.boxWidth != that.getBoxWidth()) {
755            return false;
756        }
757        if (!PaintUtils.equal(this.boxPaint, that.boxPaint)) {
758            return false;
759        }
760        if (!PaintUtils.equal(this.artifactPaint, that.artifactPaint)) {
761            return false;
762        }
763        if (this.fillBox != that.fillBox) {
764            return false;
765        }
766        return true;
767
768    }
769
770    /**
771     * Provides serialization support.
772     *
773     * @param stream  the output stream.
774     *
775     * @throws IOException  if there is an I/O error.
776     */
777    private void writeObject(ObjectOutputStream stream) throws IOException {
778        stream.defaultWriteObject();
779        SerialUtils.writePaint(this.boxPaint, stream);
780        SerialUtils.writePaint(this.artifactPaint, stream);
781    }
782
783    /**
784     * Provides serialization support.
785     *
786     * @param stream  the input stream.
787     *
788     * @throws IOException  if there is an I/O error.
789     * @throws ClassNotFoundException  if there is a classpath problem.
790     */
791    private void readObject(ObjectInputStream stream)
792        throws IOException, ClassNotFoundException {
793
794        stream.defaultReadObject();
795        this.boxPaint = SerialUtils.readPaint(stream);
796        this.artifactPaint = SerialUtils.readPaint(stream);
797    }
798
799    /**
800     * Returns a clone of the renderer.
801     *
802     * @return A clone.
803     *
804     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
805     */
806    @Override
807    public Object clone() throws CloneNotSupportedException {
808        return super.clone();
809    }
810
811}