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