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    
059    package org.jfree.data.statistics;
060    
061    import java.io.Serializable;
062    import java.util.ArrayList;
063    import java.util.HashMap;
064    import java.util.List;
065    import java.util.Map;
066    
067    import org.jfree.data.general.DatasetChangeEvent;
068    import org.jfree.data.xy.AbstractIntervalXYDataset;
069    import org.jfree.data.xy.IntervalXYDataset;
070    import org.jfree.util.ObjectUtilities;
071    import org.jfree.util.PublicCloneable;
072    
073    /**
074     * A dataset that can be used for creating histograms.
075     *
076     * @see SimpleHistogramDataset
077     */
078    public 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    }