001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2013, 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-2013, 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 * 03-Jul-2013 : Use ParamChecks (DG); 060 * 061 */ 062 063package org.jfree.data.statistics; 064 065import java.io.Serializable; 066import java.util.ArrayList; 067import java.util.HashMap; 068import java.util.List; 069import java.util.Map; 070import org.jfree.chart.util.ParamChecks; 071 072import org.jfree.data.general.DatasetChangeEvent; 073import org.jfree.data.xy.AbstractIntervalXYDataset; 074import org.jfree.data.xy.IntervalXYDataset; 075import org.jfree.util.ObjectUtilities; 076import org.jfree.util.PublicCloneable; 077 078/** 079 * A dataset that can be used for creating histograms. 080 * 081 * @see SimpleHistogramDataset 082 */ 083public class HistogramDataset extends AbstractIntervalXYDataset 084 implements IntervalXYDataset, Cloneable, PublicCloneable, 085 Serializable { 086 087 /** For serialization. */ 088 private static final long serialVersionUID = -6341668077370231153L; 089 090 /** A list of maps. */ 091 private List list; 092 093 /** The histogram type. */ 094 private HistogramType type; 095 096 /** 097 * Creates a new (empty) dataset with a default type of 098 * {@link HistogramType}.FREQUENCY. 099 */ 100 public HistogramDataset() { 101 this.list = new ArrayList(); 102 this.type = HistogramType.FREQUENCY; 103 } 104 105 /** 106 * Returns the histogram type. 107 * 108 * @return The type (never <code>null</code>). 109 */ 110 public HistogramType getType() { 111 return this.type; 112 } 113 114 /** 115 * Sets the histogram type and sends a {@link DatasetChangeEvent} to all 116 * registered listeners. 117 * 118 * @param type the type (<code>null</code> not permitted). 119 */ 120 public void setType(HistogramType type) { 121 ParamChecks.nullNotPermitted(type, "type"); 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 ParamChecks.nullNotPermitted(key, "key"); 157 ParamChecks.nullNotPermitted(values, "values"); 158 if (bins < 1) { 159 throw new IllegalArgumentException( 160 "The 'bins' value must be at least 1."); 161 } 162 double binWidth = (maximum - minimum) / bins; 163 164 double lower = minimum; 165 double upper; 166 List binList = new ArrayList(bins); 167 for (int i = 0; i < bins; i++) { 168 HistogramBin bin; 169 // make sure bins[bins.length]'s upper boundary ends at maximum 170 // to avoid the rounding issue. the bins[0] lower boundary is 171 // guaranteed start from min 172 if (i == bins - 1) { 173 bin = new HistogramBin(lower, maximum); 174 } 175 else { 176 upper = minimum + (i + 1) * binWidth; 177 bin = new HistogramBin(lower, upper); 178 lower = upper; 179 } 180 binList.add(bin); 181 } 182 // fill the bins 183 for (int i = 0; i < values.length; i++) { 184 int binIndex = bins - 1; 185 if (values[i] < maximum) { 186 double fraction = (values[i] - minimum) / (maximum - minimum); 187 if (fraction < 0.0) { 188 fraction = 0.0; 189 } 190 binIndex = (int) (fraction * bins); 191 // rounding could result in binIndex being equal to bins 192 // which will cause an IndexOutOfBoundsException - see bug 193 // report 1553088 194 if (binIndex >= bins) { 195 binIndex = bins - 1; 196 } 197 } 198 HistogramBin bin = (HistogramBin) binList.get(binIndex); 199 bin.incrementCount(); 200 } 201 // generic map for each series 202 Map map = new HashMap(); 203 map.put("key", key); 204 map.put("bins", binList); 205 map.put("values.length", new Integer(values.length)); 206 map.put("bin width", new Double(binWidth)); 207 this.list.add(map); 208 fireDatasetChanged(); 209 } 210 211 /** 212 * Returns the minimum value in an array of values. 213 * 214 * @param values the values (<code>null</code> not permitted and 215 * zero-length array not permitted). 216 * 217 * @return The minimum value. 218 */ 219 private double getMinimum(double[] values) { 220 if (values == null || values.length < 1) { 221 throw new IllegalArgumentException( 222 "Null or zero length 'values' argument."); 223 } 224 double min = Double.MAX_VALUE; 225 for (int i = 0; i < values.length; i++) { 226 if (values[i] < min) { 227 min = values[i]; 228 } 229 } 230 return min; 231 } 232 233 /** 234 * Returns the maximum value in an array of values. 235 * 236 * @param values the values (<code>null</code> not permitted and 237 * zero-length array not permitted). 238 * 239 * @return The maximum value. 240 */ 241 private double getMaximum(double[] values) { 242 if (values == null || values.length < 1) { 243 throw new IllegalArgumentException( 244 "Null or zero length 'values' argument."); 245 } 246 double max = -Double.MAX_VALUE; 247 for (int i = 0; i < values.length; i++) { 248 if (values[i] > max) { 249 max = values[i]; 250 } 251 } 252 return max; 253 } 254 255 /** 256 * Returns the bins for a series. 257 * 258 * @param series the series index (in the range <code>0</code> to 259 * <code>getSeriesCount() - 1</code>). 260 * 261 * @return A list of bins. 262 * 263 * @throws IndexOutOfBoundsException if <code>series</code> is outside the 264 * specified range. 265 */ 266 List getBins(int series) { 267 Map map = (Map) this.list.get(series); 268 return (List) map.get("bins"); 269 } 270 271 /** 272 * Returns the total number of observations for a series. 273 * 274 * @param series the series index. 275 * 276 * @return The total. 277 */ 278 private int getTotal(int series) { 279 Map map = (Map) this.list.get(series); 280 return ((Integer) map.get("values.length")).intValue(); 281 } 282 283 /** 284 * Returns the bin width for a series. 285 * 286 * @param series the series index (zero based). 287 * 288 * @return The bin width. 289 */ 290 private double getBinWidth(int series) { 291 Map map = (Map) this.list.get(series); 292 return ((Double) map.get("bin width")).doubleValue(); 293 } 294 295 /** 296 * Returns the number of series in the dataset. 297 * 298 * @return The series count. 299 */ 300 @Override 301 public int getSeriesCount() { 302 return this.list.size(); 303 } 304 305 /** 306 * Returns the key for a series. 307 * 308 * @param series the series index (in the range <code>0</code> to 309 * <code>getSeriesCount() - 1</code>). 310 * 311 * @return The series key. 312 * 313 * @throws IndexOutOfBoundsException if <code>series</code> is outside the 314 * specified range. 315 */ 316 @Override 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 @Override 334 public int getItemCount(int series) { 335 return getBins(series).size(); 336 } 337 338 /** 339 * Returns the X value for a bin. This value won't be used for plotting 340 * histograms, since the renderer will ignore it. But other renderers can 341 * use it (for example, you could use the dataset to create a line 342 * chart). 343 * 344 * @param series the series index (in the range <code>0</code> to 345 * <code>getSeriesCount() - 1</code>). 346 * @param item the item index (zero based). 347 * 348 * @return The start value. 349 * 350 * @throws IndexOutOfBoundsException if <code>series</code> is outside the 351 * specified range. 352 */ 353 @Override 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 @Override 375 public Number getY(int series, int item) { 376 List bins = getBins(series); 377 HistogramBin bin = (HistogramBin) bins.get(item); 378 double total = getTotal(series); 379 double binWidth = getBinWidth(series); 380 381 if (this.type == HistogramType.FREQUENCY) { 382 return new Double(bin.getCount()); 383 } 384 else if (this.type == HistogramType.RELATIVE_FREQUENCY) { 385 return new Double(bin.getCount() / total); 386 } 387 else if (this.type == HistogramType.SCALE_AREA_TO_1) { 388 return new Double(bin.getCount() / (binWidth * total)); 389 } 390 else { // pretty sure this shouldn't ever happen 391 throw new IllegalStateException(); 392 } 393 } 394 395 /** 396 * Returns the start value for a bin. 397 * 398 * @param series the series index (in the range <code>0</code> to 399 * <code>getSeriesCount() - 1</code>). 400 * @param item the item index (zero based). 401 * 402 * @return The start value. 403 * 404 * @throws IndexOutOfBoundsException if <code>series</code> is outside the 405 * specified range. 406 */ 407 @Override 408 public Number getStartX(int series, int item) { 409 List bins = getBins(series); 410 HistogramBin bin = (HistogramBin) bins.get(item); 411 return new Double(bin.getStartBoundary()); 412 } 413 414 /** 415 * Returns the end value for a bin. 416 * 417 * @param series the series index (in the range <code>0</code> to 418 * <code>getSeriesCount() - 1</code>). 419 * @param item the item index (zero based). 420 * 421 * @return The end value. 422 * 423 * @throws IndexOutOfBoundsException if <code>series</code> is outside the 424 * specified range. 425 */ 426 @Override 427 public Number getEndX(int series, int item) { 428 List bins = getBins(series); 429 HistogramBin bin = (HistogramBin) bins.get(item); 430 return new Double(bin.getEndBoundary()); 431 } 432 433 /** 434 * Returns the start y-value for a bin (which is the same as the y-value, 435 * this method exists only to support the general form of the 436 * {@link IntervalXYDataset} interface). 437 * 438 * @param series the series index (in the range <code>0</code> to 439 * <code>getSeriesCount() - 1</code>). 440 * @param item the item index (zero based). 441 * 442 * @return The y-value. 443 * 444 * @throws IndexOutOfBoundsException if <code>series</code> is outside the 445 * specified range. 446 */ 447 @Override 448 public Number getStartY(int series, int item) { 449 return getY(series, item); 450 } 451 452 /** 453 * Returns the end y-value for a bin (which is the same as the y-value, 454 * this method exists only to support the general form of the 455 * {@link IntervalXYDataset} interface). 456 * 457 * @param series the series index (in the range <code>0</code> to 458 * <code>getSeriesCount() - 1</code>). 459 * @param item the item index (zero based). 460 * 461 * @return The Y value. 462 * 463 * @throws IndexOutOfBoundsException if <code>series</code> is outside the 464 * specified range. 465 */ 466 @Override 467 public Number getEndY(int series, int item) { 468 return getY(series, item); 469 } 470 471 /** 472 * Tests this dataset for equality with an arbitrary object. 473 * 474 * @param obj the object to test against (<code>null</code> permitted). 475 * 476 * @return A boolean. 477 */ 478 @Override 479 public boolean equals(Object obj) { 480 if (obj == this) { 481 return true; 482 } 483 if (!(obj instanceof HistogramDataset)) { 484 return false; 485 } 486 HistogramDataset that = (HistogramDataset) obj; 487 if (!ObjectUtilities.equal(this.type, that.type)) { 488 return false; 489 } 490 if (!ObjectUtilities.equal(this.list, that.list)) { 491 return false; 492 } 493 return true; 494 } 495 496 /** 497 * Returns a clone of the dataset. 498 * 499 * @return A clone of the dataset. 500 * 501 * @throws CloneNotSupportedException if the object cannot be cloned. 502 */ 503 @Override 504 public Object clone() throws CloneNotSupportedException { 505 HistogramDataset clone = (HistogramDataset) super.clone(); 506 int seriesCount = getSeriesCount(); 507 clone.list = new java.util.ArrayList(seriesCount); 508 for (int i = 0; i < seriesCount; i++) { 509 clone.list.add(new HashMap((Map) this.list.get(i))); 510 } 511 return clone; 512 } 513 514}