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 * HistogramDataset.java
029 * ---------------------
030 * (C) Copyright 2003-2021, by Jelai Wang and Contributors.
031 *
032 * Original Author:  Jelai Wang (jelaiw AT mindspring.com);
033 * Contributor(s):   David Gilbert;
034 *                   Cameron Hayne;
035 *                   Rikard Bj?rklind;
036 *                   Thomas A Caswell (patch 2902842);
037 *
038 */
039
040package org.jfree.data.statistics;
041
042import java.io.Serializable;
043import java.util.ArrayList;
044import java.util.HashMap;
045import java.util.List;
046import java.util.Map;
047import java.util.Objects;
048
049import org.jfree.chart.internal.Args;
050import org.jfree.chart.api.PublicCloneable;
051
052import org.jfree.data.general.DatasetChangeEvent;
053import org.jfree.data.xy.AbstractIntervalXYDataset;
054import org.jfree.data.xy.IntervalXYDataset;
055
056/**
057 * A dataset that can be used for creating histograms.
058 *
059 * @see SimpleHistogramDataset
060 */
061public class HistogramDataset extends AbstractIntervalXYDataset
062        implements IntervalXYDataset, Cloneable, PublicCloneable,
063                   Serializable {
064
065    /** For serialization. */
066    private static final long serialVersionUID = -6341668077370231153L;
067
068    /** A list of maps. */
069    private List<Map<String, Object>> list;
070
071    /** The histogram type. */
072    private HistogramType type;
073
074    /**
075     * Creates a new (empty) dataset with a default type of
076     * {@link HistogramType}.FREQUENCY.
077     */
078    public HistogramDataset() {
079        this.list = new ArrayList<>();
080        this.type = HistogramType.FREQUENCY;
081    }
082
083    /**
084     * Returns the histogram type.
085     *
086     * @return The type (never {@code null}).
087     */
088    public HistogramType getType() {
089        return this.type;
090    }
091
092    /**
093     * Sets the histogram type and sends a {@link DatasetChangeEvent} to all
094     * registered listeners.
095     *
096     * @param type  the type ({@code null} not permitted).
097     */
098    public void setType(HistogramType type) {
099        Args.nullNotPermitted(type, "type");
100        this.type = type;
101        fireDatasetChanged();
102    }
103
104    /**
105     * Adds a series to the dataset, using the specified number of bins,
106     * and sends a {@link DatasetChangeEvent} to all registered listeners.
107     *
108     * @param key  the series key ({@code null} not permitted).
109     * @param values the values ({@code null} not permitted).
110     * @param bins  the number of bins (must be at least 1).
111     */
112    public void addSeries(Comparable key, double[] values, int bins) {
113        // defer argument checking...
114        double minimum = getMinimum(values);
115        double maximum = getMaximum(values);
116        addSeries(key, values, bins, minimum, maximum);
117    }
118
119    /**
120     * Adds a series to the dataset. Any data value less than minimum will be
121     * assigned to the first bin, and any data value greater than maximum will
122     * be assigned to the last bin.  Values falling on the boundary of
123     * adjacent bins will be assigned to the higher indexed bin.
124     *
125     * @param key  the series key ({@code null} not permitted).
126     * @param values  the raw observations.
127     * @param bins  the number of bins (must be at least 1).
128     * @param minimum  the lower bound of the bin range.
129     * @param maximum  the upper bound of the bin range.
130     */
131    public void addSeries(Comparable key, double[] values, int bins,
132            double minimum, double maximum) {
133
134        Args.nullNotPermitted(key, "key");
135        Args.nullNotPermitted(values, "values");
136        if (bins < 1) {
137            throw new IllegalArgumentException(
138                    "The 'bins' value must be at least 1.");
139        }
140        double binWidth = (maximum - minimum) / bins;
141
142        double lower = minimum;
143        double upper;
144        List<HistogramBin> binList = new ArrayList<>(bins);
145        for (int i = 0; i < bins; i++) {
146            HistogramBin bin;
147            // make sure bins[bins.length]'s upper boundary ends at maximum
148            // to avoid the rounding issue. the bins[0] lower boundary is
149            // guaranteed start from min
150            if (i == bins - 1) {
151                bin = new HistogramBin(lower, maximum);
152            }
153            else {
154                upper = minimum + (i + 1) * binWidth;
155                bin = new HistogramBin(lower, upper);
156                lower = upper;
157            }
158            binList.add(bin);
159        }
160        // fill the bins
161        for (int i = 0; i < values.length; i++) {
162            int binIndex = bins - 1;
163            if (values[i] < maximum) {
164                double fraction = (values[i] - minimum) / (maximum - minimum);
165                if (fraction < 0.0) {
166                    fraction = 0.0;
167                }
168                binIndex = (int) (fraction * bins);
169                // rounding could result in binIndex being equal to bins
170                // which will cause an IndexOutOfBoundsException - see bug
171                // report 1553088
172                if (binIndex >= bins) {
173                    binIndex = bins - 1;
174                }
175            }
176            HistogramBin bin = (HistogramBin) binList.get(binIndex);
177            bin.incrementCount();
178        }
179        // generic map for each series
180        Map<String, Object> map = new HashMap<>();
181        map.put("key", key);
182        map.put("bins", binList);
183        map.put("values.length", values.length);
184        map.put("bin width", binWidth);
185        this.list.add(map);
186        fireDatasetChanged();
187    }
188
189    /**
190     * Returns the minimum value in an array of values.
191     *
192     * @param values  the values ({@code null} not permitted and
193     *                zero-length array not permitted).
194     *
195     * @return The minimum value.
196     */
197    private double getMinimum(double[] values) {
198        if (values == null || values.length < 1) {
199            throw new IllegalArgumentException(
200                    "Null or zero length 'values' argument.");
201        }
202        double min = Double.MAX_VALUE;
203        for (int i = 0; i < values.length; i++) {
204            if (values[i] < min) {
205                min = values[i];
206            }
207        }
208        return min;
209    }
210
211    /**
212     * Returns the maximum value in an array of values.
213     *
214     * @param values  the values ({@code null} not permitted and
215     *                zero-length array not permitted).
216     *
217     * @return The maximum value.
218     */
219    private double getMaximum(double[] values) {
220        if (values == null || values.length < 1) {
221            throw new IllegalArgumentException(
222                    "Null or zero length 'values' argument.");
223        }
224        double max = -Double.MAX_VALUE;
225        for (int i = 0; i < values.length; i++) {
226            if (values[i] > max) {
227                max = values[i];
228            }
229        }
230        return max;
231    }
232
233    /**
234     * Returns the bins for a series.
235     *
236     * @param series  the series index (in the range {@code 0} to
237     *     {@code getSeriesCount() - 1}).
238     *
239     * @return A list of bins.
240     *
241     * @throws IndexOutOfBoundsException if {@code series} is outside the
242     *     specified range.
243     */
244    List<HistogramBin> getBins(int series) {
245        Map<String, Object> map = this.list.get(series);
246        return (List<HistogramBin>) map.get("bins");
247    }
248
249    /**
250     * Returns the total number of observations for a series.
251     *
252     * @param series  the series index.
253     *
254     * @return The total.
255     */
256    private int getTotal(int series) {
257        Map<String, Object> map = this.list.get(series);
258        return (Integer) map.get("values.length");
259    }
260
261    /**
262     * Returns the bin width for a series.
263     *
264     * @param series  the series index (zero based).
265     *
266     * @return The bin width.
267     */
268    private double getBinWidth(int series) {
269        Map<String, Object> map = this.list.get(series);
270        return (Double) map.get("bin width");
271    }
272
273    /**
274     * Returns the number of series in the dataset.
275     *
276     * @return The series count.
277     */
278    @Override
279    public int getSeriesCount() {
280        return this.list.size();
281    }
282
283    /**
284     * Returns the key for a series.
285     *
286     * @param series  the series index (in the range {@code 0} to
287     *     {@code getSeriesCount() - 1}).
288     *
289     * @return The series key.
290     *
291     * @throws IndexOutOfBoundsException if {@code series} is outside the
292     *     specified range.
293     */
294    @Override
295    public Comparable getSeriesKey(int series) {
296        Map<String, Object> map = this.list.get(series);
297        return (Comparable) map.get("key");
298    }
299
300    /**
301     * Returns the number of data items for a series.
302     *
303     * @param series  the series index (in the range {@code 0} to
304     *     {@code getSeriesCount() - 1}).
305     *
306     * @return The item count.
307     *
308     * @throws IndexOutOfBoundsException if {@code series} is outside the
309     *     specified range.
310     */
311    @Override
312    public int getItemCount(int series) {
313        return getBins(series).size();
314    }
315
316    /**
317     * Returns the X value for a bin.  This value won't be used for plotting
318     * histograms, since the renderer will ignore it.  But other renderers can
319     * use it (for example, you could use the dataset to create a line
320     * chart).
321     *
322     * @param series  the series index (in the range {@code 0} to
323     *     {@code getSeriesCount() - 1}).
324     * @param item  the item index (zero based).
325     *
326     * @return The start value.
327     *
328     * @throws IndexOutOfBoundsException if {@code series} is outside the
329     *     specified range.
330     */
331    @Override
332    public Number getX(int series, int item) {
333        List<HistogramBin> bins = getBins(series);
334        HistogramBin bin = bins.get(item);
335        return (bin.getStartBoundary() + bin.getEndBoundary()) / 2.0;
336    }
337
338    /**
339     * Returns the y-value for a bin (calculated to take into account the
340     * histogram type).
341     *
342     * @param series  the series index (in the range {@code 0} to
343     *     {@code getSeriesCount() - 1}).
344     * @param item  the item index (zero based).
345     *
346     * @return The y-value.
347     *
348     * @throws IndexOutOfBoundsException if {@code series} is outside the
349     *     specified range.
350     */
351    @Override
352    public Number getY(int series, int item) {
353        List<HistogramBin> bins = getBins(series);
354        HistogramBin bin = bins.get(item);
355        double total = getTotal(series);
356        double binWidth = getBinWidth(series);
357
358        if (this.type == HistogramType.FREQUENCY) {
359            return bin.getCount();
360        }
361        else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
362            return bin.getCount() / total;
363        }
364        else if (this.type == HistogramType.SCALE_AREA_TO_1) {
365            return bin.getCount() / (binWidth * total);
366        }
367        else { // pretty sure this shouldn't ever happen
368            throw new IllegalStateException();
369        }
370    }
371
372    /**
373     * Returns the start value for a bin.
374     *
375     * @param series  the series index (in the range {@code 0} to
376     *     {@code getSeriesCount() - 1}).
377     * @param item  the item index (zero based).
378     *
379     * @return The start value.
380     *
381     * @throws IndexOutOfBoundsException if {@code series} is outside the
382     *     specified range.
383     */
384    @Override
385    public Number getStartX(int series, int item) {
386        List<HistogramBin> bins = getBins(series);
387        HistogramBin bin = bins.get(item);
388        return bin.getStartBoundary();
389    }
390
391    /**
392     * Returns the end value for a bin.
393     *
394     * @param series  the series index (in the range {@code 0} to
395     *     {@code getSeriesCount() - 1}).
396     * @param item  the item index (zero based).
397     *
398     * @return The end value.
399     *
400     * @throws IndexOutOfBoundsException if {@code series} is outside the
401     *     specified range.
402     */
403    @Override
404    public Number getEndX(int series, int item) {
405        List<HistogramBin> bins = getBins(series);
406        HistogramBin bin = bins.get(item);
407        return bin.getEndBoundary();
408    }
409
410    /**
411     * Returns the start y-value for a bin (which is the same as the y-value,
412     * this method exists only to support the general form of the
413     * {@link IntervalXYDataset} interface).
414     *
415     * @param series  the series index (in the range {@code 0} to
416     *     {@code getSeriesCount() - 1}).
417     * @param item  the item index (zero based).
418     *
419     * @return The y-value.
420     *
421     * @throws IndexOutOfBoundsException if {@code series} is outside the
422     *     specified range.
423     */
424    @Override
425    public Number getStartY(int series, int item) {
426        return getY(series, item);
427    }
428
429    /**
430     * Returns the end y-value for a bin (which is the same as the y-value,
431     * this method exists only to support the general form of the
432     * {@link IntervalXYDataset} interface).
433     *
434     * @param series  the series index (in the range {@code 0} to
435     *     {@code getSeriesCount() - 1}).
436     * @param item  the item index (zero based).
437     *
438     * @return The Y value.
439     *
440     * @throws IndexOutOfBoundsException if {@code series} is outside the
441     *     specified range.
442     */
443    @Override
444    public Number getEndY(int series, int item) {
445        return getY(series, item);
446    }
447
448    /**
449     * Tests this dataset for equality with an arbitrary object.
450     *
451     * @param obj  the object to test against ({@code null} permitted).
452     *
453     * @return A boolean.
454     */
455    @Override
456    public boolean equals(Object obj) {
457        if (obj == this) {
458            return true;
459        }
460        if (!(obj instanceof HistogramDataset)) {
461            return false;
462        }
463        HistogramDataset that = (HistogramDataset) obj;
464        if (!Objects.equals(this.type, that.type)) {
465            return false;
466        }
467        if (!Objects.equals(this.list, that.list)) {
468            return false;
469        }
470        return true;
471    }
472
473    @Override
474    public int hashCode(){
475        int hash = 3;
476        hash = 83 * hash + Objects.hashCode(this.list);
477        hash = 83 * hash + Objects.hashCode(this.type);
478        return hash;
479    }
480
481    /**
482     * Returns a clone of the dataset.
483     *
484     * @return A clone of the dataset.
485     *
486     * @throws CloneNotSupportedException if the object cannot be cloned.
487     */
488    @Override
489    public Object clone() throws CloneNotSupportedException {
490        HistogramDataset clone = (HistogramDataset) super.clone();
491        int seriesCount = getSeriesCount();
492        clone.list = new ArrayList<>(seriesCount);
493        for (int i = 0; i < seriesCount; i++) {
494            clone.list.add(new HashMap(this.list.get(i)));
495        }
496        return clone;
497    }
498
499}