001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2011, 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-2009, 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 *
060 */
061
062package org.jfree.data.statistics;
063
064import java.io.Serializable;
065import java.util.ArrayList;
066import java.util.HashMap;
067import java.util.List;
068import java.util.Map;
069
070import org.jfree.data.general.DatasetChangeEvent;
071import org.jfree.data.xy.AbstractIntervalXYDataset;
072import org.jfree.data.xy.IntervalXYDataset;
073import org.jfree.util.ObjectUtilities;
074import org.jfree.util.PublicCloneable;
075
076/**
077 * A dataset that can be used for creating histograms.
078 *
079 * @see SimpleHistogramDataset
080 */
081public class HistogramDataset extends AbstractIntervalXYDataset
082        implements IntervalXYDataset, Cloneable, PublicCloneable,
083                   Serializable {
084
085    /** For serialization. */
086    private static final long serialVersionUID = -6341668077370231153L;
087
088    /** A list of maps. */
089    private List list;
090
091    /** The histogram type. */
092    private HistogramType type;
093
094    /**
095     * Creates a new (empty) dataset with a default type of
096     * {@link HistogramType}.FREQUENCY.
097     */
098    public HistogramDataset() {
099        this.list = new ArrayList();
100        this.type = HistogramType.FREQUENCY;
101    }
102
103    /**
104     * Returns the histogram type.
105     *
106     * @return The type (never <code>null</code>).
107     */
108    public HistogramType getType() {
109        return this.type;
110    }
111
112    /**
113     * Sets the histogram type and sends a {@link DatasetChangeEvent} to all
114     * registered listeners.
115     *
116     * @param type  the type (<code>null</code> not permitted).
117     */
118    public void setType(HistogramType type) {
119        if (type == null) {
120            throw new IllegalArgumentException("Null 'type' argument");
121        }
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        if (key == null) {
157            throw new IllegalArgumentException("Null 'key' argument.");
158        }
159        if (values == null) {
160            throw new IllegalArgumentException("Null 'values' argument.");
161        }
162        else if (bins < 1) {
163            throw new IllegalArgumentException(
164                    "The 'bins' value must be at least 1.");
165        }
166        double binWidth = (maximum - minimum) / bins;
167
168        double lower = minimum;
169        double upper;
170        List binList = new ArrayList(bins);
171        for (int i = 0; i < bins; i++) {
172            HistogramBin bin;
173            // make sure bins[bins.length]'s upper boundary ends at maximum
174            // to avoid the rounding issue. the bins[0] lower boundary is
175            // guaranteed start from min
176            if (i == bins - 1) {
177                bin = new HistogramBin(lower, maximum);
178            }
179            else {
180                upper = minimum + (i + 1) * binWidth;
181                bin = new HistogramBin(lower, upper);
182                lower = upper;
183            }
184            binList.add(bin);
185        }
186        // fill the bins
187        for (int i = 0; i < values.length; i++) {
188            int binIndex = bins - 1;
189            if (values[i] < maximum) {
190                double fraction = (values[i] - minimum) / (maximum - minimum);
191                if (fraction < 0.0) {
192                    fraction = 0.0;
193                }
194                binIndex = (int) (fraction * bins);
195                // rounding could result in binIndex being equal to bins
196                // which will cause an IndexOutOfBoundsException - see bug
197                // report 1553088
198                if (binIndex >= bins) {
199                    binIndex = bins - 1;
200                }
201            }
202            HistogramBin bin = (HistogramBin) binList.get(binIndex);
203            bin.incrementCount();
204        }
205        // generic map for each series
206        Map map = new HashMap();
207        map.put("key", key);
208        map.put("bins", binList);
209        map.put("values.length", new Integer(values.length));
210        map.put("bin width", new Double(binWidth));
211        this.list.add(map);
212        fireDatasetChanged();
213    }
214
215    /**
216     * Returns the minimum value in an array of values.
217     *
218     * @param values  the values (<code>null</code> not permitted and
219     *                zero-length array not permitted).
220     *
221     * @return The minimum value.
222     */
223    private double getMinimum(double[] values) {
224        if (values == null || values.length < 1) {
225            throw new IllegalArgumentException(
226                    "Null or zero length 'values' argument.");
227        }
228        double min = Double.MAX_VALUE;
229        for (int i = 0; i < values.length; i++) {
230            if (values[i] < min) {
231                min = values[i];
232            }
233        }
234        return min;
235    }
236
237    /**
238     * Returns the maximum value in an array of values.
239     *
240     * @param values  the values (<code>null</code> not permitted and
241     *                zero-length array not permitted).
242     *
243     * @return The maximum value.
244     */
245    private double getMaximum(double[] values) {
246        if (values == null || values.length < 1) {
247            throw new IllegalArgumentException(
248                    "Null or zero length 'values' argument.");
249        }
250        double max = -Double.MAX_VALUE;
251        for (int i = 0; i < values.length; i++) {
252            if (values[i] > max) {
253                max = values[i];
254            }
255        }
256        return max;
257    }
258
259    /**
260     * Returns the bins for a series.
261     *
262     * @param series  the series index (in the range <code>0</code> to
263     *     <code>getSeriesCount() - 1</code>).
264     *
265     * @return A list of bins.
266     *
267     * @throws IndexOutOfBoundsException if <code>series</code> is outside the
268     *     specified range.
269     */
270    List getBins(int series) {
271        Map map = (Map) this.list.get(series);
272        return (List) map.get("bins");
273    }
274
275    /**
276     * Returns the total number of observations for a series.
277     *
278     * @param series  the series index.
279     *
280     * @return The total.
281     */
282    private int getTotal(int series) {
283        Map map = (Map) this.list.get(series);
284        return ((Integer) map.get("values.length")).intValue();
285    }
286
287    /**
288     * Returns the bin width for a series.
289     *
290     * @param series  the series index (zero based).
291     *
292     * @return The bin width.
293     */
294    private double getBinWidth(int series) {
295        Map map = (Map) this.list.get(series);
296        return ((Double) map.get("bin width")).doubleValue();
297    }
298
299    /**
300     * Returns the number of series in the dataset.
301     *
302     * @return The series count.
303     */
304    public int getSeriesCount() {
305        return this.list.size();
306    }
307
308    /**
309     * Returns the key for a series.
310     *
311     * @param series  the series index (in the range <code>0</code> to
312     *     <code>getSeriesCount() - 1</code>).
313     *
314     * @return The series key.
315     *
316     * @throws IndexOutOfBoundsException if <code>series</code> is outside the
317     *     specified range.
318     */
319    public Comparable getSeriesKey(int series) {
320        Map map = (Map) this.list.get(series);
321        return (Comparable) map.get("key");
322    }
323
324    /**
325     * Returns the number of data items for a series.
326     *
327     * @param series  the series index (in the range <code>0</code> to
328     *     <code>getSeriesCount() - 1</code>).
329     *
330     * @return The item count.
331     *
332     * @throws IndexOutOfBoundsException if <code>series</code> is outside the
333     *     specified range.
334     */
335    public int getItemCount(int series) {
336        return getBins(series).size();
337    }
338
339    /**
340     * Returns the X value for a bin.  This value won't be used for plotting
341     * histograms, since the renderer will ignore it.  But other renderers can
342     * use it (for example, you could use the dataset to create a line
343     * chart).
344     *
345     * @param series  the series index (in the range <code>0</code> to
346     *     <code>getSeriesCount() - 1</code>).
347     * @param item  the item index (zero based).
348     *
349     * @return The start value.
350     *
351     * @throws IndexOutOfBoundsException if <code>series</code> is outside the
352     *     specified range.
353     */
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    public Number getY(int series, int item) {
375        List bins = getBins(series);
376        HistogramBin bin = (HistogramBin) bins.get(item);
377        double total = getTotal(series);
378        double binWidth = getBinWidth(series);
379
380        if (this.type == HistogramType.FREQUENCY) {
381            return new Double(bin.getCount());
382        }
383        else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
384            return new Double(bin.getCount() / total);
385        }
386        else if (this.type == HistogramType.SCALE_AREA_TO_1) {
387            return new Double(bin.getCount() / (binWidth * total));
388        }
389        else { // pretty sure this shouldn't ever happen
390            throw new IllegalStateException();
391        }
392    }
393
394    /**
395     * Returns the start value for a bin.
396     *
397     * @param series  the series index (in the range <code>0</code> to
398     *     <code>getSeriesCount() - 1</code>).
399     * @param item  the item index (zero based).
400     *
401     * @return The start value.
402     *
403     * @throws IndexOutOfBoundsException if <code>series</code> is outside the
404     *     specified range.
405     */
406    public Number getStartX(int series, int item) {
407        List bins = getBins(series);
408        HistogramBin bin = (HistogramBin) bins.get(item);
409        return new Double(bin.getStartBoundary());
410    }
411
412    /**
413     * Returns the end value for a bin.
414     *
415     * @param series  the series index (in the range <code>0</code> to
416     *     <code>getSeriesCount() - 1</code>).
417     * @param item  the item index (zero based).
418     *
419     * @return The end value.
420     *
421     * @throws IndexOutOfBoundsException if <code>series</code> is outside the
422     *     specified range.
423     */
424    public Number getEndX(int series, int item) {
425        List bins = getBins(series);
426        HistogramBin bin = (HistogramBin) bins.get(item);
427        return new Double(bin.getEndBoundary());
428    }
429
430    /**
431     * Returns the start y-value for a bin (which is the same as the y-value,
432     * this method exists only to support the general form of the
433     * {@link IntervalXYDataset} interface).
434     *
435     * @param series  the series index (in the range <code>0</code> to
436     *     <code>getSeriesCount() - 1</code>).
437     * @param item  the item index (zero based).
438     *
439     * @return The y-value.
440     *
441     * @throws IndexOutOfBoundsException if <code>series</code> is outside the
442     *     specified range.
443     */
444    public Number getStartY(int series, int item) {
445        return getY(series, item);
446    }
447
448    /**
449     * Returns the end y-value for a bin (which is the same as the y-value,
450     * this method exists only to support the general form of the
451     * {@link IntervalXYDataset} interface).
452     *
453     * @param series  the series index (in the range <code>0</code> to
454     *     <code>getSeriesCount() - 1</code>).
455     * @param item  the item index (zero based).
456     *
457     * @return The Y value.
458     *
459     * @throws IndexOutOfBoundsException if <code>series</code> is outside the
460     *     specified range.
461     */
462    public Number getEndY(int series, int item) {
463        return getY(series, item);
464    }
465
466    /**
467     * Tests this dataset for equality with an arbitrary object.
468     *
469     * @param obj  the object to test against (<code>null</code> permitted).
470     *
471     * @return A boolean.
472     */
473    public boolean equals(Object obj) {
474        if (obj == this) {
475            return true;
476        }
477        if (!(obj instanceof HistogramDataset)) {
478            return false;
479        }
480        HistogramDataset that = (HistogramDataset) obj;
481        if (!ObjectUtilities.equal(this.type, that.type)) {
482            return false;
483        }
484        if (!ObjectUtilities.equal(this.list, that.list)) {
485            return false;
486        }
487        return true;
488    }
489
490    /**
491     * Returns a clone of the dataset.
492     *
493     * @return A clone of the dataset.
494     *
495     * @throws CloneNotSupportedException if the object cannot be cloned.
496     */
497    public Object clone() throws CloneNotSupportedException {
498        HistogramDataset clone = (HistogramDataset) super.clone();
499        int seriesCount = getSeriesCount();
500        clone.list = new java.util.ArrayList(seriesCount);
501        for (int i = 0; i < seriesCount; i++) {
502            clone.list.add(new HashMap((Map) this.list.get(i)));
503        }
504        return clone;
505    }
506
507}