001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, 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-2005, 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.4 2005/11/22 11:06:47 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     * 
055     */
056    
057    package org.jfree.data.statistics;
058    
059    import java.io.Serializable;
060    import java.util.ArrayList;
061    import java.util.HashMap;
062    import java.util.List;
063    import java.util.Map;
064    
065    import org.jfree.data.general.DatasetChangeEvent;
066    import org.jfree.data.xy.AbstractIntervalXYDataset;
067    import org.jfree.data.xy.IntervalXYDataset;
068    import org.jfree.util.ObjectUtilities;
069    import org.jfree.util.PublicCloneable;
070    
071    /**
072     * A dataset that can be used for creating histograms.  
073     * 
074     * @see SimpleHistogramDataset
075     */
076    public class HistogramDataset extends AbstractIntervalXYDataset 
077                                  implements IntervalXYDataset, 
078                                             Cloneable, PublicCloneable, 
079                                             Serializable {
080    
081        /** For serialization. */
082        private static final long serialVersionUID = -6341668077370231153L;
083        
084        /** A list of maps. */
085        private List list;
086        
087        /** The histogram type. */
088        private HistogramType type;
089    
090        /**
091         * Creates a new (empty) dataset with a default type of 
092         * {@link HistogramType}.FREQUENCY.
093         */
094        public HistogramDataset() {
095            this.list = new ArrayList();
096            this.type = HistogramType.FREQUENCY;
097        }
098        
099        /**
100         * Returns the histogram type. 
101         * 
102         * @return The type (never <code>null</code>).
103         */
104        public HistogramType getType() { 
105            return this.type; 
106        }
107    
108        /**
109         * Sets the histogram type and sends a {@link DatasetChangeEvent} to all 
110         * registered listeners.
111         * 
112         * @param type  the type (<code>null</code> not permitted).
113         */
114        public void setType(HistogramType type) {
115            if (type == null) {
116                throw new IllegalArgumentException("Null 'type' argument");
117            }
118            this.type = type;   
119            notifyListeners(new DatasetChangeEvent(this, this));
120        }
121    
122        /**
123         * Adds a series to the dataset, using the specified number of bins.
124         * 
125         * @param key  the series key (<code>null</code> not permitted).
126         * @param values the values (<code>null</code> not permitted).
127         * @param bins  the number of bins (must be at least 1).
128         */
129        public void addSeries(Comparable key, double[] values, int bins) {
130            // defer argument checking...
131            double minimum = getMinimum(values);
132            double maximum = getMaximum(values);
133            addSeries(key, values, bins, minimum, maximum);
134        }
135    
136        /**
137         * Adds a series to the dataset. Any data value falling on a bin boundary 
138         * will be assigned to the lower value bin, with the exception of the lower 
139         * bound of the bin range which is always assigned to the first bin.
140         * 
141         * @param key  the series key (<code>null</code> not permitted).
142         * @param values  the raw observations.
143         * @param bins  the number of bins.
144         * @param minimum  the lower bound of the bin range.
145         * @param maximum  the upper bound of the bin range.
146         */
147        public void addSeries(Comparable key, 
148                              double[] values, 
149                              int bins, 
150                              double minimum, 
151                              double maximum) {
152            
153            if (key == null) {
154                throw new IllegalArgumentException("Null 'key' argument.");   
155            }
156            if (values == null) {
157                throw new IllegalArgumentException("Null 'values' argument.");
158            }
159            else if (bins < 1) {
160                throw new IllegalArgumentException(
161                    "The 'bins' value must be at least 1."
162                );
163            }
164            double binWidth = (maximum - minimum) / bins;
165    
166            double tmp = minimum;
167            List binList = new ArrayList(bins);
168            for (int i = 0; i < bins; i++) {
169                HistogramBin bin;
170                // make sure bins[bins.length]'s upper boundary ends at maximum
171                // to avoid the rounding issue. the bins[0] lower boundary is
172                // guaranteed start from min
173                if (i == bins - 1) {
174                    bin = new HistogramBin(tmp, maximum);
175                }
176                else {
177                    bin = new HistogramBin(tmp, tmp + binWidth);
178                }
179                tmp = tmp + binWidth;
180                binList.add(bin);
181            }        
182            // fill the bins
183            for (int i = 0; i < values.length; i++) {
184                int binIndex = bins - 1;
185                if (values[i] < maximum) {
186                    double fraction = (values[i] - minimum) / (maximum - minimum);
187                    if (fraction < 0.0) {
188                        fraction = 0.0;
189                    }
190                    binIndex = (int) (fraction * bins);
191                }
192                HistogramBin bin = (HistogramBin) binList.get(binIndex);
193                bin.incrementCount();
194            }
195            // generic map for each series
196            Map map = new HashMap();
197            map.put("key", key);
198            map.put("bins", binList);
199            map.put("values.length", new Integer(values.length));
200            map.put("bin width", new Double(binWidth));
201            this.list.add(map);
202        }
203        
204        /**
205         * Returns the minimum value in an array of values.
206         * 
207         * @param values  the values (<code>null</code> not permitted and 
208         *                zero-length array not permitted).
209         * 
210         * @return The minimum value.
211         */
212        private double getMinimum(double[] values) {
213            if (values == null || values.length < 1) {
214                throw new IllegalArgumentException(
215                    "Null or zero length 'values' argument."
216                );
217            }
218            double min = Double.MAX_VALUE;
219            for (int i = 0; i < values.length; i++) {
220                if (values[i] < min) {
221                    min = values[i];
222                }
223            }
224            return min;
225        }
226    
227        /**
228         * Returns the maximum value in an array of values.
229         * 
230         * @param values  the values (<code>null</code> not permitted and 
231         *                zero-length array not permitted).
232         * 
233         * @return The maximum value.
234         */
235        private double getMaximum(double[] values) {
236            if (values == null || values.length < 1) {
237                throw new IllegalArgumentException(
238                    "Null or zero length 'values' argument."
239                );
240            }
241            double max = -Double.MAX_VALUE;
242            for (int i = 0; i < values.length; i++) {
243                if (values[i] > max) {
244                    max = values[i];
245                }
246            }
247            return max;
248        }
249    
250        /**
251         * Returns the bins for a series.
252         * 
253         * @param series  the series index.
254         * 
255         * @return An array of bins.
256         */
257        List getBins(int series) {
258            Map map = (Map) this.list.get(series);
259            return (List) map.get("bins"); 
260        }
261    
262        /**
263         * Returns the total number of observations for a series.
264         * 
265         * @param series  the series index.
266         * 
267         * @return The total.
268         */
269        private int getTotal(int series) {
270            Map map = (Map) this.list.get(series);
271            return ((Integer) map.get("values.length")).intValue(); 
272        }
273    
274        /**
275         * Returns the bin width for a series.
276         * 
277         * @param series  the series index (zero based).
278         * 
279         * @return The bin width.
280         */
281        private double getBinWidth(int series) {
282            Map map = (Map) this.list.get(series);
283            return ((Double) map.get("bin width")).doubleValue(); 
284        }
285    
286        /**
287         * Returns the number of series in the dataset.
288         * 
289         * @return The series count.
290         */
291        public int getSeriesCount() { 
292            return this.list.size(); 
293        }
294        
295        /**
296         * Returns the key for a series.
297         * 
298         * @param series  the series index (zero based).
299         * 
300         * @return The series key.
301         */
302        public Comparable getSeriesKey(int series) {
303            Map map = (Map) this.list.get(series);
304            return (Comparable) map.get("key"); 
305        }
306    
307        /**
308         * Returns the number of data items for a series.
309         * 
310         * @param series  the series index (zero based).
311         * 
312         * @return The item count.
313         */
314        public int getItemCount(int series) {
315            return getBins(series).size(); 
316        }
317    
318        /**
319         * Returns the X value for a bin.  This value won't be used for plotting 
320         * histograms, since the renderer will ignore it.  But other renderers can 
321         * use it (for example, you could use the dataset to create a line
322         * chart).
323         * 
324         * @param series  the series index (zero based).
325         * @param item  the item index (zero based).
326         * 
327         * @return The start value.
328         */
329        public Number getX(int series, int item) {
330            List bins = getBins(series);
331            HistogramBin bin = (HistogramBin) bins.get(item);
332            double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
333            return new Double(x);
334        }
335    
336        /**
337         * Returns the y-value for a bin (calculated to take into account the 
338         * histogram type).
339         * 
340         * @param series  the series index (zero based).
341         * @param item  the item index (zero based).
342         * 
343         * @return The y-value.
344         */
345        public Number getY(int series, int item) {
346            List bins = getBins(series);
347            HistogramBin bin = (HistogramBin) bins.get(item);
348            double total = getTotal(series);
349            double binWidth = getBinWidth(series);
350    
351            if (this.type == HistogramType.FREQUENCY) {
352                return new Double(bin.getCount());
353            }
354            else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
355                return new Double(bin.getCount() / total);
356            }
357            else if (this.type == HistogramType.SCALE_AREA_TO_1) {
358                return new Double(bin.getCount() / (binWidth * total));
359            }
360            else { // pretty sure this shouldn't ever happen
361                throw new IllegalStateException();
362            }
363        }
364    
365        /**
366         * Returns the start value for a bin.
367         * 
368         * @param series  the series index (zero based).
369         * @param item  the item index (zero based).
370         * 
371         * @return The start value.
372         */
373        public Number getStartX(int series, int item) {
374            List bins = getBins(series);
375            HistogramBin bin = (HistogramBin) bins.get(item);
376            return new Double(bin.getStartBoundary());
377        }
378    
379        /**
380         * Returns the end value for a bin.
381         * 
382         * @param series  the series index (zero based).
383         * @param item  the item index (zero based).
384         * 
385         * @return The end value.
386         */
387        public Number getEndX(int series, int item) {
388            List bins = getBins(series);
389            HistogramBin bin = (HistogramBin) bins.get(item);
390            return new Double(bin.getEndBoundary());
391        }
392    
393        /**
394         * Returns the start y-value for a bin (which is the same as the y-value, 
395         * this method exists only to support the general form of the 
396         * {@link IntervalXYDataset} interface).
397         * 
398         * @param series  the series index (zero based).
399         * @param item  the item index (zero based).
400         * 
401         * @return The y-value.
402         */
403        public Number getStartY(int series, int item) {
404            return getY(series, item);
405        }
406    
407        /**
408         * Returns the end y-value for a bin (which is the same as the y-value, 
409         * this method exists only to support the general form of the 
410         * {@link IntervalXYDataset} interface).
411         * 
412         * @param series  the series index (zero based).
413         * @param item  the item index (zero based).
414         * 
415         * @return The Y value.
416         */    
417        public Number getEndY(int series, int item) {
418            return getY(series, item);
419        }
420    
421        /**
422         * Tests this dataset for equality with an arbitrary object.
423         * 
424         * @param obj  the object to test against (<code>null</code> permitted).
425         * 
426         * @return A boolean.
427         */
428        public boolean equals(Object obj) {
429            if (obj == this) {
430                return true;   
431            }
432            if (!(obj instanceof HistogramDataset)) {
433                return false;
434            }
435            HistogramDataset that = (HistogramDataset) obj;
436            if (!ObjectUtilities.equal(this.type, that.type)) {
437                return false;
438            }
439            if (!ObjectUtilities.equal(this.list, that.list)) {
440                return false;
441            }
442            return true;   
443        }
444    
445        /**
446         * Returns a clone of the dataset.
447         * 
448         * @return A clone of the dataset.
449         * 
450         * @throws CloneNotSupportedException if the object cannot be cloned.
451         */
452        public Object clone() throws CloneNotSupportedException {
453            return super.clone();   
454        }
455    
456    }