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-2007, 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-2007, 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:  * Changes
  38:  * -------
  39:  * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG);
  40:  * 07-Jul-2003 : Changed package and added Javadocs (DG);
  41:  * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW);
  42:  * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG);
  43:  * 01-Mar-2004 : Added equals() and clone() methods and implemented 
  44:  *               Serializable.  Also added new addSeries() method (DG);
  45:  * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG);
  46:  * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
  47:  *               getYValue() (DG);
  48:  * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron
  49:  *               Hayne (DG);
  50:  * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG);
  51:  * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG);
  52:  * ------------- JFREECHART 1.0.0 ---------------------------------------------
  53:  * 03-Aug-2006 : Improved precision of bin boundary calculation (DG);
  54:  * 07-Sep-2006 : Fixed bug 1553088 (DG);
  55:  * 
  56:  */
  57: 
  58: package org.jfree.data.statistics;
  59: 
  60: import java.io.Serializable;
  61: import java.util.ArrayList;
  62: import java.util.HashMap;
  63: import java.util.List;
  64: import java.util.Map;
  65: 
  66: import org.jfree.data.general.DatasetChangeEvent;
  67: import org.jfree.data.xy.AbstractIntervalXYDataset;
  68: import org.jfree.data.xy.IntervalXYDataset;
  69: import org.jfree.util.ObjectUtilities;
  70: import org.jfree.util.PublicCloneable;
  71: 
  72: /**
  73:  * A dataset that can be used for creating histograms.
  74:  * 
  75:  * @see SimpleHistogramDataset
  76:  */
  77: public class HistogramDataset extends AbstractIntervalXYDataset 
  78:                               implements IntervalXYDataset, 
  79:                                          Cloneable, PublicCloneable, 
  80:                                          Serializable {
  81: 
  82:     /** For serialization. */
  83:     private static final long serialVersionUID = -6341668077370231153L;
  84:     
  85:     /** A list of maps. */
  86:     private List list;
  87:     
  88:     /** The histogram type. */
  89:     private HistogramType type;
  90: 
  91:     /**
  92:      * Creates a new (empty) dataset with a default type of 
  93:      * {@link HistogramType}.FREQUENCY.
  94:      */
  95:     public HistogramDataset() {
  96:         this.list = new ArrayList();
  97:         this.type = HistogramType.FREQUENCY;
  98:     }
  99:     
 100:     /**
 101:      * Returns the histogram type. 
 102:      * 
 103:      * @return The type (never <code>null</code>).
 104:      */
 105:     public HistogramType getType() { 
 106:         return this.type; 
 107:     }
 108: 
 109:     /**
 110:      * Sets the histogram type and sends a {@link DatasetChangeEvent} to all 
 111:      * registered listeners.
 112:      * 
 113:      * @param type  the type (<code>null</code> not permitted).
 114:      */
 115:     public void setType(HistogramType type) {
 116:         if (type == null) {
 117:             throw new IllegalArgumentException("Null 'type' argument");
 118:         }
 119:         this.type = type;   
 120:         notifyListeners(new DatasetChangeEvent(this, this));
 121:     }
 122: 
 123:     /**
 124:      * Adds a series to the dataset, using the specified number of bins.
 125:      * 
 126:      * @param key  the series key (<code>null</code> not permitted).
 127:      * @param values the values (<code>null</code> not permitted).
 128:      * @param bins  the number of bins (must be at least 1).
 129:      */
 130:     public void addSeries(Comparable key, double[] values, int bins) {
 131:         // defer argument checking...
 132:         double minimum = getMinimum(values);
 133:         double maximum = getMaximum(values);
 134:         addSeries(key, values, bins, minimum, maximum);
 135:     }
 136: 
 137:     /**
 138:      * Adds a series to the dataset. Any data value less than minimum will be
 139:      * assigned to the first bin, and any data value greater than maximum will
 140:      * be assigned to the last bin.  Values falling on the boundary of 
 141:      * adjacent bins will be assigned to the higher indexed bin.
 142:      * 
 143:      * @param key  the series key (<code>null</code> not permitted).
 144:      * @param values  the raw observations.
 145:      * @param bins  the number of bins (must be at least 1).
 146:      * @param minimum  the lower bound of the bin range.
 147:      * @param maximum  the upper bound of the bin range.
 148:      */
 149:     public void addSeries(Comparable key, 
 150:                           double[] values, 
 151:                           int bins, 
 152:                           double minimum, 
 153:                           double maximum) {
 154:         
 155:         if (key == null) {
 156:             throw new IllegalArgumentException("Null 'key' argument.");   
 157:         }
 158:         if (values == null) {
 159:             throw new IllegalArgumentException("Null 'values' argument.");
 160:         }
 161:         else if (bins < 1) {
 162:             throw new IllegalArgumentException(
 163:                     "The 'bins' value must be at least 1.");
 164:         }
 165:         double binWidth = (maximum - minimum) / bins;
 166: 
 167:         double lower = minimum;
 168:         double upper;
 169:         List binList = new ArrayList(bins);
 170:         for (int i = 0; i < bins; i++) {
 171:             HistogramBin bin;
 172:             // make sure bins[bins.length]'s upper boundary ends at maximum
 173:             // to avoid the rounding issue. the bins[0] lower boundary is
 174:             // guaranteed start from min
 175:             if (i == bins - 1) {
 176:                 bin = new HistogramBin(lower, maximum);
 177:             }
 178:             else {
 179:                 upper = minimum + (i + 1) * binWidth;
 180:                 bin = new HistogramBin(lower, upper);
 181:                 lower = upper;
 182:             }
 183:             binList.add(bin);
 184:         }        
 185:         // fill the bins
 186:         for (int i = 0; i < values.length; i++) {
 187:             int binIndex = bins - 1;
 188:             if (values[i] < maximum) {
 189:                 double fraction = (values[i] - minimum) / (maximum - minimum);
 190:                 if (fraction < 0.0) {
 191:                     fraction = 0.0;
 192:                 }
 193:                 binIndex = (int) (fraction * bins);
 194:                 // rounding could result in binIndex being equal to bins
 195:                 // which will cause an IndexOutOfBoundsException - see bug
 196:                 // report 1553088
 197:                 if (binIndex >= bins) {
 198:                     binIndex = bins - 1;
 199:                 }
 200:             }
 201:             HistogramBin bin = (HistogramBin) binList.get(binIndex);
 202:             bin.incrementCount();
 203:         }
 204:         // generic map for each series
 205:         Map map = new HashMap();
 206:         map.put("key", key);
 207:         map.put("bins", binList);
 208:         map.put("values.length", new Integer(values.length));
 209:         map.put("bin width", new Double(binWidth));
 210:         this.list.add(map);
 211:     }
 212:     
 213:     /**
 214:      * Returns the minimum value in an array of values.
 215:      * 
 216:      * @param values  the values (<code>null</code> not permitted and 
 217:      *                zero-length array not permitted).
 218:      * 
 219:      * @return The minimum value.
 220:      */
 221:     private double getMinimum(double[] values) {
 222:         if (values == null || values.length < 1) {
 223:             throw new IllegalArgumentException(
 224:                     "Null or zero length 'values' argument.");
 225:         }
 226:         double min = Double.MAX_VALUE;
 227:         for (int i = 0; i < values.length; i++) {
 228:             if (values[i] < min) {
 229:                 min = values[i];
 230:             }
 231:         }
 232:         return min;
 233:     }
 234: 
 235:     /**
 236:      * Returns the maximum value in an array of values.
 237:      * 
 238:      * @param values  the values (<code>null</code> not permitted and 
 239:      *                zero-length array not permitted).
 240:      * 
 241:      * @return The maximum value.
 242:      */
 243:     private double getMaximum(double[] values) {
 244:         if (values == null || values.length < 1) {
 245:             throw new IllegalArgumentException(
 246:                     "Null or zero length 'values' argument.");
 247:         }
 248:         double max = -Double.MAX_VALUE;
 249:         for (int i = 0; i < values.length; i++) {
 250:             if (values[i] > max) {
 251:                 max = values[i];
 252:             }
 253:         }
 254:         return max;
 255:     }
 256: 
 257:     /**
 258:      * Returns the bins for a series.
 259:      * 
 260:      * @param series  the series index (in the range <code>0</code> to 
 261:      *     <code>getSeriesCount() - 1</code>).
 262:      * 
 263:      * @return A list of bins.
 264:      * 
 265:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 266:      *     specified range.
 267:      */
 268:     List getBins(int series) {
 269:         Map map = (Map) this.list.get(series);
 270:         return (List) map.get("bins"); 
 271:     }
 272: 
 273:     /**
 274:      * Returns the total number of observations for a series.
 275:      * 
 276:      * @param series  the series index.
 277:      * 
 278:      * @return The total.
 279:      */
 280:     private int getTotal(int series) {
 281:         Map map = (Map) this.list.get(series);
 282:         return ((Integer) map.get("values.length")).intValue(); 
 283:     }
 284: 
 285:     /**
 286:      * Returns the bin width for a series.
 287:      * 
 288:      * @param series  the series index (zero based).
 289:      * 
 290:      * @return The bin width.
 291:      */
 292:     private double getBinWidth(int series) {
 293:         Map map = (Map) this.list.get(series);
 294:         return ((Double) map.get("bin width")).doubleValue(); 
 295:     }
 296: 
 297:     /**
 298:      * Returns the number of series in the dataset.
 299:      * 
 300:      * @return The series count.
 301:      */
 302:     public int getSeriesCount() { 
 303:         return this.list.size(); 
 304:     }
 305:     
 306:     /**
 307:      * Returns the key for a series.
 308:      * 
 309:      * @param series  the series index (in the range <code>0</code> to 
 310:      *     <code>getSeriesCount() - 1</code>).
 311:      * 
 312:      * @return The series key.
 313:      * 
 314:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 315:      *     specified range.
 316:      */
 317:     public Comparable getSeriesKey(int series) {
 318:         Map map = (Map) this.list.get(series);
 319:         return (Comparable) map.get("key"); 
 320:     }
 321: 
 322:     /**
 323:      * Returns the number of data items for a series.
 324:      * 
 325:      * @param series  the series index (in the range <code>0</code> to 
 326:      *     <code>getSeriesCount() - 1</code>).
 327:      * 
 328:      * @return The item count.
 329:      * 
 330:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 331:      *     specified range.
 332:      */
 333:     public int getItemCount(int series) {
 334:         return getBins(series).size(); 
 335:     }
 336: 
 337:     /**
 338:      * Returns the X value for a bin.  This value won't be used for plotting 
 339:      * histograms, since the renderer will ignore it.  But other renderers can 
 340:      * use it (for example, you could use the dataset to create a line
 341:      * chart).
 342:      * 
 343:      * @param series  the series index (in the range <code>0</code> to 
 344:      *     <code>getSeriesCount() - 1</code>).
 345:      * @param item  the item index (zero based).
 346:      * 
 347:      * @return The start value.
 348:      * 
 349:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 350:      *     specified range.
 351:      */
 352:     public Number getX(int series, int item) {
 353:         List bins = getBins(series);
 354:         HistogramBin bin = (HistogramBin) bins.get(item);
 355:         double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
 356:         return new Double(x);
 357:     }
 358: 
 359:     /**
 360:      * Returns the y-value for a bin (calculated to take into account the 
 361:      * histogram type).
 362:      * 
 363:      * @param series  the series index (in the range <code>0</code> to 
 364:      *     <code>getSeriesCount() - 1</code>).
 365:      * @param item  the item index (zero based).
 366:      * 
 367:      * @return The y-value.
 368:      * 
 369:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 370:      *     specified range.
 371:      */
 372:     public Number getY(int series, int item) {
 373:         List bins = getBins(series);
 374:         HistogramBin bin = (HistogramBin) bins.get(item);
 375:         double total = getTotal(series);
 376:         double binWidth = getBinWidth(series);
 377: 
 378:         if (this.type == HistogramType.FREQUENCY) {
 379:             return new Double(bin.getCount());
 380:         }
 381:         else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
 382:             return new Double(bin.getCount() / total);
 383:         }
 384:         else if (this.type == HistogramType.SCALE_AREA_TO_1) {
 385:             return new Double(bin.getCount() / (binWidth * total));
 386:         }
 387:         else { // pretty sure this shouldn't ever happen
 388:             throw new IllegalStateException();
 389:         }
 390:     }
 391: 
 392:     /**
 393:      * Returns the start value for a bin.
 394:      * 
 395:      * @param series  the series index (in the range <code>0</code> to 
 396:      *     <code>getSeriesCount() - 1</code>).
 397:      * @param item  the item index (zero based).
 398:      * 
 399:      * @return The start value.
 400:      * 
 401:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 402:      *     specified range.
 403:      */
 404:     public Number getStartX(int series, int item) {
 405:         List bins = getBins(series);
 406:         HistogramBin bin = (HistogramBin) bins.get(item);
 407:         return new Double(bin.getStartBoundary());
 408:     }
 409: 
 410:     /**
 411:      * Returns the end value for a bin.
 412:      * 
 413:      * @param series  the series index (in the range <code>0</code> to 
 414:      *     <code>getSeriesCount() - 1</code>).
 415:      * @param item  the item index (zero based).
 416:      * 
 417:      * @return The end value.
 418:      * 
 419:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 420:      *     specified range.
 421:      */
 422:     public Number getEndX(int series, int item) {
 423:         List bins = getBins(series);
 424:         HistogramBin bin = (HistogramBin) bins.get(item);
 425:         return new Double(bin.getEndBoundary());
 426:     }
 427: 
 428:     /**
 429:      * Returns the start y-value for a bin (which is the same as the y-value, 
 430:      * this method exists only to support the general form of the 
 431:      * {@link IntervalXYDataset} interface).
 432:      * 
 433:      * @param series  the series index (in the range <code>0</code> to 
 434:      *     <code>getSeriesCount() - 1</code>).
 435:      * @param item  the item index (zero based).
 436:      * 
 437:      * @return The y-value.
 438:      * 
 439:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 440:      *     specified range.
 441:      */
 442:     public Number getStartY(int series, int item) {
 443:         return getY(series, item);
 444:     }
 445: 
 446:     /**
 447:      * Returns the end y-value for a bin (which is the same as the y-value, 
 448:      * this method exists only to support the general form of the 
 449:      * {@link IntervalXYDataset} interface).
 450:      * 
 451:      * @param series  the series index (in the range <code>0</code> to 
 452:      *     <code>getSeriesCount() - 1</code>).
 453:      * @param item  the item index (zero based).
 454:      * 
 455:      * @return The Y value.
 456:      * 
 457:      * @throws IndexOutOfBoundsException if <code>series</code> is outside the
 458:      *     specified range.
 459:      */    
 460:     public Number getEndY(int series, int item) {
 461:         return getY(series, item);
 462:     }
 463: 
 464:     /**
 465:      * Tests this dataset for equality with an arbitrary object.
 466:      * 
 467:      * @param obj  the object to test against (<code>null</code> permitted).
 468:      * 
 469:      * @return A boolean.
 470:      */
 471:     public boolean equals(Object obj) {
 472:         if (obj == this) {
 473:             return true;   
 474:         }
 475:         if (!(obj instanceof HistogramDataset)) {
 476:             return false;
 477:         }
 478:         HistogramDataset that = (HistogramDataset) obj;
 479:         if (!ObjectUtilities.equal(this.type, that.type)) {
 480:             return false;
 481:         }
 482:         if (!ObjectUtilities.equal(this.list, that.list)) {
 483:             return false;
 484:         }
 485:         return true;   
 486:     }
 487: 
 488:     /**
 489:      * Returns a clone of the dataset.
 490:      * 
 491:      * @return A clone of the dataset.
 492:      * 
 493:      * @throws CloneNotSupportedException if the object cannot be cloned.
 494:      */
 495:     public Object clone() throws CloneNotSupportedException {
 496:         return super.clone();   
 497:     }
 498: 
 499: }