Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * --------------------- 28: * HistogramDataset.java 29: * --------------------- 30: * (C) Copyright 2003-2008, by Jelai Wang and Contributors. 31: * 32: * Original Author: Jelai Wang (jelaiw AT mindspring.com); 33: * Contributor(s): David Gilbert (for Object Refinery Limited); 34: * Cameron Hayne; 35: * Rikard Bj?rklind; 36: * 37: * Changes 38: * ------- 39: * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG); 40: * 07-Jul-2003 : Changed package and added Javadocs (DG); 41: * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW); 42: * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG); 43: * 01-Mar-2004 : Added equals() and clone() methods and implemented 44: * Serializable. Also added new addSeries() method (DG); 45: * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG); 46: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 47: * getYValue() (DG); 48: * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron 49: * Hayne (DG); 50: * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG); 51: * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG); 52: * ------------- JFREECHART 1.0.0 --------------------------------------------- 53: * 03-Aug-2006 : Improved precision of bin boundary calculation (DG); 54: * 07-Sep-2006 : Fixed bug 1553088 (DG); 55: * 22-May-2008 : Implemented clone() method override (DG); 56: * 57: */ 58: 59: package org.jfree.data.statistics; 60: 61: import java.io.Serializable; 62: import java.util.ArrayList; 63: import java.util.HashMap; 64: import java.util.List; 65: import java.util.Map; 66: 67: import org.jfree.data.general.DatasetChangeEvent; 68: import org.jfree.data.xy.AbstractIntervalXYDataset; 69: import org.jfree.data.xy.IntervalXYDataset; 70: import org.jfree.util.ObjectUtilities; 71: import org.jfree.util.PublicCloneable; 72: 73: /** 74: * A dataset that can be used for creating histograms. 75: * 76: * @see SimpleHistogramDataset 77: */ 78: public class HistogramDataset extends AbstractIntervalXYDataset 79: implements IntervalXYDataset, Cloneable, PublicCloneable, 80: Serializable { 81: 82: /** For serialization. */ 83: private static final long serialVersionUID = -6341668077370231153L; 84: 85: /** A list of maps. */ 86: private List list; 87: 88: /** The histogram type. */ 89: private HistogramType type; 90: 91: /** 92: * Creates a new (empty) dataset with a default type of 93: * {@link HistogramType}.FREQUENCY. 94: */ 95: public HistogramDataset() { 96: this.list = new ArrayList(); 97: this.type = HistogramType.FREQUENCY; 98: } 99: 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: }