    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    }