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 * 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 (for Object Refinery Limited);
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;
048import org.jfree.chart.util.Args;
049import org.jfree.chart.util.PublicCloneable;
050
051import org.jfree.data.general.DatasetChangeEvent;
052import org.jfree.data.xy.AbstractIntervalXYDataset;
053import org.jfree.data.xy.IntervalXYDataset;
054
055/**
056 * A dataset that can be used for creating histograms.
057 *
058 * @see SimpleHistogramDataset
059 */
060public class HistogramDataset extends AbstractIntervalXYDataset
061        implements IntervalXYDataset, Cloneable, PublicCloneable,
062                   Serializable {
063
064    /** For serialization. */
065    private static final long serialVersionUID = -6341668077370231153L;
066
067    /** A list of maps. */
068    private List list;
069
070    /** The histogram type. */
071    private HistogramType type;
072
073    /**
074     * Creates a new (empty) dataset with a default type of
075     * {@link HistogramType}.FREQUENCY.
076     */
077    public HistogramDataset() {
078        this.list = new ArrayList();
079        this.type = HistogramType.FREQUENCY;
080    }
081
082    /**
083     * Returns the histogram type.
084     *
085     * @return The type (never {@code null}).
086     */
087    public HistogramType getType() {
088        return this.type;
089    }
090
091    /**
092     * Sets the histogram type and sends a {@link DatasetChangeEvent} to all
093     * registered listeners.
094     *
095     * @param type  the type ({@code null} not permitted).
096     */
097    public void setType(HistogramType type) {
098        Args.nullNotPermitted(type, "type");
099        this.type = type;
100        fireDatasetChanged();
101    }
102
103    /**
104     * Adds a series to the dataset, using the specified number of bins,
105     * and sends a {@link DatasetChangeEvent} to all registered listeners.
106     *
107     * @param key  the series key ({@code null} not permitted).
108     * @param values the values ({@code null} not permitted).
109     * @param bins  the number of bins (must be at least 1).
110     */
111    public void addSeries(Comparable key, double[] values, int bins) {
112        // defer argument checking...
113        double minimum = getMinimum(values);
114        double maximum = getMaximum(values);
115        addSeries(key, values, bins, minimum, maximum);
116    }
117
118    /**
119     * Adds a series to the dataset. Any data value less than minimum will be
120     * assigned to the first bin, and any data value greater than maximum will
121     * be assigned to the last bin.  Values falling on the boundary of
122     * adjacent bins will be assigned to the higher indexed bin.
123     *
124     * @param key  the series key ({@code null} not permitted).
125     * @param values  the raw observations.
126     * @param bins  the number of bins (must be at least 1).
127     * @param minimum  the lower bound of the bin range.
128     * @param maximum  the upper bound of the bin range.
129     */
130    public void addSeries(Comparable key, double[] values, int bins,
131            double minimum, double maximum) {
132
133        Args.nullNotPermitted(key, "key");
134        Args.nullNotPermitted(values, "values");
135        if (bins < 1) {
136            throw new IllegalArgumentException(
137                    "The 'bins' value must be at least 1.");
138        }
139        double binWidth = (maximum - minimum) / bins;
140
141        double lower = minimum;
142        double upper;
143        List binList = new ArrayList(bins);
144        for (int i = 0; i < bins; i++) {
145            HistogramBin bin;
146            // make sure bins[bins.length]'s upper boundary ends at maximum
147            // to avoid the rounding issue. the bins[0] lower boundary is
148            // guaranteed start from min
149            if (i == bins - 1) {
150                bin = new HistogramBin(lower, maximum);
151            }
152            else {
153                upper = minimum + (i + 1) * binWidth;
154                bin = new HistogramBin(lower, upper);
155                lower = upper;
156            }
157            binList.add(bin);
158        }
159        // fill the bins
160        for (int i = 0; i < values.length; i++) {
161            int binIndex = bins - 1;
162            if (values[i] < maximum) {
163                double fraction = (values[i] - minimum) / (maximum - minimum);
164                if (fraction < 0.0) {
165                    fraction = 0.0;
166                }
167                binIndex = (int) (fraction * bins);
168                // rounding could result in binIndex being equal to bins
169                // which will cause an IndexOutOfBoundsException - see bug
170                // report 1553088
171                if (binIndex >= bins) {
172                    binIndex = bins - 1;
173                }
174            }
175            HistogramBin bin = (HistogramBin) binList.get(binIndex);
176            bin.incrementCount();
177        }
178        // generic map for each series
179        Map map = new HashMap();
180        map.put("key", key);
181        map.put("bins", binList);
182        map.put("values.length", values.length);
183        map.put("bin width", binWidth);
184        this.list.add(map);
185        fireDatasetChanged();
186    }
187
188    /**
189     * Returns the minimum value in an array of values.
190     *
191     * @param values  the values ({@code null} not permitted and
192     *                zero-length array not permitted).
193     *
194     * @return The minimum value.
195     */
196    private double getMinimum(double[] values) {
197        if (values == null || values.length < 1) {
198            throw new IllegalArgumentException(
199                    "Null or zero length 'values' argument.");
200        }
201        double min = Double.MAX_VALUE;
202        for (int i = 0; i < values.length; i++) {
203            if (values[i] < min) {
204                min = values[i];
205            }
206        }
207        return min;
208    }
209
210    /**
211     * Returns the maximum value in an array of values.
212     *
213     * @param values  the values ({@code null} not permitted and
214     *                zero-length array not permitted).
215     *
216     * @return The maximum value.
217     */
218    private double getMaximum(double[] values) {
219        if (values == null || values.length < 1) {
220            throw new IllegalArgumentException(
221                    "Null or zero length 'values' argument.");
222        }
223        double max = -Double.MAX_VALUE;
224        for (int i = 0; i < values.length; i++) {
225            if (values[i] > max) {
226                max = values[i];
227            }
228        }
229        return max;
230    }
231
232    /**
233     * Returns the bins for a series.
234     *
235     * @param series  the series index (in the range {@code 0} to
236     *     {@code getSeriesCount() - 1}).
237     *
238     * @return A list of bins.
239     *
240     * @throws IndexOutOfBoundsException if {@code series} is outside the
241     *     specified range.
242     */
243    List getBins(int series) {
244        Map map = (Map) this.list.get(series);
245        return (List) map.get("bins");
246    }
247
248    /**
249     * Returns the total number of observations for a series.
250     *
251     * @param series  the series index.
252     *
253     * @return The total.
254     */
255    private int getTotal(int series) {
256        Map map = (Map) this.list.get(series);
257        return ((Integer) map.get("values.length"));
258    }
259
260    /**
261     * Returns the bin width for a series.
262     *
263     * @param series  the series index (zero based).
264     *
265     * @return The bin width.
266     */
267    private double getBinWidth(int series) {
268        Map map = (Map) this.list.get(series);
269        return ((Double) map.get("bin width"));
270    }
271
272    /**
273     * Returns the number of series in the dataset.
274     *
275     * @return The series count.
276     */
277    @Override
278    public int getSeriesCount() {
279        return this.list.size();
280    }
281
282    /**
283     * Returns the key for a series.
284     *
285     * @param series  the series index (in the range {@code 0} to
286     *     {@code getSeriesCount() - 1}).
287     *
288     * @return The series key.
289     *
290     * @throws IndexOutOfBoundsException if {@code series} is outside the
291     *     specified range.
292     */
293    @Override
294    public Comparable getSeriesKey(int series) {
295        Map map = (Map) this.list.get(series);
296        return (Comparable) map.get("key");
297    }
298
299    /**
300     * Returns the number of data items for a series.
301     *
302     * @param series  the series index (in the range {@code 0} to
303     *     {@code getSeriesCount() - 1}).
304     *
305     * @return The item count.
306     *
307     * @throws IndexOutOfBoundsException if {@code series} is outside the
308     *     specified range.
309     */
310    @Override
311    public int getItemCount(int series) {
312        return getBins(series).size();
313    }
314
315    /**
316     * Returns the X value for a bin.  This value won't be used for plotting
317     * histograms, since the renderer will ignore it.  But other renderers can
318     * use it (for example, you could use the dataset to create a line
319     * chart).
320     *
321     * @param series  the series index (in the range {@code 0} to
322     *     {@code getSeriesCount() - 1}).
323     * @param item  the item index (zero based).
324     *
325     * @return The start value.
326     *
327     * @throws IndexOutOfBoundsException if {@code series} is outside the
328     *     specified range.
329     */
330    @Override
331    public Number getX(int series, int item) {
332        List bins = getBins(series);
333        HistogramBin bin = (HistogramBin) bins.get(item);
334        double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
335        return x;
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 bins = getBins(series);
354        HistogramBin bin = (HistogramBin) 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 bins = getBins(series);
387        HistogramBin bin = (HistogramBin) 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 bins = getBins(series);
406        HistogramBin bin = (HistogramBin) 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    /**
474     * Returns a clone of the dataset.
475     *
476     * @return A clone of the dataset.
477     *
478     * @throws CloneNotSupportedException if the object cannot be cloned.
479     */
480    @Override
481    public Object clone() throws CloneNotSupportedException {
482        HistogramDataset clone = (HistogramDataset) super.clone();
483        int seriesCount = getSeriesCount();
484        clone.list = new java.util.ArrayList(seriesCount);
485        for (int i = 0; i < seriesCount; i++) {
486            clone.list.add(new HashMap((Map) this.list.get(i)));
487        }
488        return clone;
489    }
490
491}