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-2008, 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-2008, 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:  * 22-May-2008 : Implemented clone() method override (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, 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:         HistogramDataset clone = (HistogramDataset) super.clone();
 497:         int seriesCount = getSeriesCount();
 498:         clone.list = new java.util.ArrayList(seriesCount);
 499:         for (int i = 0; i < seriesCount; i++) {
 500:             clone.list.add(new HashMap((Map) this.list.get(i)));
 501:         }
 502:         return clone;
 503:     }
 504: 
 505: }