001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2006, 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-2006, 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     * $Id: HistogramDataset.java,v 1.9.2.7 2006/09/07 15:26:49 mungady Exp $
038     *
039     * Changes
040     * -------
041     * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG);
042     * 07-Jul-2003 : Changed package and added Javadocs (DG);
043     * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW);
044     * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG);
045     * 01-Mar-2004 : Added equals() and clone() methods and implemented 
046     *               Serializable.  Also added new addSeries() method (DG);
047     * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG);
048     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
049     *               getYValue() (DG);
050     * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron
051     *               Hayne (DG);
052     * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG);
053     * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG);
054     * ------------- JFREECHART 1.0.0 ---------------------------------------------
055     * 03-Aug-2006 : Improved precision of bin boundary calculation (DG);
056     * 07-Sep-2006 : Fixed bug 1553088 (DG);
057     * 
058     */
059    
060    package org.jfree.data.statistics;
061    
062    import java.io.Serializable;
063    import java.util.ArrayList;
064    import java.util.HashMap;
065    import java.util.List;
066    import java.util.Map;
067    
068    import org.jfree.data.general.DatasetChangeEvent;
069    import org.jfree.data.xy.AbstractIntervalXYDataset;
070    import org.jfree.data.xy.IntervalXYDataset;
071    import org.jfree.util.ObjectUtilities;
072    import org.jfree.util.PublicCloneable;
073    
074    /**
075     * A dataset that can be used for creating histograms.
076     * 
077     * @see SimpleHistogramDataset
078     */
079    public class HistogramDataset extends AbstractIntervalXYDataset 
080                                  implements IntervalXYDataset, 
081                                             Cloneable, PublicCloneable, 
082                                             Serializable {
083    
084        /** For serialization. */
085        private static final long serialVersionUID = -6341668077370231153L;
086        
087        /** A list of maps. */
088        private List list;
089        
090        /** The histogram type. */
091        private HistogramType type;
092    
093        /**
094         * Creates a new (empty) dataset with a default type of 
095         * {@link HistogramType}.FREQUENCY.
096         */
097        public HistogramDataset() {
098            this.list = new ArrayList();
099            this.type = HistogramType.FREQUENCY;
100        }
101        
102        /**
103         * Returns the histogram type. 
104         * 
105         * @return The type (never <code>null</code>).
106         */
107        public HistogramType getType() { 
108            return this.type; 
109        }
110    
111        /**
112         * Sets the histogram type and sends a {@link DatasetChangeEvent} to all 
113         * registered listeners.
114         * 
115         * @param type  the type (<code>null</code> not permitted).
116         */
117        public void setType(HistogramType type) {
118            if (type == null) {
119                throw new IllegalArgumentException("Null 'type' argument");
120            }
121            this.type = type;   
122            notifyListeners(new DatasetChangeEvent(this, this));
123        }
124    
125        /**
126         * Adds a series to the dataset, using the specified number of bins.
127         * 
128         * @param key  the series key (<code>null</code> not permitted).
129         * @param values the values (<code>null</code> not permitted).
130         * @param bins  the number of bins (must be at least 1).
131         */
132        public void addSeries(Comparable key, double[] values, int bins) {
133            // defer argument checking...
134            double minimum = getMinimum(values);
135            double maximum = getMaximum(values);
136            addSeries(key, values, bins, minimum, maximum);
137        }
138    
139        /**
140         * Adds a series to the dataset. Any data value less than minimum will be
141         * assigned to the first bin, and any data value greater than maximum will
142         * be assigned to the last bin.  Values falling on the boundary of 
143         * adjacent bins will be assigned to the higher indexed bin.
144         * 
145         * @param key  the series key (<code>null</code> not permitted).
146         * @param values  the raw observations.
147         * @param bins  the number of bins (must be at least 1).
148         * @param minimum  the lower bound of the bin range.
149         * @param maximum  the upper bound of the bin range.
150         */
151        public void addSeries(Comparable key, 
152                              double[] values, 
153                              int bins, 
154                              double minimum, 
155                              double maximum) {
156            
157            if (key == null) {
158                throw new IllegalArgumentException("Null 'key' argument.");   
159            }
160            if (values == null) {
161                throw new IllegalArgumentException("Null 'values' argument.");
162            }
163            else if (bins < 1) {
164                throw new IllegalArgumentException(
165                        "The 'bins' value must be at least 1.");
166            }
167            double binWidth = (maximum - minimum) / bins;
168    
169            double lower = minimum;
170            double upper;
171            List binList = new ArrayList(bins);
172            for (int i = 0; i < bins; i++) {
173                HistogramBin bin;
174                // make sure bins[bins.length]'s upper boundary ends at maximum
175                // to avoid the rounding issue. the bins[0] lower boundary is
176                // guaranteed start from min
177                if (i == bins - 1) {
178                    bin = new HistogramBin(lower, maximum);
179                }
180                else {
181                    upper = minimum + (i + 1) * binWidth;
182                    bin = new HistogramBin(lower, upper);
183                    lower = upper;
184                }
185                binList.add(bin);
186            }        
187            // fill the bins
188            for (int i = 0; i < values.length; i++) {
189                int binIndex = bins - 1;
190                if (values[i] < maximum) {
191                    double fraction = (values[i] - minimum) / (maximum - minimum);
192                    if (fraction < 0.0) {
193                        fraction = 0.0;
194                    }
195                    binIndex = (int) (fraction * bins);
196                    // rounding could result in binIndex being equal to bins
197                    // which will cause an IndexOutOfBoundsException - see bug
198                    // report 1553088
199                    if (binIndex >= bins) {
200                        binIndex = bins - 1;
201                    }
202                }
203                HistogramBin bin = (HistogramBin) binList.get(binIndex);
204                bin.incrementCount();
205            }
206            // generic map for each series
207            Map map = new HashMap();
208            map.put("key", key);
209            map.put("bins", binList);
210            map.put("values.length", new Integer(values.length));
211            map.put("bin width", new Double(binWidth));
212            this.list.add(map);
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            return super.clone();   
499        }
500    
501    }