001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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-2007, 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.0 ---------------------------------------------
053     * 03-Aug-2006 : Improved precision of bin boundary calculation (DG);
054     * 07-Sep-2006 : Fixed bug 1553088 (DG);
055     * 
056     */
057    
058    package org.jfree.data.statistics;
059    
060    import java.io.Serializable;
061    import java.util.ArrayList;
062    import java.util.HashMap;
063    import java.util.List;
064    import java.util.Map;
065    
066    import org.jfree.data.general.DatasetChangeEvent;
067    import org.jfree.data.xy.AbstractIntervalXYDataset;
068    import org.jfree.data.xy.IntervalXYDataset;
069    import org.jfree.util.ObjectUtilities;
070    import org.jfree.util.PublicCloneable;
071    
072    /**
073     * A dataset that can be used for creating histograms.
074     * 
075     * @see SimpleHistogramDataset
076     */
077    public class HistogramDataset extends AbstractIntervalXYDataset 
078                                  implements IntervalXYDataset, 
079                                             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            return super.clone();   
497        }
498    
499    }