001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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-2008, 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 * Changes 038 * ------- 039 * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG); 040 * 07-Jul-2003 : Changed package and added Javadocs (DG); 041 * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW); 042 * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG); 043 * 01-Mar-2004 : Added equals() and clone() methods and implemented 044 * Serializable. Also added new addSeries() method (DG); 045 * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG); 046 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 047 * getYValue() (DG); 048 * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron 049 * Hayne (DG); 050 * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG); 051 * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG); 052 * ------------- JFREECHART 1.0.x --------------------------------------------- 053 * 03-Aug-2006 : Improved precision of bin boundary calculation (DG); 054 * 07-Sep-2006 : Fixed bug 1553088 (DG); 055 * 22-May-2008 : Implemented clone() method override (DG); 056 * 057 */ 058 059 package org.jfree.data.statistics; 060 061 import java.io.Serializable; 062 import java.util.ArrayList; 063 import java.util.HashMap; 064 import java.util.List; 065 import java.util.Map; 066 067 import org.jfree.data.general.DatasetChangeEvent; 068 import org.jfree.data.xy.AbstractIntervalXYDataset; 069 import org.jfree.data.xy.IntervalXYDataset; 070 import org.jfree.util.ObjectUtilities; 071 import org.jfree.util.PublicCloneable; 072 073 /** 074 * A dataset that can be used for creating histograms. 075 * 076 * @see SimpleHistogramDataset 077 */ 078 public class HistogramDataset extends AbstractIntervalXYDataset 079 implements IntervalXYDataset, Cloneable, PublicCloneable, 080 Serializable { 081 082 /** For serialization. */ 083 private static final long serialVersionUID = -6341668077370231153L; 084 085 /** A list of maps. */ 086 private List list; 087 088 /** The histogram type. */ 089 private HistogramType type; 090 091 /** 092 * Creates a new (empty) dataset with a default type of 093 * {@link HistogramType}.FREQUENCY. 094 */ 095 public HistogramDataset() { 096 this.list = new ArrayList(); 097 this.type = HistogramType.FREQUENCY; 098 } 099 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 }