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