/* =========================================================== * JFreeChart : a free chart library for the Java(tm) platform * =========================================================== * * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jfreechart/index.html * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * --------------------- * HistogramDataset.java * --------------------- * (C) Copyright 2003-2009, by Jelai Wang and Contributors. * * Original Author: Jelai Wang (jelaiw AT mindspring.com); * Contributor(s): David Gilbert (for Object Refinery Limited); * Cameron Hayne; * Rikard Bj?rklind; * Thomas A Caswell (patch 2902842); * * Changes * ------- * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG); * 07-Jul-2003 : Changed package and added Javadocs (DG); * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW); * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG); * 01-Mar-2004 : Added equals() and clone() methods and implemented * Serializable. Also added new addSeries() method (DG); * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG); * 15-Jul-2004 : Switched getX() with getXValue() and getY() with * getYValue() (DG); * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron * Hayne (DG); * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG); * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG); * ------------- JFREECHART 1.0.x --------------------------------------------- * 03-Aug-2006 : Improved precision of bin boundary calculation (DG); * 07-Sep-2006 : Fixed bug 1553088 (DG); * 22-May-2008 : Implemented clone() method override (DG); * 08-Dec-2009 : Fire change event in addSeries() - see patch 2902842 * contributed by Thomas A Caswell (DG); * */ package org.jfree.data.statistics; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jfree.data.general.DatasetChangeEvent; import org.jfree.data.xy.AbstractIntervalXYDataset; import org.jfree.data.xy.IntervalXYDataset; import org.jfree.util.ObjectUtilities; import org.jfree.util.PublicCloneable; /** * A dataset that can be used for creating histograms. * * @see SimpleHistogramDataset */ public class HistogramDataset extends AbstractIntervalXYDataset implements IntervalXYDataset, Cloneable, PublicCloneable, Serializable { /** For serialization. */ private static final long serialVersionUID = -6341668077370231153L; /** A list of maps. */ private List list; /** The histogram type. */ private HistogramType type; /** * Creates a new (empty) dataset with a default type of * {@link HistogramType}.FREQUENCY. */ public HistogramDataset() { this.list = new ArrayList(); this.type = HistogramType.FREQUENCY; } /** * Returns the histogram type. * * @return The type (never <code>null</code>). */ public HistogramType getType() { return this.type; } /** * Sets the histogram type and sends a {@link DatasetChangeEvent} to all * registered listeners. * * @param type the type (<code>null</code> not permitted). */ public void setType(HistogramType type) { if (type == null) { throw new IllegalArgumentException("Null 'type' argument"); } this.type = type; fireDatasetChanged(); } /** * Adds a series to the dataset, using the specified number of bins, * and sends a {@link DatasetChangeEvent} to all registered listeners. * * @param key the series key (<code>null</code> not permitted). * @param values the values (<code>null</code> not permitted). * @param bins the number of bins (must be at least 1). */ public void addSeries(Comparable key, double[] values, int bins) { // defer argument checking... double minimum = getMinimum(values); double maximum = getMaximum(values); addSeries(key, values, bins, minimum, maximum); } /** * Adds a series to the dataset. Any data value less than minimum will be * assigned to the first bin, and any data value greater than maximum will * be assigned to the last bin. Values falling on the boundary of * adjacent bins will be assigned to the higher indexed bin. * * @param key the series key (<code>null</code> not permitted). * @param values the raw observations. * @param bins the number of bins (must be at least 1). * @param minimum the lower bound of the bin range. * @param maximum the upper bound of the bin range. */ public void addSeries(Comparable key, double[] values, int bins, double minimum, double maximum) { if (key == null) { throw new IllegalArgumentException("Null 'key' argument."); } if (values == null) { throw new IllegalArgumentException("Null 'values' argument."); } else if (bins < 1) { throw new IllegalArgumentException( "The 'bins' value must be at least 1."); } double binWidth = (maximum - minimum) / bins; double lower = minimum; double upper; List binList = new ArrayList(bins); for (int i = 0; i < bins; i++) { HistogramBin bin; // make sure bins[bins.length]'s upper boundary ends at maximum // to avoid the rounding issue. the bins[0] lower boundary is // guaranteed start from min if (i == bins - 1) { bin = new HistogramBin(lower, maximum); } else { upper = minimum + (i + 1) * binWidth; bin = new HistogramBin(lower, upper); lower = upper; } binList.add(bin); } // fill the bins for (int i = 0; i < values.length; i++) { int binIndex = bins - 1; if (values[i] < maximum) { double fraction = (values[i] - minimum) / (maximum - minimum); if (fraction < 0.0) { fraction = 0.0; } binIndex = (int) (fraction * bins); // rounding could result in binIndex being equal to bins // which will cause an IndexOutOfBoundsException - see bug // report 1553088 if (binIndex >= bins) { binIndex = bins - 1; } } HistogramBin bin = (HistogramBin) binList.get(binIndex); bin.incrementCount(); } // generic map for each series Map map = new HashMap(); map.put("key", key); map.put("bins", binList); map.put("values.length", new Integer(values.length)); map.put("bin width", new Double(binWidth)); this.list.add(map); fireDatasetChanged(); } /** * Returns the minimum value in an array of values. * * @param values the values (<code>null</code> not permitted and * zero-length array not permitted). * * @return The minimum value. */ private double getMinimum(double[] values) { if (values == null || values.length < 1) { throw new IllegalArgumentException( "Null or zero length 'values' argument."); } double min = Double.MAX_VALUE; for (int i = 0; i < values.length; i++) { if (values[i] < min) { min = values[i]; } } return min; } /** * Returns the maximum value in an array of values. * * @param values the values (<code>null</code> not permitted and * zero-length array not permitted). * * @return The maximum value. */ private double getMaximum(double[] values) { if (values == null || values.length < 1) { throw new IllegalArgumentException( "Null or zero length 'values' argument."); } double max = -Double.MAX_VALUE; for (int i = 0; i < values.length; i++) { if (values[i] > max) { max = values[i]; } } return max; } /** * Returns the bins for a series. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * * @return A list of bins. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ List getBins(int series) { Map map = (Map) this.list.get(series); return (List) map.get("bins"); } /** * Returns the total number of observations for a series. * * @param series the series index. * * @return The total. */ private int getTotal(int series) { Map map = (Map) this.list.get(series); return ((Integer) map.get("values.length")).intValue(); } /** * Returns the bin width for a series. * * @param series the series index (zero based). * * @return The bin width. */ private double getBinWidth(int series) { Map map = (Map) this.list.get(series); return ((Double) map.get("bin width")).doubleValue(); } /** * Returns the number of series in the dataset. * * @return The series count. */ public int getSeriesCount() { return this.list.size(); } /** * Returns the key for a series. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * * @return The series key. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ public Comparable getSeriesKey(int series) { Map map = (Map) this.list.get(series); return (Comparable) map.get("key"); } /** * Returns the number of data items for a series. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * * @return The item count. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ public int getItemCount(int series) { return getBins(series).size(); } /** * Returns the X value for a bin. This value won't be used for plotting * histograms, since the renderer will ignore it. But other renderers can * use it (for example, you could use the dataset to create a line * chart). * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (zero based). * * @return The start value. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ public Number getX(int series, int item) { List bins = getBins(series); HistogramBin bin = (HistogramBin) bins.get(item); double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.; return new Double(x); } /** * Returns the y-value for a bin (calculated to take into account the * histogram type). * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (zero based). * * @return The y-value. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ public Number getY(int series, int item) { List bins = getBins(series); HistogramBin bin = (HistogramBin) bins.get(item); double total = getTotal(series); double binWidth = getBinWidth(series); if (this.type == HistogramType.FREQUENCY) { return new Double(bin.getCount()); } else if (this.type == HistogramType.RELATIVE_FREQUENCY) { return new Double(bin.getCount() / total); } else if (this.type == HistogramType.SCALE_AREA_TO_1) { return new Double(bin.getCount() / (binWidth * total)); } else { // pretty sure this shouldn't ever happen throw new IllegalStateException(); } } /** * Returns the start value for a bin. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (zero based). * * @return The start value. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ public Number getStartX(int series, int item) { List bins = getBins(series); HistogramBin bin = (HistogramBin) bins.get(item); return new Double(bin.getStartBoundary()); } /** * Returns the end value for a bin. * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (zero based). * * @return The end value. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ public Number getEndX(int series, int item) { List bins = getBins(series); HistogramBin bin = (HistogramBin) bins.get(item); return new Double(bin.getEndBoundary()); } /** * Returns the start y-value for a bin (which is the same as the y-value, * this method exists only to support the general form of the * {@link IntervalXYDataset} interface). * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (zero based). * * @return The y-value. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ public Number getStartY(int series, int item) { return getY(series, item); } /** * Returns the end y-value for a bin (which is the same as the y-value, * this method exists only to support the general form of the * {@link IntervalXYDataset} interface). * * @param series the series index (in the range <code>0</code> to * <code>getSeriesCount() - 1</code>). * @param item the item index (zero based). * * @return The Y value. * * @throws IndexOutOfBoundsException if <code>series</code> is outside the * specified range. */ public Number getEndY(int series, int item) { return getY(series, item); } /** * Tests this dataset for equality with an arbitrary object. * * @param obj the object to test against (<code>null</code> permitted). * * @return A boolean. */ public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof HistogramDataset)) { return false; } HistogramDataset that = (HistogramDataset) obj; if (!ObjectUtilities.equal(this.type, that.type)) { return false; } if (!ObjectUtilities.equal(this.list, that.list)) { return false; } return true; } /** * Returns a clone of the dataset. * * @return A clone of the dataset. * * @throws CloneNotSupportedException if the object cannot be cloned. */ public Object clone() throws CloneNotSupportedException { HistogramDataset clone = (HistogramDataset) super.clone(); int seriesCount = getSeriesCount(); clone.list = new java.util.ArrayList(seriesCount); for (int i = 0; i < seriesCount; i++) { clone.list.add(new HashMap((Map) this.list.get(i))); } return clone; } }