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 * DefaultBoxAndWhiskerXYDataset.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.data.statistics;
039
040import java.util.ArrayList;
041import java.util.Date;
042import java.util.List;
043import java.util.Objects;
044
045import org.jfree.data.Range;
046import org.jfree.data.RangeInfo;
047import org.jfree.data.general.DatasetChangeEvent;
048import org.jfree.data.xy.AbstractXYDataset;
049
050/**
051 * A simple implementation of the {@link BoxAndWhiskerXYDataset} interface.
052 * This dataset implementation can hold only one series.
053 */
054public class DefaultBoxAndWhiskerXYDataset extends AbstractXYDataset
055            implements BoxAndWhiskerXYDataset, RangeInfo {
056
057    /** The series key. */
058    private Comparable seriesKey;
059
060    /** Storage for the dates. */
061    private List dates;
062
063    /** Storage for the box and whisker statistics. */
064    private List items;
065
066    /** The minimum range value. */
067    private Number minimumRangeValue;
068
069    /** The maximum range value. */
070    private Number maximumRangeValue;
071
072    /** The range of values. */
073    private Range rangeBounds;
074
075    /**
076     * The coefficient used to calculate outliers. Tukey's default value is
077     * 1.5 (see EDA) Any value which is greater than Q3 + (interquartile range
078     * * outlier coefficient) is considered to be an outlier.  Can be altered
079     * if the data is particularly skewed.
080     */
081    private double outlierCoefficient = 1.5;
082
083    /**
084     * The coefficient used to calculate farouts. Tukey's default value is 2
085     * (see EDA) Any value which is greater than Q3 + (interquartile range *
086     * farout coefficient) is considered to be a farout.  Can be altered if the
087     * data is particularly skewed.
088     */
089    private double faroutCoefficient = 2.0;
090
091    /**
092     * Constructs a new box and whisker dataset.
093     * <p>
094     * The current implementation allows only one series in the dataset.
095     * This may be extended in a future version.
096     *
097     * @param seriesKey  the key for the series.
098     */
099    public DefaultBoxAndWhiskerXYDataset(Comparable seriesKey) {
100        this.seriesKey = seriesKey;
101        this.dates = new ArrayList();
102        this.items = new ArrayList();
103        this.minimumRangeValue = null;
104        this.maximumRangeValue = null;
105        this.rangeBounds = null;
106    }
107
108    /**
109     * Returns the value used as the outlier coefficient. The outlier
110     * coefficient gives an indication of the degree of certainty in an
111     * unskewed distribution.  Increasing the coefficient increases the number
112     * of values included. Currently only used to ensure farout coefficient is
113     * greater than the outlier coefficient
114     *
115     * @return A {@code double} representing the value used to calculate
116     *         outliers.
117     *
118     * @see #setOutlierCoefficient(double)
119     */
120    @Override
121    public double getOutlierCoefficient() {
122        return this.outlierCoefficient;
123    }
124
125    /**
126     * Sets the value used as the outlier coefficient
127     *
128     * @param outlierCoefficient  being a {@code double} representing the
129     *                            value used to calculate outliers.
130     *
131     * @see #getOutlierCoefficient()
132     */
133    public void setOutlierCoefficient(double outlierCoefficient) {
134        this.outlierCoefficient = outlierCoefficient;
135    }
136
137    /**
138     * Returns the value used as the farout coefficient. The farout coefficient
139     * allows the calculation of which values will be off the graph.
140     *
141     * @return A {@code double} representing the value used to calculate
142     *         farouts.
143     *
144     * @see #setFaroutCoefficient(double)
145     */
146    @Override
147    public double getFaroutCoefficient() {
148        return this.faroutCoefficient;
149    }
150
151    /**
152     * Sets the value used as the farouts coefficient. The farout coefficient
153     * must b greater than the outlier coefficient.
154     *
155     * @param faroutCoefficient being a {@code double} representing the
156     *                          value used to calculate farouts.
157     *
158     * @see #getFaroutCoefficient()
159     */
160    public void setFaroutCoefficient(double faroutCoefficient) {
161
162        if (faroutCoefficient > getOutlierCoefficient()) {
163            this.faroutCoefficient = faroutCoefficient;
164        }
165        else {
166            throw new IllegalArgumentException("Farout value must be greater "
167                + "than the outlier value, which is currently set at: ("
168                + getOutlierCoefficient() + ")");
169        }
170    }
171
172    /**
173     * Returns the number of series in the dataset.
174     * <p>
175     * This implementation only allows one series.
176     *
177     * @return The number of series.
178     */
179    @Override
180    public int getSeriesCount() {
181        return 1;
182    }
183
184    /**
185     * Returns the number of items in the specified series.
186     *
187     * @param series  the index (zero-based) of the series.
188     *
189     * @return The number of items in the specified series.
190     */
191    @Override
192    public int getItemCount(int series) {
193        return this.dates.size();
194    }
195
196    /**
197     * Adds an item to the dataset and sends a {@link DatasetChangeEvent} to
198     * all registered listeners.
199     *
200     * @param date  the date ({@code null} not permitted).
201     * @param item  the item ({@code null} not permitted).
202     */
203    public void add(Date date, BoxAndWhiskerItem item) {
204        this.dates.add(date);
205        this.items.add(item);
206        if (this.minimumRangeValue == null) {
207            this.minimumRangeValue = item.getMinRegularValue();
208        }
209        else {
210            if (item.getMinRegularValue().doubleValue()
211                    < this.minimumRangeValue.doubleValue()) {
212                this.minimumRangeValue = item.getMinRegularValue();
213            }
214        }
215        if (this.maximumRangeValue == null) {
216            this.maximumRangeValue = item.getMaxRegularValue();
217        }
218        else {
219            if (item.getMaxRegularValue().doubleValue()
220                    > this.maximumRangeValue.doubleValue()) {
221                this.maximumRangeValue = item.getMaxRegularValue();
222            }
223        }
224        this.rangeBounds = new Range(this.minimumRangeValue.doubleValue(),
225                this.maximumRangeValue.doubleValue());
226        fireDatasetChanged();
227    }
228
229    /**
230     * Returns the name of the series stored in this dataset.
231     *
232     * @param i  the index of the series. Currently ignored.
233     *
234     * @return The name of this series.
235     */
236    @Override
237    public Comparable getSeriesKey(int i) {
238        return this.seriesKey;
239    }
240
241    /**
242     * Return an item from within the dataset.
243     *
244     * @param series  the series index (ignored, since this dataset contains
245     *                only one series).
246     * @param item  the item within the series (zero-based index)
247     *
248     * @return The item.
249     */
250    public BoxAndWhiskerItem getItem(int series, int item) {
251        return (BoxAndWhiskerItem) this.items.get(item);
252    }
253
254    /**
255     * Returns the x-value for one item in a series.
256     * <p>
257     * The value returned is a Long object generated from the underlying Date
258     * object.
259     *
260     * @param series  the series (zero-based index).
261     * @param item  the item (zero-based index).
262     *
263     * @return The x-value.
264     */
265    @Override
266    public Number getX(int series, int item) {
267        return ((Date) this.dates.get(item)).getTime();
268    }
269
270    /**
271     * Returns the x-value for one item in a series, as a Date.
272     * <p>
273     * This method is provided for convenience only.
274     *
275     * @param series  the series (zero-based index).
276     * @param item  the item (zero-based index).
277     *
278     * @return The x-value as a Date.
279     */
280    public Date getXDate(int series, int item) {
281        return (Date) this.dates.get(item);
282    }
283
284    /**
285     * Returns the y-value for one item in a series.
286     * <p>
287     * This method (from the XYDataset interface) is mapped to the
288     * getMeanValue() method.
289     *
290     * @param series  the series (zero-based index).
291     * @param item  the item (zero-based index).
292     *
293     * @return The y-value.
294     */
295    @Override
296    public Number getY(int series, int item) {
297        return getMeanValue(series, item);
298    }
299
300    /**
301     * Returns the mean for the specified series and item.
302     *
303     * @param series  the series (zero-based index).
304     * @param item  the item (zero-based index).
305     *
306     * @return The mean for the specified series and item.
307     */
308    @Override
309    public Number getMeanValue(int series, int item) {
310        Number result = null;
311        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
312        if (stats != null) {
313            result = stats.getMean();
314        }
315        return result;
316    }
317
318    /**
319     * Returns the median-value for the specified series and item.
320     *
321     * @param series  the series (zero-based index).
322     * @param item  the item (zero-based index).
323     *
324     * @return The median-value for the specified series and item.
325     */
326    @Override
327    public Number getMedianValue(int series, int item) {
328        Number result = null;
329        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
330        if (stats != null) {
331            result = stats.getMedian();
332        }
333        return result;
334    }
335
336    /**
337     * Returns the Q1 median-value for the specified series and item.
338     *
339     * @param series  the series (zero-based index).
340     * @param item  the item (zero-based index).
341     *
342     * @return The Q1 median-value for the specified series and item.
343     */
344    @Override
345    public Number getQ1Value(int series, int item) {
346        Number result = null;
347        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
348        if (stats != null) {
349            result = stats.getQ1();
350        }
351        return result;
352    }
353
354    /**
355     * Returns the Q3 median-value for the specified series and item.
356     *
357     * @param series  the series (zero-based index).
358     * @param item  the item (zero-based index).
359     *
360     * @return The Q3 median-value for the specified series and item.
361     */
362    @Override
363    public Number getQ3Value(int series, int item) {
364        Number result = null;
365        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
366        if (stats != null) {
367            result = stats.getQ3();
368        }
369        return result;
370    }
371
372    /**
373     * Returns the min-value for the specified series and item.
374     *
375     * @param series  the series (zero-based index).
376     * @param item  the item (zero-based index).
377     *
378     * @return The min-value for the specified series and item.
379     */
380    @Override
381    public Number getMinRegularValue(int series, int item) {
382        Number result = null;
383        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
384        if (stats != null) {
385            result = stats.getMinRegularValue();
386        }
387        return result;
388    }
389
390    /**
391     * Returns the max-value for the specified series and item.
392     *
393     * @param series  the series (zero-based index).
394     * @param item  the item (zero-based index).
395     *
396     * @return The max-value for the specified series and item.
397     */
398    @Override
399    public Number getMaxRegularValue(int series, int item) {
400        Number result = null;
401        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
402        if (stats != null) {
403            result = stats.getMaxRegularValue();
404        }
405        return result;
406    }
407
408    /**
409     * Returns the minimum value which is not a farout.
410     * @param series  the series (zero-based index).
411     * @param item  the item (zero-based index).
412     *
413     * @return A {@code Number} representing the maximum non-farout value.
414     */
415    @Override
416    public Number getMinOutlier(int series, int item) {
417        Number result = null;
418        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
419        if (stats != null) {
420            result = stats.getMinOutlier();
421        }
422        return result;
423    }
424
425    /**
426     * Returns the maximum value which is not a farout, ie Q3 + (interquartile
427     * range * farout coefficient).
428     *
429     * @param series  the series (zero-based index).
430     * @param item  the item (zero-based index).
431     *
432     * @return A {@code Number} representing the maximum non-farout value.
433     */
434    @Override
435    public Number getMaxOutlier(int series, int item) {
436        Number result = null;
437        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
438        if (stats != null) {
439            result = stats.getMaxOutlier();
440        }
441        return result;
442    }
443
444    /**
445     * Returns a list of outliers for the specified series and item.
446     *
447     * @param series  the series (zero-based index).
448     * @param item  the item (zero-based index).
449     *
450     * @return The list of outliers for the specified series and item
451     *         (possibly {@code null}).
452     */
453    @Override
454    public List getOutliers(int series, int item) {
455        List result = null;
456        BoxAndWhiskerItem stats = (BoxAndWhiskerItem) this.items.get(item);
457        if (stats != null) {
458            result = stats.getOutliers();
459        }
460        return result;
461    }
462
463    /**
464     * Returns the minimum y-value in the dataset.
465     *
466     * @param includeInterval  a flag that determines whether or not the
467     *                         y-interval is taken into account.
468     *
469     * @return The minimum value.
470     */
471    @Override
472    public double getRangeLowerBound(boolean includeInterval) {
473        double result = Double.NaN;
474        if (this.minimumRangeValue != null) {
475            result = this.minimumRangeValue.doubleValue();
476        }
477        return result;
478    }
479
480    /**
481     * Returns the maximum y-value in the dataset.
482     *
483     * @param includeInterval  a flag that determines whether or not the
484     *                         y-interval is taken into account.
485     *
486     * @return The maximum value.
487     */
488    @Override
489    public double getRangeUpperBound(boolean includeInterval) {
490        double result = Double.NaN;
491        if (this.maximumRangeValue != null) {
492            result = this.maximumRangeValue.doubleValue();
493        }
494        return result;
495    }
496
497    /**
498     * Returns the range of the values in this dataset's range.
499     *
500     * @param includeInterval  a flag that determines whether or not the
501     *                         y-interval is taken into account.
502     *
503     * @return The range.
504     */
505    @Override
506    public Range getRangeBounds(boolean includeInterval) {
507        return this.rangeBounds;
508    }
509
510    /**
511     * Tests this dataset for equality with an arbitrary object.
512     *
513     * @param obj  the object ({@code null} permitted).
514     *
515     * @return A boolean.
516     */
517    @Override
518    public boolean equals(Object obj) {
519        if (obj == this) {
520            return true;
521        }
522        if (!(obj instanceof DefaultBoxAndWhiskerXYDataset)) {
523            return false;
524        }
525        DefaultBoxAndWhiskerXYDataset that
526                = (DefaultBoxAndWhiskerXYDataset) obj;
527        if (!Objects.equals(this.seriesKey, that.seriesKey)) {
528            return false;
529        }
530        if (!this.dates.equals(that.dates)) {
531            return false;
532        }
533        if (!this.items.equals(that.items)) {
534            return false;
535        }
536        return true;
537    }
538
539    /**
540     * Returns a clone of the plot.
541     *
542     * @return A clone.
543     *
544     * @throws CloneNotSupportedException  if the cloning is not supported.
545     */
546    @Override
547    public Object clone() throws CloneNotSupportedException {
548        DefaultBoxAndWhiskerXYDataset clone
549                = (DefaultBoxAndWhiskerXYDataset) super.clone();
550        clone.dates = new java.util.ArrayList(this.dates);
551        clone.items = new java.util.ArrayList(this.items);
552        return clone;
553    }
554
555}