    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
    062package org.jfree.data.statistics;
    063
    064import java.io.Serializable;
    065import java.util.ArrayList;
    066import java.util.HashMap;
    067import java.util.List;
    068import java.util.Map;
    069
    070import org.jfree.data.general.DatasetChangeEvent;
    071import org.jfree.data.xy.AbstractIntervalXYDataset;
    072import org.jfree.data.xy.IntervalXYDataset;
    073import org.jfree.util.ObjectUtilities;
    074import org.jfree.util.PublicCloneable;
    075
    076/**
    077 * A dataset that can be used for creating histograms.
    078 *
    079 * @see SimpleHistogramDataset
    080 */
    081public 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}