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.7 2006/09/07 15:26:49 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:  * 07-Sep-2006 : Fixed bug 1553088 (DG);
  57:  * 
  58:  */
  59: 
  60: package org.jfree.data.statistics;
  61: 
  62: import java.io.Serializable;
  63: import java.util.ArrayList;
  64: import java.util.HashMap;
  65: import java.util.List;
  66: import java.util.Map;
  67: 
  68: import org.jfree.data.general.DatasetChangeEvent;
  69: import org.jfree.data.xy.AbstractIntervalXYDataset;
  70: import org.jfree.data.xy.IntervalXYDataset;
  71: import org.jfree.util.ObjectUtilities;
  72: import org.jfree.util.PublicCloneable;
  73: 
  74: /**
  75:  * A dataset that can be used for creating histograms.
  76:  * 
  77:  * @see SimpleHistogramDataset
  78:  */
  79: public class HistogramDataset extends AbstractIntervalXYDataset 
  80:                               implements IntervalXYDataset, 
  81:                                          Cloneable, PublicCloneable, 
  82:                                          Serializable {
  83: 
  84:     /** For serialization. */
  85:     private static final long serialVersionUID = -6341668077370231153L;
  86:     
  87:     /** A list of maps. */
  88:     private List list;
  89:     
  90:     /** The histogram type. */
  91:     private HistogramType type;
  92: 
  93:     /**
  94:      * Creates a new (empty) dataset with a default type of 
  95:      * {@link HistogramType}.FREQUENCY.
  96:      */
  97:     public HistogramDataset() {
  98:         this.list = new ArrayList();
  99:         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: }