001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2020, 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-2020, 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     * @since 1.0.10
264     */
265    protected Paint lookupBoxPaint(int series, int item) {
266        Paint p = getBoxPaint();
267        if (p != null) {
268            return p;
269        }
270        else {
271            // TODO: could change this to itemFillPaint().  For backwards
272            // compatibility, it might require a useFillPaint flag.
273            return getItemPaint(series, item);
274        }
275    }
276
277    /**
278     * Draws the visual representation of a single data item.
279     *
280     * @param g2  the graphics device.
281     * @param state  the renderer state.
282     * @param dataArea  the area within which the plot is being drawn.
283     * @param info  collects info about the drawing.
284     * @param plot  the plot (can be used to obtain standard color
285     *              information etc).
286     * @param domainAxis  the domain axis.
287     * @param rangeAxis  the range axis.
288     * @param dataset  the dataset (must be an instance of
289     *                 {@link BoxAndWhiskerXYDataset}).
290     * @param series  the series index (zero-based).
291     * @param item  the item index (zero-based).
292     * @param crosshairState  crosshair information for the plot
293     *                        ({@code null} permitted).
294     * @param pass  the pass index.
295     */
296    @Override
297    public void drawItem(Graphics2D g2, XYItemRendererState state,
298            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
299            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
300            int series, int item, CrosshairState crosshairState, int pass) {
301
302        PlotOrientation orientation = plot.getOrientation();
303
304        if (orientation == PlotOrientation.HORIZONTAL) {
305            drawHorizontalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
306                    dataset, series, item, crosshairState, pass);
307        }
308        else if (orientation == PlotOrientation.VERTICAL) {
309            drawVerticalItem(g2, dataArea, info, plot, domainAxis, rangeAxis,
310                    dataset, series, item, crosshairState, pass);
311        }
312
313    }
314
315    /**
316     * Draws the visual representation of a single data item.
317     *
318     * @param g2  the graphics device.
319     * @param dataArea  the area within which the plot is being drawn.
320     * @param info  collects info about the drawing.
321     * @param plot  the plot (can be used to obtain standard color
322     *              information etc).
323     * @param domainAxis  the domain axis.
324     * @param rangeAxis  the range axis.
325     * @param dataset  the dataset (must be an instance of
326     *                 {@link BoxAndWhiskerXYDataset}).
327     * @param series  the series index (zero-based).
328     * @param item  the item index (zero-based).
329     * @param crosshairState  crosshair information for the plot
330     *                        ({@code null} permitted).
331     * @param pass  the pass index.
332     */
333    public void drawHorizontalItem(Graphics2D g2, Rectangle2D dataArea,
334            PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis,
335            ValueAxis rangeAxis, XYDataset dataset, int series,
336            int item, CrosshairState crosshairState, int pass) {
337
338        // setup for collecting optional entity info...
339        EntityCollection entities = null;
340        if (info != null) {
341            entities = info.getOwner().getEntityCollection();
342        }
343
344        BoxAndWhiskerXYDataset boxAndWhiskerData
345                = (BoxAndWhiskerXYDataset) dataset;
346
347        Number x = boxAndWhiskerData.getX(series, item);
348        Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
349        Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
350        Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
351        Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
352        Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
353        Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
354
355        double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea,
356                plot.getDomainAxisEdge());
357
358        RectangleEdge location = plot.getRangeAxisEdge();
359        double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea,
360                location);
361        double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea,
362                location);
363        double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
364                dataArea, location);
365        double yyAverage = 0.0;
366        if (yAverage != null) {
367            yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(),
368                    dataArea, location);
369        }
370        double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(),
371                dataArea, location);
372        double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(),
373                dataArea, location);
374
375        double exactBoxWidth = getBoxWidth();
376        double width = exactBoxWidth;
377        double dataAreaX = dataArea.getHeight();
378        double maxBoxPercent = 0.1;
379        double maxBoxWidth = dataAreaX * maxBoxPercent;
380        if (exactBoxWidth <= 0.0) {
381            int itemCount = boxAndWhiskerData.getItemCount(series);
382            exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
383            if (exactBoxWidth < 3) {
384                width = 3;
385            }
386            else if (exactBoxWidth > maxBoxWidth) {
387                width = maxBoxWidth;
388            }
389            else {
390                width = exactBoxWidth;
391            }
392        }
393
394        g2.setPaint(getItemPaint(series, item));
395        Stroke s = getItemStroke(series, item);
396        g2.setStroke(s);
397
398        // draw the upper shadow
399        g2.draw(new Line2D.Double(yyMax, xx, yyQ3Median, xx));
400        g2.draw(new Line2D.Double(yyMax, xx - width / 2, yyMax,
401                xx + width / 2));
402
403        // draw the lower shadow
404        g2.draw(new Line2D.Double(yyMin, xx, yyQ1Median, xx));
405        g2.draw(new Line2D.Double(yyMin, xx - width / 2, yyMin,
406                xx + width / 2));
407
408        // draw the body
409        Shape box;
410        if (yyQ1Median < yyQ3Median) {
411            box = new Rectangle2D.Double(yyQ1Median, xx - width / 2,
412                    yyQ3Median - yyQ1Median, width);
413        }
414        else {
415            box = new Rectangle2D.Double(yyQ3Median, xx - width / 2,
416                    yyQ1Median - yyQ3Median, width);
417        }
418        if (this.fillBox) {
419            g2.setPaint(lookupBoxPaint(series, item));
420            g2.fill(box);
421        }
422        g2.setStroke(getItemOutlineStroke(series, item));
423        g2.setPaint(getItemOutlinePaint(series, item));
424        g2.draw(box);
425
426        // draw median
427        g2.setPaint(getArtifactPaint());
428        g2.draw(new Line2D.Double(yyMedian,
429                xx - width / 2, yyMedian, xx + width / 2));
430
431        // draw average - SPECIAL AIMS REQUIREMENT
432        if (yAverage != null) {
433            double aRadius = width / 4;
434            // here we check that the average marker will in fact be visible
435            // before drawing it...
436            if ((yyAverage > (dataArea.getMinX() - aRadius))
437                    && (yyAverage < (dataArea.getMaxX() + aRadius))) {
438                Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
439                        yyAverage - aRadius, xx - aRadius, aRadius * 2,
440                        aRadius * 2);
441                g2.fill(avgEllipse);
442                g2.draw(avgEllipse);
443            }
444        }
445
446        // FIXME: draw outliers
447
448        // add an entity for the item...
449        if (entities != null && box.intersects(dataArea)) {
450            addEntity(entities, box, dataset, series, item, yyAverage, xx);
451        }
452
453    }
454
455    /**
456     * Draws the visual representation of a single data item.
457     *
458     * @param g2  the graphics device.
459     * @param dataArea  the area within which the plot is being drawn.
460     * @param info  collects info about the drawing.
461     * @param plot  the plot (can be used to obtain standard color
462     *              information etc).
463     * @param domainAxis  the domain axis.
464     * @param rangeAxis  the range axis.
465     * @param dataset  the dataset (must be an instance of
466     *                 {@link BoxAndWhiskerXYDataset}).
467     * @param series  the series index (zero-based).
468     * @param item  the item index (zero-based).
469     * @param crosshairState  crosshair information for the plot
470     *                        ({@code null} permitted).
471     * @param pass  the pass index.
472     */
473    public void drawVerticalItem(Graphics2D g2, Rectangle2D dataArea,
474            PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis,
475            ValueAxis rangeAxis, XYDataset dataset, int series,
476            int item, CrosshairState crosshairState, int pass) {
477
478        // setup for collecting optional entity info...
479        EntityCollection entities = null;
480        if (info != null) {
481            entities = info.getOwner().getEntityCollection();
482        }
483
484        BoxAndWhiskerXYDataset boxAndWhiskerData
485            = (BoxAndWhiskerXYDataset) dataset;
486
487        Number x = boxAndWhiskerData.getX(series, item);
488        Number yMax = boxAndWhiskerData.getMaxRegularValue(series, item);
489        Number yMin = boxAndWhiskerData.getMinRegularValue(series, item);
490        Number yMedian = boxAndWhiskerData.getMedianValue(series, item);
491        Number yAverage = boxAndWhiskerData.getMeanValue(series, item);
492        Number yQ1Median = boxAndWhiskerData.getQ1Value(series, item);
493        Number yQ3Median = boxAndWhiskerData.getQ3Value(series, item);
494        List yOutliers = boxAndWhiskerData.getOutliers(series, item);
495        // yOutliers can be null, but we'd prefer it to be an empty list in
496        // that case...
497        if (yOutliers == null) {
498            yOutliers = Collections.EMPTY_LIST;
499        }
500
501        double xx = domainAxis.valueToJava2D(x.doubleValue(), dataArea,
502                plot.getDomainAxisEdge());
503
504        RectangleEdge location = plot.getRangeAxisEdge();
505        double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(), dataArea,
506                location);
507        double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(), dataArea,
508                location);
509        double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
510                dataArea, location);
511        double yyAverage = 0.0;
512        if (yAverage != null) {
513            yyAverage = rangeAxis.valueToJava2D(yAverage.doubleValue(),
514                    dataArea, location);
515        }
516        double yyQ1Median = rangeAxis.valueToJava2D(yQ1Median.doubleValue(),
517                dataArea, location);
518        double yyQ3Median = rangeAxis.valueToJava2D(yQ3Median.doubleValue(),
519                dataArea, location);
520        double yyOutlier;
521
522        double exactBoxWidth = getBoxWidth();
523        double width = exactBoxWidth;
524        double dataAreaX = dataArea.getMaxX() - dataArea.getMinX();
525        double maxBoxPercent = 0.1;
526        double maxBoxWidth = dataAreaX * maxBoxPercent;
527        if (exactBoxWidth <= 0.0) {
528            int itemCount = boxAndWhiskerData.getItemCount(series);
529            exactBoxWidth = dataAreaX / itemCount * 4.5 / 7;
530            if (exactBoxWidth < 3) {
531                width = 3;
532            }
533            else if (exactBoxWidth > maxBoxWidth) {
534                width = maxBoxWidth;
535            }
536            else {
537                width = exactBoxWidth;
538            }
539        }
540
541        g2.setPaint(getItemPaint(series, item));
542        Stroke s = getItemStroke(series, item);
543        g2.setStroke(s);
544
545        // draw the upper shadow
546        g2.draw(new Line2D.Double(xx, yyMax, xx, yyQ3Median));
547        g2.draw(new Line2D.Double(xx - width / 2, yyMax, xx + width / 2,
548                yyMax));
549
550        // draw the lower shadow
551        g2.draw(new Line2D.Double(xx, yyMin, xx, yyQ1Median));
552        g2.draw(new Line2D.Double(xx - width / 2, yyMin, xx + width / 2,
553                yyMin));
554
555        // draw the body
556        Shape box;
557        if (yyQ1Median > yyQ3Median) {
558            box = new Rectangle2D.Double(xx - width / 2, yyQ3Median, width,
559                    yyQ1Median - yyQ3Median);
560        }
561        else {
562            box = new Rectangle2D.Double(xx - width / 2, yyQ1Median, width,
563                    yyQ3Median - yyQ1Median);
564        }
565        if (this.fillBox) {
566            g2.setPaint(lookupBoxPaint(series, item));
567            g2.fill(box);
568        }
569        g2.setStroke(getItemOutlineStroke(series, item));
570        g2.setPaint(getItemOutlinePaint(series, item));
571        g2.draw(box);
572
573        // draw median
574        g2.setPaint(getArtifactPaint());
575        g2.draw(new Line2D.Double(xx - width / 2, yyMedian, xx + width / 2,
576                yyMedian));
577
578        double aRadius = 0;                 // average radius
579        double oRadius = width / 3;    // outlier radius
580
581        // draw average - SPECIAL AIMS REQUIREMENT
582        if (yAverage != null) {
583            aRadius = width / 4;
584            // here we check that the average marker will in fact be visible
585            // before drawing it...
586            if ((yyAverage > (dataArea.getMinY() - aRadius))
587                    && (yyAverage < (dataArea.getMaxY() + aRadius))) {
588                Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx - aRadius,
589                        yyAverage - aRadius, aRadius * 2, aRadius * 2);
590                g2.fill(avgEllipse);
591                g2.draw(avgEllipse);
592            }
593        }
594
595        List outliers = new ArrayList();
596        OutlierListCollection outlierListCollection
597                = new OutlierListCollection();
598
599        /* From outlier array sort out which are outliers and put these into
600         * an arraylist. If there are any farouts, set the flag on the
601         * OutlierListCollection
602         */
603        for (int i = 0; i < yOutliers.size(); i++) {
604            double outlier = ((Number) yOutliers.get(i)).doubleValue();
605            if (outlier > boxAndWhiskerData.getMaxOutlier(series,
606                    item).doubleValue()) {
607                outlierListCollection.setHighFarOut(true);
608            }
609            else if (outlier < boxAndWhiskerData.getMinOutlier(series,
610                    item).doubleValue()) {
611                outlierListCollection.setLowFarOut(true);
612            }
613            else if (outlier > boxAndWhiskerData.getMaxRegularValue(series,
614                    item).doubleValue()) {
615                yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
616                        location);
617                outliers.add(new Outlier(xx, yyOutlier, oRadius));
618            }
619            else if (outlier < boxAndWhiskerData.getMinRegularValue(series,
620                    item).doubleValue()) {
621                yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
622                        location);
623                outliers.add(new Outlier(xx, yyOutlier, oRadius));
624            }
625            Collections.sort(outliers);
626        }
627
628        // Process outliers. Each outlier is either added to the appropriate
629        // outlier list or a new outlier list is made
630        for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
631            Outlier outlier = (Outlier) iterator.next();
632            outlierListCollection.add(outlier);
633        }
634
635        // draw yOutliers
636        double maxAxisValue = rangeAxis.valueToJava2D(rangeAxis.getUpperBound(),
637                dataArea, location) + aRadius;
638        double minAxisValue = rangeAxis.valueToJava2D(rangeAxis.getLowerBound(),
639                dataArea, location) - aRadius;
640
641        // draw outliers
642        for (Iterator iterator = outlierListCollection.iterator();
643                iterator.hasNext();) {
644            OutlierList list = (OutlierList) iterator.next();
645            Outlier outlier = list.getAveragedOutlier();
646            Point2D point = outlier.getPoint();
647
648            if (list.isMultiple()) {
649                drawMultipleEllipse(point, width, oRadius, g2);
650            }
651            else {
652                drawEllipse(point, oRadius, g2);
653            }
654        }
655
656        // draw farout
657        if (outlierListCollection.isHighFarOut()) {
658            drawHighFarOut(aRadius, g2, xx, maxAxisValue);
659        }
660
661        if (outlierListCollection.isLowFarOut()) {
662            drawLowFarOut(aRadius, g2, xx, minAxisValue);
663        }
664
665        // add an entity for the item...
666        if (entities != null && box.intersects(dataArea)) {
667            addEntity(entities, box, dataset, series, item, xx, yyAverage);
668        }
669
670    }
671
672    /**
673     * Draws an ellipse to represent an outlier.
674     *
675     * @param point  the location.
676     * @param oRadius  the radius.
677     * @param g2  the graphics device.
678     */
679    protected void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
680        Ellipse2D.Double dot = new Ellipse2D.Double(point.getX() + oRadius / 2,
681                point.getY(), oRadius, oRadius);
682        g2.draw(dot);
683    }
684
685    /**
686     * Draws two ellipses to represent overlapping outliers.
687     *
688     * @param point  the location.
689     * @param boxWidth  the box width.
690     * @param oRadius  the radius.
691     * @param g2  the graphics device.
692     */
693    protected void drawMultipleEllipse(Point2D point, double boxWidth,
694                                       double oRadius, Graphics2D g2) {
695
696        Ellipse2D.Double dot1 = new Ellipse2D.Double(point.getX()
697                - (boxWidth / 2) + oRadius, point.getY(), oRadius, oRadius);
698        Ellipse2D.Double dot2 = new Ellipse2D.Double(point.getX()
699                + (boxWidth / 2), point.getY(), oRadius, oRadius);
700        g2.draw(dot1);
701        g2.draw(dot2);
702
703    }
704
705    /**
706     * Draws a triangle to indicate the presence of far out values.
707     *
708     * @param aRadius  the radius.
709     * @param g2  the graphics device.
710     * @param xx  the x value.
711     * @param m  the max y value.
712     */
713    protected void drawHighFarOut(double aRadius, Graphics2D g2, double xx,
714            double m) {
715        double side = aRadius * 2;
716        g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
717        g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
718        g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
719    }
720
721    /**
722     * Draws a triangle to indicate the presence of far out values.
723     *
724     * @param aRadius  the radius.
725     * @param g2  the graphics device.
726     * @param xx  the x value.
727     * @param m  the min y value.
728     */
729    protected void drawLowFarOut(double aRadius, Graphics2D g2, double xx,
730            double m) {
731        double side = aRadius * 2;
732        g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
733        g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
734        g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
735    }
736
737    /**
738     * Tests this renderer for equality with another object.
739     *
740     * @param obj  the object ({@code null} permitted).
741     *
742     * @return {@code true} or {@code false}.
743     */
744    @Override
745    public boolean equals(Object obj) {
746        if (obj == this) {
747            return true;
748        }
749        if (!(obj instanceof XYBoxAndWhiskerRenderer)) {
750            return false;
751        }
752        if (!super.equals(obj)) {
753            return false;
754        }
755        XYBoxAndWhiskerRenderer that = (XYBoxAndWhiskerRenderer) obj;
756        if (this.boxWidth != that.getBoxWidth()) {
757            return false;
758        }
759        if (!PaintUtils.equal(this.boxPaint, that.boxPaint)) {
760            return false;
761        }
762        if (!PaintUtils.equal(this.artifactPaint, that.artifactPaint)) {
763            return false;
764        }
765        if (this.fillBox != that.fillBox) {
766            return false;
767        }
768        return true;
769
770    }
771
772    /**
773     * Provides serialization support.
774     *
775     * @param stream  the output stream.
776     *
777     * @throws IOException  if there is an I/O error.
778     */
779    private void writeObject(ObjectOutputStream stream) throws IOException {
780        stream.defaultWriteObject();
781        SerialUtils.writePaint(this.boxPaint, stream);
782        SerialUtils.writePaint(this.artifactPaint, stream);
783    }
784
785    /**
786     * Provides serialization support.
787     *
788     * @param stream  the input stream.
789     *
790     * @throws IOException  if there is an I/O error.
791     * @throws ClassNotFoundException  if there is a classpath problem.
792     */
793    private void readObject(ObjectInputStream stream)
794        throws IOException, ClassNotFoundException {
795
796        stream.defaultReadObject();
797        this.boxPaint = SerialUtils.readPaint(stream);
798        this.artifactPaint = SerialUtils.readPaint(stream);
799    }
800
801    /**
802     * Returns a clone of the renderer.
803     *
804     * @return A clone.
805     *
806     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
807     */
808    @Override
809    public Object clone() throws CloneNotSupportedException {
810        return super.clone();
811    }
812
813}