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 * SimpleHistogramDataset.java
029 * ---------------------------
030 * (C) Copyright 2005-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Sergei Ivanov;
034 *
035 */
036
037package org.jfree.data.statistics;
038
039import java.io.Serializable;
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.Iterator;
043import java.util.List;
044import java.util.Objects;
045import org.jfree.chart.internal.Args;
046import org.jfree.chart.internal.CloneUtils;
047import org.jfree.chart.api.PublicCloneable;
048
049import org.jfree.data.DomainOrder;
050import org.jfree.data.general.DatasetChangeEvent;
051import org.jfree.data.xy.AbstractIntervalXYDataset;
052import org.jfree.data.xy.IntervalXYDataset;
053
054/**
055 * A dataset used for creating simple histograms with custom defined bins.
056 *
057 * @see HistogramDataset
058 */
059public class SimpleHistogramDataset<K extends Comparable<K>> 
060        extends AbstractIntervalXYDataset implements IntervalXYDataset, 
061        Cloneable, PublicCloneable, Serializable {
062
063    /** For serialization. */
064    private static final long serialVersionUID = 7997996479768018443L;
065
066    /** The series key. */
067    private K key;
068
069    /** The bins. */
070    private List<SimpleHistogramBin> bins;
071
072    /**
073     * A flag that controls whether or not the bin count is divided by the
074     * bin size.
075     */
076    private boolean adjustForBinSize;
077
078    /**
079     * Creates a new histogram dataset.  Note that the
080     * {@code adjustForBinSize} flag defaults to {@code true}.
081     *
082     * @param key  the series key ({@code null} not permitted).
083     */
084    public SimpleHistogramDataset(K key) {
085        Args.nullNotPermitted(key, "key");
086        this.key = key;
087        this.bins = new ArrayList<>();
088        this.adjustForBinSize = true;
089    }
090
091    /**
092     * Returns a flag that controls whether or not the bin count is divided by
093     * the bin size in the {@link #getXValue(int, int)} method.
094     *
095     * @return A boolean.
096     *
097     * @see #setAdjustForBinSize(boolean)
098     */
099    public boolean getAdjustForBinSize() {
100        return this.adjustForBinSize;
101    }
102
103    /**
104     * Sets the flag that controls whether or not the bin count is divided by
105     * the bin size in the {@link #getYValue(int, int)} method, and sends a
106     * {@link DatasetChangeEvent} to all registered listeners.
107     *
108     * @param adjust  the flag.
109     *
110     * @see #getAdjustForBinSize()
111     */
112    public void setAdjustForBinSize(boolean adjust) {
113        this.adjustForBinSize = adjust;
114        notifyListeners(new DatasetChangeEvent(this, this));
115    }
116
117    /**
118     * Returns the number of series in the dataset (always 1 for this dataset).
119     *
120     * @return The series count.
121     */
122    @Override
123    public int getSeriesCount() {
124        return 1;
125    }
126
127    /**
128     * Returns the key for a series.  Since this dataset only stores a single
129     * series, the {@code series} argument is ignored.
130     *
131     * @param series  the series (zero-based index, ignored in this dataset).
132     *
133     * @return The key for the series.
134     */
135    @Override
136    public K getSeriesKey(int series) {
137        return this.key;
138    }
139
140    /**
141     * Returns the order of the domain (or X) values returned by the dataset.
142     *
143     * @return The order (never {@code null}).
144     */
145    @Override
146    public DomainOrder getDomainOrder() {
147        return DomainOrder.ASCENDING;
148    }
149
150    /**
151     * Returns the number of items in a series.  Since this dataset only stores
152     * a single series, the {@code series} argument is ignored.
153     *
154     * @param series  the series index (zero-based, ignored in this dataset).
155     *
156     * @return The item count.
157     */
158    @Override
159    public int getItemCount(int series) {
160        return this.bins.size();
161    }
162
163    /**
164     * Adds a bin to the dataset.  An exception is thrown if the bin overlaps
165     * with any existing bin in the dataset.
166     *
167     * @param binToAdd  the bin ({@code null} not permitted).
168     *
169     * @see #removeAllBins()
170     */
171    public void addBin(SimpleHistogramBin binToAdd) {
172        // check that the new bin doesn't overlap with any existing bin
173        for (SimpleHistogramBin bin : this.bins) {
174            if (binToAdd.overlapsWith(bin)) {
175                throw new RuntimeException("Overlapping bin");
176            }
177        }
178        this.bins.add(binToAdd);
179        Collections.sort(this.bins);
180    }
181
182    /**
183     * Adds an observation to the dataset (by incrementing the item count for
184     * the appropriate bin).  A runtime exception is thrown if the value does
185     * not fit into any bin.
186     *
187     * @param value  the value.
188     */
189    public void addObservation(double value) {
190        addObservation(value, true);
191    }
192
193    /**
194     * Adds an observation to the dataset (by incrementing the item count for
195     * the appropriate bin).  A runtime exception is thrown if the value does
196     * not fit into any bin.
197     *
198     * @param value  the value.
199     * @param notify  send {@link DatasetChangeEvent} to listeners?
200     */
201    public void addObservation(double value, boolean notify) {
202        boolean placed = false;
203        Iterator iterator = this.bins.iterator();
204        while (iterator.hasNext() && !placed) {
205            SimpleHistogramBin bin = (SimpleHistogramBin) iterator.next();
206            if (bin.accepts(value)) {
207                bin.setItemCount(bin.getItemCount() + 1);
208                placed = true;
209            }
210        }
211        if (!placed) {
212            throw new RuntimeException("No bin.");
213        }
214        if (notify) {
215            notifyListeners(new DatasetChangeEvent(this, this));
216        }
217    }
218
219    /**
220     * Adds a set of values to the dataset and sends a
221     * {@link DatasetChangeEvent} to all registered listeners.
222     *
223     * @param values  the values ({@code null} not permitted).
224     *
225     * @see #clearObservations()
226     */
227    public void addObservations(double[] values) {
228        for (int i = 0; i < values.length; i++) {
229            addObservation(values[i], false);
230        }
231        notifyListeners(new DatasetChangeEvent(this, this));
232    }
233
234    /**
235     * Removes all current observation data and sends a
236     * {@link DatasetChangeEvent} to all registered listeners.
237     *
238     * @since 1.0.6
239     *
240     * @see #addObservations(double[])
241     * @see #removeAllBins()
242     */
243    public void clearObservations() {
244        for (SimpleHistogramBin bin : this.bins) {
245            bin.setItemCount(0);
246        }
247        notifyListeners(new DatasetChangeEvent(this, this));
248    }
249
250    /**
251     * Removes all bins and sends a {@link DatasetChangeEvent} to all
252     * registered listeners.
253     *
254     * @since 1.0.6
255     *
256     * @see #addBin(SimpleHistogramBin)
257     */
258    public void removeAllBins() {
259        this.bins = new ArrayList<>();
260        notifyListeners(new DatasetChangeEvent(this, this));
261    }
262
263    /**
264     * Returns the x-value for an item within a series.  The x-values may or
265     * may not be returned in ascending order, that is up to the class
266     * implementing the interface.
267     *
268     * @param series  the series index (zero-based).
269     * @param item  the item index (zero-based).
270     *
271     * @return The x-value (never {@code null}).
272     */
273    @Override
274    public Number getX(int series, int item) {
275        return getXValue(series, item);
276    }
277
278    /**
279     * Returns the x-value (as a double primitive) for an item within a series.
280     *
281     * @param series  the series index (zero-based).
282     * @param item  the item index (zero-based).
283     *
284     * @return The x-value.
285     */
286    @Override
287    public double getXValue(int series, int item) {
288        SimpleHistogramBin bin = this.bins.get(item);
289        return (bin.getLowerBound() + bin.getUpperBound()) / 2.0;
290    }
291
292    /**
293     * Returns the y-value for an item within a series.
294     *
295     * @param series  the series index (zero-based).
296     * @param item  the item index (zero-based).
297     *
298     * @return The y-value (possibly {@code null}).
299     */
300    @Override
301    public Number getY(int series, int item) {
302        return getYValue(series, item);
303    }
304
305    /**
306     * Returns the y-value (as a double primitive) for an item within a series.
307     *
308     * @param series  the series index (zero-based).
309     * @param item  the item index (zero-based).
310     *
311     * @return The y-value.
312     *
313     * @see #getAdjustForBinSize()
314     */
315    @Override
316    public double getYValue(int series, int item) {
317        SimpleHistogramBin bin = this.bins.get(item);
318        if (this.adjustForBinSize) {
319            return bin.getItemCount()
320                   / (bin.getUpperBound() - bin.getLowerBound());
321        }
322        else {
323            return bin.getItemCount();
324        }
325    }
326
327    /**
328     * Returns the starting X value for the specified series and item.
329     *
330     * @param series  the series index (zero-based).
331     * @param item  the item index (zero-based).
332     *
333     * @return The value.
334     */
335    @Override
336    public Number getStartX(int series, int item) {
337        return getStartXValue(series, item);
338    }
339
340    /**
341     * Returns the start x-value (as a double primitive) for an item within a
342     * series.
343     *
344     * @param series  the series (zero-based index).
345     * @param item  the item (zero-based index).
346     *
347     * @return The start x-value.
348     */
349    @Override
350    public double getStartXValue(int series, int item) {
351        SimpleHistogramBin bin = this.bins.get(item);
352        return bin.getLowerBound();
353    }
354
355    /**
356     * Returns the ending X value for the specified series and item.
357     *
358     * @param series  the series index (zero-based).
359     * @param item  the item index (zero-based).
360     *
361     * @return The value.
362     */
363    @Override
364    public Number getEndX(int series, int item) {
365        return getEndXValue(series, item);
366    }
367
368    /**
369     * Returns the end x-value (as a double primitive) for an item within a
370     * series.
371     *
372     * @param series  the series index (zero-based).
373     * @param item  the item index (zero-based).
374     *
375     * @return The end x-value.
376     */
377    @Override
378    public double getEndXValue(int series, int item) {
379        SimpleHistogramBin bin = this.bins.get(item);
380        return bin.getUpperBound();
381    }
382
383    /**
384     * Returns the starting Y value for the specified series and item.
385     *
386     * @param series  the series index (zero-based).
387     * @param item  the item index (zero-based).
388     *
389     * @return The value.
390     */
391    @Override
392    public Number getStartY(int series, int item) {
393        return getY(series, item);
394    }
395
396    /**
397     * Returns the start y-value (as a double primitive) for an item within a
398     * series.
399     *
400     * @param series  the series index (zero-based).
401     * @param item  the item index (zero-based).
402     *
403     * @return The start y-value.
404     */
405    @Override
406    public double getStartYValue(int series, int item) {
407        return getYValue(series, item);
408    }
409
410    /**
411     * Returns the ending Y value for the specified series and item.
412     *
413     * @param series  the series index (zero-based).
414     * @param item  the item index (zero-based).
415     *
416     * @return The value.
417     */
418    @Override
419    public Number getEndY(int series, int item) {
420        return getY(series, item);
421    }
422
423    /**
424     * Returns the end y-value (as a double primitive) for an item within a
425     * series.
426     *
427     * @param series  the series index (zero-based).
428     * @param item  the item index (zero-based).
429     *
430     * @return The end y-value.
431     */
432    @Override
433    public double getEndYValue(int series, int item) {
434        return getYValue(series, item);
435    }
436
437    /**
438     * Compares the dataset for equality with an arbitrary object.
439     *
440     * @param obj  the object ({@code null} permitted).
441     *
442     * @return A boolean.
443     */
444    @Override
445    public boolean equals(Object obj) {
446        if (obj == this) {
447            return true;
448        }
449        if (!(obj instanceof SimpleHistogramDataset)) {
450            return false;
451        }
452        SimpleHistogramDataset that = (SimpleHistogramDataset) obj;
453        if (!this.key.equals(that.key)) {
454            return false;
455        }
456        if (this.adjustForBinSize != that.adjustForBinSize) {
457            return false;
458        }
459        if (!this.bins.equals(that.bins)) {
460            return false;
461        }
462        return true;
463    }
464
465    @Override
466    public int hashCode(){
467        int hash = 7;
468        hash = 11 * hash + Objects.hashCode(this.key);
469        hash = 11 * hash + Objects.hashCode(this.bins);
470        hash = 11 * hash + (this.adjustForBinSize ? 1 : 0);
471        return hash;
472    }
473
474    /**
475     * Returns a clone of the dataset.
476     *
477     * @return A clone.
478     *
479     * @throws CloneNotSupportedException not thrown by this class, but maybe
480     *         by subclasses (if any).
481     */
482    @Override
483    public Object clone() throws CloneNotSupportedException {
484        SimpleHistogramDataset clone = (SimpleHistogramDataset) super.clone();
485        clone.bins = CloneUtils.cloneList(this.bins);
486        return clone;
487    }
488
489}