001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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-present, by Jelai Wang and Contributors. 031 * 032 * Original Author: Jelai Wang (jelaiw AT mindspring.com); 033 * Contributor(s): David Gilbert; 034 * Cameron Hayne; 035 * Rikard Bj?rklind; 036 * Thomas A Caswell (patch 2902842); 037 * 038 */ 039 040package org.jfree.data.statistics; 041 042import java.io.Serializable; 043import java.util.ArrayList; 044import java.util.HashMap; 045import java.util.List; 046import java.util.Map; 047import java.util.Objects; 048import org.jfree.chart.util.Args; 049import org.jfree.chart.util.PublicCloneable; 050 051import org.jfree.data.general.DatasetChangeEvent; 052import org.jfree.data.xy.AbstractIntervalXYDataset; 053import org.jfree.data.xy.IntervalXYDataset; 054 055/** 056 * A dataset that can be used for creating histograms. 057 * 058 * @see SimpleHistogramDataset 059 */ 060public class HistogramDataset extends AbstractIntervalXYDataset 061 implements IntervalXYDataset, Cloneable, PublicCloneable, 062 Serializable { 063 064 /** For serialization. */ 065 private static final long serialVersionUID = -6341668077370231153L; 066 067 /** A list of maps. */ 068 private List list; 069 070 /** The histogram type. */ 071 private HistogramType type; 072 073 /** 074 * Creates a new (empty) dataset with a default type of 075 * {@link HistogramType}.FREQUENCY. 076 */ 077 public HistogramDataset() { 078 this.list = new ArrayList(); 079 this.type = HistogramType.FREQUENCY; 080 } 081 082 /** 083 * Returns the histogram type. 084 * 085 * @return The type (never {@code null}). 086 */ 087 public HistogramType getType() { 088 return this.type; 089 } 090 091 /** 092 * Sets the histogram type and sends a {@link DatasetChangeEvent} to all 093 * registered listeners. 094 * 095 * @param type the type ({@code null} not permitted). 096 */ 097 public void setType(HistogramType type) { 098 Args.nullNotPermitted(type, "type"); 099 this.type = type; 100 fireDatasetChanged(); 101 } 102 103 /** 104 * Adds a series to the dataset, using the specified number of bins, 105 * and sends a {@link DatasetChangeEvent} to all registered listeners. 106 * 107 * @param key the series key ({@code null} not permitted). 108 * @param values the values ({@code null} not permitted). 109 * @param bins the number of bins (must be at least 1). 110 */ 111 public void addSeries(Comparable key, double[] values, int bins) { 112 // defer argument checking... 113 double minimum = getMinimum(values); 114 double maximum = getMaximum(values); 115 addSeries(key, values, bins, minimum, maximum); 116 } 117 118 /** 119 * Adds a series to the dataset. Any data value less than minimum will be 120 * assigned to the first bin, and any data value greater than maximum will 121 * be assigned to the last bin. Values falling on the boundary of 122 * adjacent bins will be assigned to the higher indexed bin. 123 * 124 * @param key the series key ({@code null} not permitted). 125 * @param values the raw observations. 126 * @param bins the number of bins (must be at least 1). 127 * @param minimum the lower bound of the bin range. 128 * @param maximum the upper bound of the bin range. 129 */ 130 public void addSeries(Comparable key, double[] values, int bins, 131 double minimum, double maximum) { 132 133 Args.nullNotPermitted(key, "key"); 134 Args.nullNotPermitted(values, "values"); 135 if (bins < 1) { 136 throw new IllegalArgumentException( 137 "The 'bins' value must be at least 1."); 138 } 139 double binWidth = (maximum - minimum) / bins; 140 141 double lower = minimum; 142 double upper; 143 List binList = new ArrayList(bins); 144 for (int i = 0; i < bins; i++) { 145 HistogramBin bin; 146 // make sure bins[bins.length]'s upper boundary ends at maximum 147 // to avoid the rounding issue. the bins[0] lower boundary is 148 // guaranteed start from min 149 if (i == bins - 1) { 150 bin = new HistogramBin(lower, maximum); 151 } 152 else { 153 upper = minimum + (i + 1) * binWidth; 154 bin = new HistogramBin(lower, upper); 155 lower = upper; 156 } 157 binList.add(bin); 158 } 159 // fill the bins 160 for (int i = 0; i < values.length; i++) { 161 int binIndex = bins - 1; 162 if (values[i] < maximum) { 163 double fraction = (values[i] - minimum) / (maximum - minimum); 164 if (fraction < 0.0) { 165 fraction = 0.0; 166 } 167 binIndex = (int) (fraction * bins); 168 // rounding could result in binIndex being equal to bins 169 // which will cause an IndexOutOfBoundsException - see bug 170 // report 1553088 171 if (binIndex >= bins) { 172 binIndex = bins - 1; 173 } 174 } 175 HistogramBin bin = (HistogramBin) binList.get(binIndex); 176 bin.incrementCount(); 177 } 178 // generic map for each series 179 Map map = new HashMap(); 180 map.put("key", key); 181 map.put("bins", binList); 182 map.put("values.length", values.length); 183 map.put("bin width", binWidth); 184 this.list.add(map); 185 fireDatasetChanged(); 186 } 187 188 /** 189 * Returns the minimum value in an array of values. 190 * 191 * @param values the values ({@code null} not permitted and 192 * zero-length array not permitted). 193 * 194 * @return The minimum value. 195 */ 196 private double getMinimum(double[] values) { 197 if (values == null || values.length < 1) { 198 throw new IllegalArgumentException( 199 "Null or zero length 'values' argument."); 200 } 201 double min = Double.MAX_VALUE; 202 for (int i = 0; i < values.length; i++) { 203 if (values[i] < min) { 204 min = values[i]; 205 } 206 } 207 return min; 208 } 209 210 /** 211 * Returns the maximum value in an array of values. 212 * 213 * @param values the values ({@code null} not permitted and 214 * zero-length array not permitted). 215 * 216 * @return The maximum value. 217 */ 218 private double getMaximum(double[] values) { 219 if (values == null || values.length < 1) { 220 throw new IllegalArgumentException( 221 "Null or zero length 'values' argument."); 222 } 223 double max = -Double.MAX_VALUE; 224 for (int i = 0; i < values.length; i++) { 225 if (values[i] > max) { 226 max = values[i]; 227 } 228 } 229 return max; 230 } 231 232 /** 233 * Returns the bins for a series. 234 * 235 * @param series the series index (in the range {@code 0} to 236 * {@code getSeriesCount() - 1}). 237 * 238 * @return A list of bins. 239 * 240 * @throws IndexOutOfBoundsException if {@code series} is outside the 241 * specified range. 242 */ 243 List getBins(int series) { 244 Map map = (Map) this.list.get(series); 245 return (List) map.get("bins"); 246 } 247 248 /** 249 * Returns the total number of observations for a series. 250 * 251 * @param series the series index. 252 * 253 * @return The total. 254 */ 255 private int getTotal(int series) { 256 Map map = (Map) this.list.get(series); 257 return ((Integer) map.get("values.length")); 258 } 259 260 /** 261 * Returns the bin width for a series. 262 * 263 * @param series the series index (zero based). 264 * 265 * @return The bin width. 266 */ 267 private double getBinWidth(int series) { 268 Map map = (Map) this.list.get(series); 269 return ((Double) map.get("bin width")); 270 } 271 272 /** 273 * Returns the number of series in the dataset. 274 * 275 * @return The series count. 276 */ 277 @Override 278 public int getSeriesCount() { 279 return this.list.size(); 280 } 281 282 /** 283 * Returns the key for a series. 284 * 285 * @param series the series index (in the range {@code 0} to 286 * {@code getSeriesCount() - 1}). 287 * 288 * @return The series key. 289 * 290 * @throws IndexOutOfBoundsException if {@code series} is outside the 291 * specified range. 292 */ 293 @Override 294 public Comparable getSeriesKey(int series) { 295 Map map = (Map) this.list.get(series); 296 return (Comparable) map.get("key"); 297 } 298 299 /** 300 * Returns the number of data items for a series. 301 * 302 * @param series the series index (in the range {@code 0} to 303 * {@code getSeriesCount() - 1}). 304 * 305 * @return The item count. 306 * 307 * @throws IndexOutOfBoundsException if {@code series} is outside the 308 * specified range. 309 */ 310 @Override 311 public int getItemCount(int series) { 312 return getBins(series).size(); 313 } 314 315 /** 316 * Returns the X value for a bin. This value won't be used for plotting 317 * histograms, since the renderer will ignore it. But other renderers can 318 * use it (for example, you could use the dataset to create a line 319 * chart). 320 * 321 * @param series the series index (in the range {@code 0} to 322 * {@code getSeriesCount() - 1}). 323 * @param item the item index (zero based). 324 * 325 * @return The start value. 326 * 327 * @throws IndexOutOfBoundsException if {@code series} is outside the 328 * specified range. 329 */ 330 @Override 331 public Number getX(int series, int item) { 332 List bins = getBins(series); 333 HistogramBin bin = (HistogramBin) bins.get(item); 334 double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.; 335 return x; 336 } 337 338 /** 339 * Returns the y-value for a bin (calculated to take into account the 340 * histogram type). 341 * 342 * @param series the series index (in the range {@code 0} to 343 * {@code getSeriesCount() - 1}). 344 * @param item the item index (zero based). 345 * 346 * @return The y-value. 347 * 348 * @throws IndexOutOfBoundsException if {@code series} is outside the 349 * specified range. 350 */ 351 @Override 352 public Number getY(int series, int item) { 353 List bins = getBins(series); 354 HistogramBin bin = (HistogramBin) bins.get(item); 355 double total = getTotal(series); 356 double binWidth = getBinWidth(series); 357 358 if (this.type == HistogramType.FREQUENCY) { 359 return bin.getCount(); 360 } 361 else if (this.type == HistogramType.RELATIVE_FREQUENCY) { 362 return bin.getCount() / total; 363 } 364 else if (this.type == HistogramType.SCALE_AREA_TO_1) { 365 return bin.getCount() / (binWidth * total); 366 } 367 else { // pretty sure this shouldn't ever happen 368 throw new IllegalStateException(); 369 } 370 } 371 372 /** 373 * Returns the start value for a bin. 374 * 375 * @param series the series index (in the range {@code 0} to 376 * {@code getSeriesCount() - 1}). 377 * @param item the item index (zero based). 378 * 379 * @return The start value. 380 * 381 * @throws IndexOutOfBoundsException if {@code series} is outside the 382 * specified range. 383 */ 384 @Override 385 public Number getStartX(int series, int item) { 386 List bins = getBins(series); 387 HistogramBin bin = (HistogramBin) bins.get(item); 388 return bin.getStartBoundary(); 389 } 390 391 /** 392 * Returns the end value for a bin. 393 * 394 * @param series the series index (in the range {@code 0} to 395 * {@code getSeriesCount() - 1}). 396 * @param item the item index (zero based). 397 * 398 * @return The end value. 399 * 400 * @throws IndexOutOfBoundsException if {@code series} is outside the 401 * specified range. 402 */ 403 @Override 404 public Number getEndX(int series, int item) { 405 List bins = getBins(series); 406 HistogramBin bin = (HistogramBin) bins.get(item); 407 return bin.getEndBoundary(); 408 } 409 410 /** 411 * Returns the start y-value for a bin (which is the same as the y-value, 412 * this method exists only to support the general form of the 413 * {@link IntervalXYDataset} interface). 414 * 415 * @param series the series index (in the range {@code 0} to 416 * {@code getSeriesCount() - 1}). 417 * @param item the item index (zero based). 418 * 419 * @return The y-value. 420 * 421 * @throws IndexOutOfBoundsException if {@code series} is outside the 422 * specified range. 423 */ 424 @Override 425 public Number getStartY(int series, int item) { 426 return getY(series, item); 427 } 428 429 /** 430 * Returns the end y-value for a bin (which is the same as the y-value, 431 * this method exists only to support the general form of the 432 * {@link IntervalXYDataset} interface). 433 * 434 * @param series the series index (in the range {@code 0} to 435 * {@code getSeriesCount() - 1}). 436 * @param item the item index (zero based). 437 * 438 * @return The Y value. 439 * 440 * @throws IndexOutOfBoundsException if {@code series} is outside the 441 * specified range. 442 */ 443 @Override 444 public Number getEndY(int series, int item) { 445 return getY(series, item); 446 } 447 448 /** 449 * Tests this dataset for equality with an arbitrary object. 450 * 451 * @param obj the object to test against ({@code null} permitted). 452 * 453 * @return A boolean. 454 */ 455 @Override 456 public boolean equals(Object obj) { 457 if (obj == this) { 458 return true; 459 } 460 if (!(obj instanceof HistogramDataset)) { 461 return false; 462 } 463 HistogramDataset that = (HistogramDataset) obj; 464 if (!Objects.equals(this.type, that.type)) { 465 return false; 466 } 467 if (!Objects.equals(this.list, that.list)) { 468 return false; 469 } 470 return true; 471 } 472 473 /** 474 * Returns a clone of the dataset. 475 * 476 * @return A clone of the dataset. 477 * 478 * @throws CloneNotSupportedException if the object cannot be cloned. 479 */ 480 @Override 481 public Object clone() throws CloneNotSupportedException { 482 HistogramDataset clone = (HistogramDataset) super.clone(); 483 int seriesCount = getSeriesCount(); 484 clone.list = new java.util.ArrayList(seriesCount); 485 for (int i = 0; i < seriesCount; i++) { 486 clone.list.add(new HashMap((Map) this.list.get(i))); 487 } 488 return clone; 489 } 490 491}