    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    }