Source for org.jfree.data.statistics.HistogramDataset

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2006, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * ---------------------
  28:  * HistogramDataset.java
  29:  * ---------------------
  30:  * (C) Copyright 2003-2006, by Jelai Wang and Contributors.
  31:  *
  32:  * Original Author:  Jelai Wang (jelaiw AT mindspring.com);
  33:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  34:  *                   Cameron Hayne;
  35:  *                   Rikard Bj?rklind;
  36:  *
  37:  * $Id: HistogramDataset.java,v 1.9.2.6 2006/08/03 10:37:53 mungady Exp $
  38:  *
  39:  * Changes
  40:  * -------
  41:  * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG);
  42:  * 07-Jul-2003 : Changed package and added Javadocs (DG);
  43:  * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW);
  44:  * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG);
  45:  * 01-Mar-2004 : Added equals() and clone() methods and implemented 
  46:  *               Serializable.  Also added new addSeries() method (DG);
  47:  * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG);
  48:  * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
  49:  *               getYValue() (DG);
  50:  * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron
  51:  *               Hayne (DG);
  52:  * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG);
  53:  * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG);
  54:  * ------------- JFREECHART 1.0.0 ---------------------------------------------
  55:  * 03-Aug-2006 : Improved precision of bin boundary calculation (DG);
  56:  * 
  57:  */
  58: 
  59: package org.jfree.data.statistics;
  60: 
  61: import java.io.Serializable;
  62: import java.util.ArrayList;
  63: import java.util.HashMap;
  64: import java.util.List;
  65: import java.util.Map;
  66: 
  67: import org.jfree.data.general.DatasetChangeEvent;
  68: import org.jfree.data.xy.AbstractIntervalXYDataset;
  69: import org.jfree.data.xy.IntervalXYDataset;
  70: import org.jfree.util.ObjectUtilities;
  71: import org.jfree.util.PublicCloneable;
  72: 
  73: /**
  74:  * A dataset that can be used for creating histograms.
  75:  * 
  76:  * @see SimpleHistogramDataset
  77:  */
  78: public class HistogramDataset extends AbstractIntervalXYDataset 
  79:                               implements IntervalXYDataset, 
  80:                                          Cloneable, PublicCloneable, 
  81:                                          Serializable {
  82: 
  83:     /** For serialization. */
  84:     private static final long serialVersionUID = -6341668077370231153L;
  85:     
  86:     /** A list of maps. */
  87:     private List list;
  88:     
  89:     /** The histogram type. */
  90:     private HistogramType type;
  91: 
  92:     /**
  93:      * Creates a new (empty) dataset with a default type of 
  94:      * {@link HistogramType}.FREQUENCY.
  95:      */
  96:     public HistogramDataset() {
  97:         this.list = new ArrayList();
  98:         this.type = HistogramType.FREQUENCY;
  99:     }
 100:     
 101:     /**
 102:      * Returns the histogram type. 
 103:      * 
 104:      * @return The type (never <code>null</code>).
 105:      */
 106:     public HistogramType getType() { 
 107:         return this.type; 
 108:     }
 109: 
 110:     /**
 111:      * Sets the histogram type and sends a {@link DatasetChangeEvent} to all 
 112:      * registered listeners.
 113:      * 
 114:      * @param type  the type (<code>null</code> not permitted).
 115:      */
 116:     public void setType(HistogramType type) {
 117:         if (type == null) {
 118:             throw new IllegalArgumentException("Null 'type' argument");
 119:         }
 120:         this.type = type;   
 121:         notifyListeners(new DatasetChangeEvent(this, this));
 122:     }
 123: 
 124:     /**
 125:      * Adds a series to the dataset, using the specified number of bins.
 126:      * 
 127:      * @param key  the series key (<code>null</code> not permitted).
 128:      * @param values the values (<code>null</code> not permitted).
 129:      * @param bins  the number of bins (must be at least 1).
 130:      */
 131:     public void addSeries(Comparable key, double[] values, int bins) {
 132:         // defer argument checking...
 133:         double minimum = getMinimum(values);
 134:         double maximum = getMaximum(values);
 135:         addSeries(key, values, bins, minimum, maximum);
 136:     }
 137: 
 138:     /**
 139:      * Adds a series to the dataset. Any data value less than minimum will be
 140:      * assigned to the first bin, and any data value greater than maximum will
 141:      * be assigned to the last bin.  Values falling on the boundary of 
 142:      * adjacent bins will be assigned to the higher indexed bin.
 143:      * 
 144:      * @param key  the series key (<code>null</code> not permitted).
 145:      * @param values  the raw observations.
 146:      * @param bins  the number of bins (must be at least 1).
 147:      * @param minimum  the lower bound of the bin range.
 148:      * @param maximum  the upper bound of the bin range.
 149:      */
 150:     public void addSeries(Comparable key, 
 151:                           double[] values, 
 152:                           int bins, 
 153:                           double minimum, 
 154:                           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:             }
 196:             HistogramBin bin = (HistogramBin) binList.get(binIndex);
 197:             bin.incrementCount();
 198:         }
 199:         // generic map for each series
 200:         Map map = new HashMap();
 201:         map.put("key", key);
 202:         map.put("bins", binList);
 203:         map.put("values.length", new Integer(values.length));
 204:         map.put("bin width", new Double(binWidth));
 205:         this.list.add(map);
 206:     }
 207:     
 208:     /**
 209:      * Returns the minimum value in an array of values.
 210:      * 
 211:      * @param values  the values (<code>null</code> not permitted and 
 212:      *                zero-length array not permitted).
 213:      * 
 214:      * @return The minimum value.
 215:      */
 216:     private double getMinimum(double[] values) {
 217:         if (values == null || values.length < 1) {
 218:             throw new IllegalArgumentException(
 219:                     "Null or zero length 'values' argument.");
 220:         }
 221:         double min = Double.MAX_VALUE;
 222:         for (int i = 0; i < values.length; i++) {
 223:             if (values[i] < min) {
 224:                 min = values[i];
 225:             }
 226:         }
 227:         return min;
 228:     }
 229: 
 230:     /**
 231:      * Returns the maximum value in an array of values.
 232:      * 
 233:      * @param values  the values (<code>null</code> not permitted and 
 234:      *                zero-length array not permitted).
 235:      * 
 236:      * @return The maximum value.
 237:      */
 238:     private double getMaximum(double[] values) {
 239:         if (values == null || values.length < 1) {
 240:             throw new IllegalArgumentException(
 241:                     "Null or zero length 'values' argument.");
 242:         }
 243:         double max = -Double.MAX_VALUE;
 244:         for (int i = 0; i < values.length; i++) {
 245:             if (values[i] > max) {
 246:                 max = values[i];
 247:             }
 248:         }
 249:         return max;
 250:     }
 251: 
 252:     /**
 253:      * Returns the bins for a series.
 254:      * 
 255:      * @param series  the series index (in the range <code>0</code> to 
 256:      *     <code>getSeriesCount() - 1</code>).
 257:      * 
 258:      * @return A list of bins.
 259:      * 
 260:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 261:      *     specified range.
 262:      */
 263:     List getBins(int series) {
 264:         Map map = (Map) this.list.get(series);
 265:         return (List) map.get("bins"); 
 266:     }
 267: 
 268:     /**
 269:      * Returns the total number of observations for a series.
 270:      * 
 271:      * @param series  the series index.
 272:      * 
 273:      * @return The total.
 274:      */
 275:     private int getTotal(int series) {
 276:         Map map = (Map) this.list.get(series);
 277:         return ((Integer) map.get("values.length")).intValue(); 
 278:     }
 279: 
 280:     /**
 281:      * Returns the bin width for a series.
 282:      * 
 283:      * @param series  the series index (zero based).
 284:      * 
 285:      * @return The bin width.
 286:      */
 287:     private double getBinWidth(int series) {
 288:         Map map = (Map) this.list.get(series);
 289:         return ((Double) map.get("bin width")).doubleValue(); 
 290:     }
 291: 
 292:     /**
 293:      * Returns the number of series in the dataset.
 294:      * 
 295:      * @return The series count.
 296:      */
 297:     public int getSeriesCount() { 
 298:         return this.list.size(); 
 299:     }
 300:     
 301:     /**
 302:      * Returns the key for a series.
 303:      * 
 304:      * @param series  the series index (in the range <code>0</code> to 
 305:      *     <code>getSeriesCount() - 1</code>).
 306:      * 
 307:      * @return The series key.
 308:      * 
 309:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 310:      *     specified range.
 311:      */
 312:     public Comparable getSeriesKey(int series) {
 313:         Map map = (Map) this.list.get(series);
 314:         return (Comparable) map.get("key"); 
 315:     }
 316: 
 317:     /**
 318:      * Returns the number of data items for a series.
 319:      * 
 320:      * @param series  the series index (in the range <code>0</code> to 
 321:      *     <code>getSeriesCount() - 1</code>).
 322:      * 
 323:      * @return The item count.
 324:      * 
 325:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 326:      *     specified range.
 327:      */
 328:     public int getItemCount(int series) {
 329:         return getBins(series).size(); 
 330:     }
 331: 
 332:     /**
 333:      * Returns the X value for a bin.  This value won't be used for plotting 
 334:      * histograms, since the renderer will ignore it.  But other renderers can 
 335:      * use it (for example, you could use the dataset to create a line
 336:      * chart).
 337:      * 
 338:      * @param series  the series index (in the range <code>0</code> to 
 339:      *     <code>getSeriesCount() - 1</code>).
 340:      * @param item  the item index (zero based).
 341:      * 
 342:      * @return The start value.
 343:      * 
 344:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 345:      *     specified range.
 346:      */
 347:     public Number getX(int series, int item) {
 348:         List bins = getBins(series);
 349:         HistogramBin bin = (HistogramBin) bins.get(item);
 350:         double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
 351:         return new Double(x);
 352:     }
 353: 
 354:     /**
 355:      * Returns the y-value for a bin (calculated to take into account the 
 356:      * histogram type).
 357:      * 
 358:      * @param series  the series index (in the range <code>0</code> to 
 359:      *     <code>getSeriesCount() - 1</code>).
 360:      * @param item  the item index (zero based).
 361:      * 
 362:      * @return The y-value.
 363:      * 
 364:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 365:      *     specified range.
 366:      */
 367:     public Number getY(int series, int item) {
 368:         List bins = getBins(series);
 369:         HistogramBin bin = (HistogramBin) bins.get(item);
 370:         double total = getTotal(series);
 371:         double binWidth = getBinWidth(series);
 372: 
 373:         if (this.type == HistogramType.FREQUENCY) {
 374:             return new Double(bin.getCount());
 375:         }
 376:         else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
 377:             return new Double(bin.getCount() / total);
 378:         }
 379:         else if (this.type == HistogramType.SCALE_AREA_TO_1) {
 380:             return new Double(bin.getCount() / (binWidth * total));
 381:         }
 382:         else { // pretty sure this shouldn't ever happen
 383:             throw new IllegalStateException();
 384:         }
 385:     }
 386: 
 387:     /**
 388:      * Returns the start value for a bin.
 389:      * 
 390:      * @param series  the series index (in the range <code>0</code> to 
 391:      *     <code>getSeriesCount() - 1</code>).
 392:      * @param item  the item index (zero based).
 393:      * 
 394:      * @return The start value.
 395:      * 
 396:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 397:      *     specified range.
 398:      */
 399:     public Number getStartX(int series, int item) {
 400:         List bins = getBins(series);
 401:         HistogramBin bin = (HistogramBin) bins.get(item);
 402:         return new Double(bin.getStartBoundary());
 403:     }
 404: 
 405:     /**
 406:      * Returns the end value for a bin.
 407:      * 
 408:      * @param series  the series index (in the range <code>0</code> to 
 409:      *     <code>getSeriesCount() - 1</code>).
 410:      * @param item  the item index (zero based).
 411:      * 
 412:      * @return The end value.
 413:      * 
 414:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 415:      *     specified range.
 416:      */
 417:     public Number getEndX(int series, int item) {
 418:         List bins = getBins(series);
 419:         HistogramBin bin = (HistogramBin) bins.get(item);
 420:         return new Double(bin.getEndBoundary());
 421:     }
 422: 
 423:     /**
 424:      * Returns the start y-value for a bin (which is the same as the y-value, 
 425:      * this method exists only to support the general form of the 
 426:      * {@link IntervalXYDataset} interface).
 427:      * 
 428:      * @param series  the series index (in the range <code>0</code> to 
 429:      *     <code>getSeriesCount() - 1</code>).
 430:      * @param item  the item index (zero based).
 431:      * 
 432:      * @return The y-value.
 433:      * 
 434:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 435:      *     specified range.
 436:      */
 437:     public Number getStartY(int series, int item) {
 438:         return getY(series, item);
 439:     }
 440: 
 441:     /**
 442:      * Returns the end y-value for a bin (which is the same as the y-value, 
 443:      * this method exists only to support the general form of the 
 444:      * {@link IntervalXYDataset} interface).
 445:      * 
 446:      * @param series  the series index (in the range <code>0</code> to 
 447:      *     <code>getSeriesCount() - 1</code>).
 448:      * @param item  the item index (zero based).
 449:      * 
 450:      * @return The Y value.
 451:      * 
 452:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 453:      *     specified range.
 454:      */    
 455:     public Number getEndY(int series, int item) {
 456:         return getY(series, item);
 457:     }
 458: 
 459:     /**
 460:      * Tests this dataset for equality with an arbitrary object.
 461:      * 
 462:      * @param obj  the object to test against (<code>null</code> permitted).
 463:      * 
 464:      * @return A boolean.
 465:      */
 466:     public boolean equals(Object obj) {
 467:         if (obj == this) {
 468:             return true;   
 469:         }
 470:         if (!(obj instanceof HistogramDataset)) {
 471:             return false;
 472:         }
 473:         HistogramDataset that = (HistogramDataset) obj;
 474:         if (!ObjectUtilities.equal(this.type, that.type)) {
 475:             return false;
 476:         }
 477:         if (!ObjectUtilities.equal(this.list, that.list)) {
 478:             return false;
 479:         }
 480:         return true;   
 481:     }
 482: 
 483:     /**
 484:      * Returns a clone of the dataset.
 485:      * 
 486:      * @return A clone of the dataset.
 487:      * 
 488:      * @throws CloneNotSupportedException if the object cannot be cloned.
 489:      */
 490:     public Object clone() throws CloneNotSupportedException {
 491:         return super.clone();   
 492:     }
 493: 
 494: }