001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2006, 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-2006, 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 * $Id: HistogramDataset.java,v 1.9.2.7 2006/09/07 15:26:49 mungady Exp $ 038 * 039 * Changes 040 * ------- 041 * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG); 042 * 07-Jul-2003 : Changed package and added Javadocs (DG); 043 * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW); 044 * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG); 045 * 01-Mar-2004 : Added equals() and clone() methods and implemented 046 * Serializable. Also added new addSeries() method (DG); 047 * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG); 048 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 049 * getYValue() (DG); 050 * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron 051 * Hayne (DG); 052 * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG); 053 * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG); 054 * ------------- JFREECHART 1.0.0 --------------------------------------------- 055 * 03-Aug-2006 : Improved precision of bin boundary calculation (DG); 056 * 07-Sep-2006 : Fixed bug 1553088 (DG); 057 * 058 */ 059 060 package org.jfree.data.statistics; 061 062 import java.io.Serializable; 063 import java.util.ArrayList; 064 import java.util.HashMap; 065 import java.util.List; 066 import java.util.Map; 067 068 import org.jfree.data.general.DatasetChangeEvent; 069 import org.jfree.data.xy.AbstractIntervalXYDataset; 070 import org.jfree.data.xy.IntervalXYDataset; 071 import org.jfree.util.ObjectUtilities; 072 import org.jfree.util.PublicCloneable; 073 074 /** 075 * A dataset that can be used for creating histograms. 076 * 077 * @see SimpleHistogramDataset 078 */ 079 public class HistogramDataset extends AbstractIntervalXYDataset 080 implements IntervalXYDataset, 081 Cloneable, PublicCloneable, 082 Serializable { 083 084 /** For serialization. */ 085 private static final long serialVersionUID = -6341668077370231153L; 086 087 /** A list of maps. */ 088 private List list; 089 090 /** The histogram type. */ 091 private HistogramType type; 092 093 /** 094 * Creates a new (empty) dataset with a default type of 095 * {@link HistogramType}.FREQUENCY. 096 */ 097 public HistogramDataset() { 098 this.list = new ArrayList(); 099 this.type = HistogramType.FREQUENCY; 100 } 101 102 /** 103 * Returns the histogram type. 104 * 105 * @return The type (never <code>null</code>). 106 */ 107 public HistogramType getType() { 108 return this.type; 109 } 110 111 /** 112 * Sets the histogram type and sends a {@link DatasetChangeEvent} to all 113 * registered listeners. 114 * 115 * @param type the type (<code>null</code> not permitted). 116 */ 117 public void setType(HistogramType type) { 118 if (type == null) { 119 throw new IllegalArgumentException("Null 'type' argument"); 120 } 121 this.type = type; 122 notifyListeners(new DatasetChangeEvent(this, this)); 123 } 124 125 /** 126 * Adds a series to the dataset, using the specified number of bins. 127 * 128 * @param key the series key (<code>null</code> not permitted). 129 * @param values the values (<code>null</code> not permitted). 130 * @param bins the number of bins (must be at least 1). 131 */ 132 public void addSeries(Comparable key, double[] values, int bins) { 133 // defer argument checking... 134 double minimum = getMinimum(values); 135 double maximum = getMaximum(values); 136 addSeries(key, values, bins, minimum, maximum); 137 } 138 139 /** 140 * Adds a series to the dataset. Any data value less than minimum will be 141 * assigned to the first bin, and any data value greater than maximum will 142 * be assigned to the last bin. Values falling on the boundary of 143 * adjacent bins will be assigned to the higher indexed bin. 144 * 145 * @param key the series key (<code>null</code> not permitted). 146 * @param values the raw observations. 147 * @param bins the number of bins (must be at least 1). 148 * @param minimum the lower bound of the bin range. 149 * @param maximum the upper bound of the bin range. 150 */ 151 public void addSeries(Comparable key, 152 double[] values, 153 int bins, 154 double minimum, 155 double maximum) { 156 157 if (key == null) { 158 throw new IllegalArgumentException("Null 'key' argument."); 159 } 160 if (values == null) { 161 throw new IllegalArgumentException("Null 'values' argument."); 162 } 163 else if (bins < 1) { 164 throw new IllegalArgumentException( 165 "The 'bins' value must be at least 1."); 166 } 167 double binWidth = (maximum - minimum) / bins; 168 169 double lower = minimum; 170 double upper; 171 List binList = new ArrayList(bins); 172 for (int i = 0; i < bins; i++) { 173 HistogramBin bin; 174 // make sure bins[bins.length]'s upper boundary ends at maximum 175 // to avoid the rounding issue. the bins[0] lower boundary is 176 // guaranteed start from min 177 if (i == bins - 1) { 178 bin = new HistogramBin(lower, maximum); 179 } 180 else { 181 upper = minimum + (i + 1) * binWidth; 182 bin = new HistogramBin(lower, upper); 183 lower = upper; 184 } 185 binList.add(bin); 186 } 187 // fill the bins 188 for (int i = 0; i < values.length; i++) { 189 int binIndex = bins - 1; 190 if (values[i] < maximum) { 191 double fraction = (values[i] - minimum) / (maximum - minimum); 192 if (fraction < 0.0) { 193 fraction = 0.0; 194 } 195 binIndex = (int) (fraction * bins); 196 // rounding could result in binIndex being equal to bins 197 // which will cause an IndexOutOfBoundsException - see bug 198 // report 1553088 199 if (binIndex >= bins) { 200 binIndex = bins - 1; 201 } 202 } 203 HistogramBin bin = (HistogramBin) binList.get(binIndex); 204 bin.incrementCount(); 205 } 206 // generic map for each series 207 Map map = new HashMap(); 208 map.put("key", key); 209 map.put("bins", binList); 210 map.put("values.length", new Integer(values.length)); 211 map.put("bin width", new Double(binWidth)); 212 this.list.add(map); 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 return super.clone(); 499 } 500 501 }