001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, 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-2021, 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; 048 049import org.jfree.chart.internal.Args; 050import org.jfree.chart.api.PublicCloneable; 051 052import org.jfree.data.general.DatasetChangeEvent; 053import org.jfree.data.xy.AbstractIntervalXYDataset; 054import org.jfree.data.xy.IntervalXYDataset; 055 056/** 057 * A dataset that can be used for creating histograms. 058 * 059 * @see SimpleHistogramDataset 060 */ 061public class HistogramDataset extends AbstractIntervalXYDataset 062 implements IntervalXYDataset, Cloneable, PublicCloneable, 063 Serializable { 064 065 /** For serialization. */ 066 private static final long serialVersionUID = -6341668077370231153L; 067 068 /** A list of maps. */ 069 private List<Map<String, Object>> list; 070 071 /** The histogram type. */ 072 private HistogramType type; 073 074 /** 075 * Creates a new (empty) dataset with a default type of 076 * {@link HistogramType}.FREQUENCY. 077 */ 078 public HistogramDataset() { 079 this.list = new ArrayList<>(); 080 this.type = HistogramType.FREQUENCY; 081 } 082 083 /** 084 * Returns the histogram type. 085 * 086 * @return The type (never {@code null}). 087 */ 088 public HistogramType getType() { 089 return this.type; 090 } 091 092 /** 093 * Sets the histogram type and sends a {@link DatasetChangeEvent} to all 094 * registered listeners. 095 * 096 * @param type the type ({@code null} not permitted). 097 */ 098 public void setType(HistogramType type) { 099 Args.nullNotPermitted(type, "type"); 100 this.type = type; 101 fireDatasetChanged(); 102 } 103 104 /** 105 * Adds a series to the dataset, using the specified number of bins, 106 * and sends a {@link DatasetChangeEvent} to all registered listeners. 107 * 108 * @param key the series key ({@code null} not permitted). 109 * @param values the values ({@code null} not permitted). 110 * @param bins the number of bins (must be at least 1). 111 */ 112 public void addSeries(Comparable key, double[] values, int bins) { 113 // defer argument checking... 114 double minimum = getMinimum(values); 115 double maximum = getMaximum(values); 116 addSeries(key, values, bins, minimum, maximum); 117 } 118 119 /** 120 * Adds a series to the dataset. Any data value less than minimum will be 121 * assigned to the first bin, and any data value greater than maximum will 122 * be assigned to the last bin. Values falling on the boundary of 123 * adjacent bins will be assigned to the higher indexed bin. 124 * 125 * @param key the series key ({@code null} not permitted). 126 * @param values the raw observations. 127 * @param bins the number of bins (must be at least 1). 128 * @param minimum the lower bound of the bin range. 129 * @param maximum the upper bound of the bin range. 130 */ 131 public void addSeries(Comparable key, double[] values, int bins, 132 double minimum, double maximum) { 133 134 Args.nullNotPermitted(key, "key"); 135 Args.nullNotPermitted(values, "values"); 136 if (bins < 1) { 137 throw new IllegalArgumentException( 138 "The 'bins' value must be at least 1."); 139 } 140 double binWidth = (maximum - minimum) / bins; 141 142 double lower = minimum; 143 double upper; 144 List<HistogramBin> binList = new ArrayList<>(bins); 145 for (int i = 0; i < bins; i++) { 146 HistogramBin bin; 147 // make sure bins[bins.length]'s upper boundary ends at maximum 148 // to avoid the rounding issue. the bins[0] lower boundary is 149 // guaranteed start from min 150 if (i == bins - 1) { 151 bin = new HistogramBin(lower, maximum); 152 } 153 else { 154 upper = minimum + (i + 1) * binWidth; 155 bin = new HistogramBin(lower, upper); 156 lower = upper; 157 } 158 binList.add(bin); 159 } 160 // fill the bins 161 for (int i = 0; i < values.length; i++) { 162 int binIndex = bins - 1; 163 if (values[i] < maximum) { 164 double fraction = (values[i] - minimum) / (maximum - minimum); 165 if (fraction < 0.0) { 166 fraction = 0.0; 167 } 168 binIndex = (int) (fraction * bins); 169 // rounding could result in binIndex being equal to bins 170 // which will cause an IndexOutOfBoundsException - see bug 171 // report 1553088 172 if (binIndex >= bins) { 173 binIndex = bins - 1; 174 } 175 } 176 HistogramBin bin = (HistogramBin) binList.get(binIndex); 177 bin.incrementCount(); 178 } 179 // generic map for each series 180 Map<String, Object> map = new HashMap<>(); 181 map.put("key", key); 182 map.put("bins", binList); 183 map.put("values.length", values.length); 184 map.put("bin width", binWidth); 185 this.list.add(map); 186 fireDatasetChanged(); 187 } 188 189 /** 190 * Returns the minimum value in an array of values. 191 * 192 * @param values the values ({@code null} not permitted and 193 * zero-length array not permitted). 194 * 195 * @return The minimum value. 196 */ 197 private double getMinimum(double[] values) { 198 if (values == null || values.length < 1) { 199 throw new IllegalArgumentException( 200 "Null or zero length 'values' argument."); 201 } 202 double min = Double.MAX_VALUE; 203 for (int i = 0; i < values.length; i++) { 204 if (values[i] < min) { 205 min = values[i]; 206 } 207 } 208 return min; 209 } 210 211 /** 212 * Returns the maximum value in an array of values. 213 * 214 * @param values the values ({@code null} not permitted and 215 * zero-length array not permitted). 216 * 217 * @return The maximum value. 218 */ 219 private double getMaximum(double[] values) { 220 if (values == null || values.length < 1) { 221 throw new IllegalArgumentException( 222 "Null or zero length 'values' argument."); 223 } 224 double max = -Double.MAX_VALUE; 225 for (int i = 0; i < values.length; i++) { 226 if (values[i] > max) { 227 max = values[i]; 228 } 229 } 230 return max; 231 } 232 233 /** 234 * Returns the bins for a series. 235 * 236 * @param series the series index (in the range {@code 0} to 237 * {@code getSeriesCount() - 1}). 238 * 239 * @return A list of bins. 240 * 241 * @throws IndexOutOfBoundsException if {@code series} is outside the 242 * specified range. 243 */ 244 List<HistogramBin> getBins(int series) { 245 Map<String, Object> map = this.list.get(series); 246 return (List<HistogramBin>) map.get("bins"); 247 } 248 249 /** 250 * Returns the total number of observations for a series. 251 * 252 * @param series the series index. 253 * 254 * @return The total. 255 */ 256 private int getTotal(int series) { 257 Map<String, Object> map = this.list.get(series); 258 return (Integer) map.get("values.length"); 259 } 260 261 /** 262 * Returns the bin width for a series. 263 * 264 * @param series the series index (zero based). 265 * 266 * @return The bin width. 267 */ 268 private double getBinWidth(int series) { 269 Map<String, Object> map = this.list.get(series); 270 return (Double) map.get("bin width"); 271 } 272 273 /** 274 * Returns the number of series in the dataset. 275 * 276 * @return The series count. 277 */ 278 @Override 279 public int getSeriesCount() { 280 return this.list.size(); 281 } 282 283 /** 284 * Returns the key for a series. 285 * 286 * @param series the series index (in the range {@code 0} to 287 * {@code getSeriesCount() - 1}). 288 * 289 * @return The series key. 290 * 291 * @throws IndexOutOfBoundsException if {@code series} is outside the 292 * specified range. 293 */ 294 @Override 295 public Comparable getSeriesKey(int series) { 296 Map<String, Object> map = this.list.get(series); 297 return (Comparable) map.get("key"); 298 } 299 300 /** 301 * Returns the number of data items for a series. 302 * 303 * @param series the series index (in the range {@code 0} to 304 * {@code getSeriesCount() - 1}). 305 * 306 * @return The item count. 307 * 308 * @throws IndexOutOfBoundsException if {@code series} is outside the 309 * specified range. 310 */ 311 @Override 312 public int getItemCount(int series) { 313 return getBins(series).size(); 314 } 315 316 /** 317 * Returns the X value for a bin. This value won't be used for plotting 318 * histograms, since the renderer will ignore it. But other renderers can 319 * use it (for example, you could use the dataset to create a line 320 * chart). 321 * 322 * @param series the series index (in the range {@code 0} to 323 * {@code getSeriesCount() - 1}). 324 * @param item the item index (zero based). 325 * 326 * @return The start value. 327 * 328 * @throws IndexOutOfBoundsException if {@code series} is outside the 329 * specified range. 330 */ 331 @Override 332 public Number getX(int series, int item) { 333 List<HistogramBin> bins = getBins(series); 334 HistogramBin bin = bins.get(item); 335 return (bin.getStartBoundary() + bin.getEndBoundary()) / 2.0; 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<HistogramBin> bins = getBins(series); 354 HistogramBin bin = 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<HistogramBin> bins = getBins(series); 387 HistogramBin bin = 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<HistogramBin> bins = getBins(series); 406 HistogramBin bin = 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 @Override 474 public int hashCode(){ 475 int hash = 3; 476 hash = 83 * hash + Objects.hashCode(this.list); 477 hash = 83 * hash + Objects.hashCode(this.type); 478 return hash; 479 } 480 481 /** 482 * Returns a clone of the dataset. 483 * 484 * @return A clone of the dataset. 485 * 486 * @throws CloneNotSupportedException if the object cannot be cloned. 487 */ 488 @Override 489 public Object clone() throws CloneNotSupportedException { 490 HistogramDataset clone = (HistogramDataset) super.clone(); 491 int seriesCount = getSeriesCount(); 492 clone.list = new ArrayList<>(seriesCount); 493 for (int i = 0; i < seriesCount; i++) { 494 clone.list.add(new HashMap(this.list.get(i))); 495 } 496 return clone; 497 } 498 499}