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}