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