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