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