    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    }