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, Cloneable, PublicCloneable,
081: Serializable {
082:
083: /** For serialization. */
084: private static final long serialVersionUID = -6341668077370231153L;
085:
086: /** A list of maps. */
087: private List list;
088:
089: /** The histogram type. */
090: private HistogramType type;
091:
092: /**
093: * Creates a new (empty) dataset with a default type of
094: * {@link HistogramType}.FREQUENCY.
095: */
096: public HistogramDataset() {
097: this .list = new ArrayList();
098: this .type = HistogramType.FREQUENCY;
099: }
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, double[] values, int bins,
151: double minimum, double maximum) {
152:
153: if (key == null) {
154: throw new IllegalArgumentException("Null 'key' argument.");
155: }
156: if (values == null) {
157: throw new IllegalArgumentException(
158: "Null 'values' argument.");
159: } else if (bins < 1) {
160: throw new IllegalArgumentException(
161: "The 'bins' value must be at least 1.");
162: }
163: double binWidth = (maximum - minimum) / bins;
164:
165: double lower = minimum;
166: double upper;
167: List binList = new ArrayList(bins);
168: for (int i = 0; i < bins; i++) {
169: HistogramBin bin;
170: // make sure bins[bins.length]'s upper boundary ends at maximum
171: // to avoid the rounding issue. the bins[0] lower boundary is
172: // guaranteed start from min
173: if (i == bins - 1) {
174: bin = new HistogramBin(lower, maximum);
175: } else {
176: upper = minimum + (i + 1) * binWidth;
177: bin = new HistogramBin(lower, upper);
178: lower = upper;
179: }
180: binList.add(bin);
181: }
182: // fill the bins
183: for (int i = 0; i < values.length; i++) {
184: int binIndex = bins - 1;
185: if (values[i] < maximum) {
186: double fraction = (values[i] - minimum)
187: / (maximum - minimum);
188: if (fraction < 0.0) {
189: fraction = 0.0;
190: }
191: binIndex = (int) (fraction * bins);
192: // rounding could result in binIndex being equal to bins
193: // which will cause an IndexOutOfBoundsException - see bug
194: // report 1553088
195: if (binIndex >= bins) {
196: binIndex = bins - 1;
197: }
198: }
199: HistogramBin bin = (HistogramBin) binList.get(binIndex);
200: bin.incrementCount();
201: }
202: // generic map for each series
203: Map map = new HashMap();
204: map.put("key", key);
205: map.put("bins", binList);
206: map.put("values.length", new Integer(values.length));
207: map.put("bin width", new Double(binWidth));
208: this .list.add(map);
209: }
210:
211: /**
212: * Returns the minimum value in an array of values.
213: *
214: * @param values the values (<code>null</code> not permitted and
215: * zero-length array not permitted).
216: *
217: * @return The minimum value.
218: */
219: private double getMinimum(double[] values) {
220: if (values == null || values.length < 1) {
221: throw new IllegalArgumentException(
222: "Null or zero length 'values' argument.");
223: }
224: double min = Double.MAX_VALUE;
225: for (int i = 0; i < values.length; i++) {
226: if (values[i] < min) {
227: min = values[i];
228: }
229: }
230: return min;
231: }
232:
233: /**
234: * Returns the maximum value in an array of values.
235: *
236: * @param values the values (<code>null</code> not permitted and
237: * zero-length array not permitted).
238: *
239: * @return The maximum value.
240: */
241: private double getMaximum(double[] values) {
242: if (values == null || values.length < 1) {
243: throw new IllegalArgumentException(
244: "Null or zero length 'values' argument.");
245: }
246: double max = -Double.MAX_VALUE;
247: for (int i = 0; i < values.length; i++) {
248: if (values[i] > max) {
249: max = values[i];
250: }
251: }
252: return max;
253: }
254:
255: /**
256: * Returns the bins for a series.
257: *
258: * @param series the series index (in the range <code>0</code> to
259: * <code>getSeriesCount() - 1</code>).
260: *
261: * @return A list of bins.
262: *
263: * @throws IndexOutOfBoundsException if <code>series</code> is outside the
264: * specified range.
265: */
266: List getBins(int series) {
267: Map map = (Map) this .list.get(series);
268: return (List) map.get("bins");
269: }
270:
271: /**
272: * Returns the total number of observations for a series.
273: *
274: * @param series the series index.
275: *
276: * @return The total.
277: */
278: private int getTotal(int series) {
279: Map map = (Map) this .list.get(series);
280: return ((Integer) map.get("values.length")).intValue();
281: }
282:
283: /**
284: * Returns the bin width for a series.
285: *
286: * @param series the series index (zero based).
287: *
288: * @return The bin width.
289: */
290: private double getBinWidth(int series) {
291: Map map = (Map) this .list.get(series);
292: return ((Double) map.get("bin width")).doubleValue();
293: }
294:
295: /**
296: * Returns the number of series in the dataset.
297: *
298: * @return The series count.
299: */
300: public int getSeriesCount() {
301: return this .list.size();
302: }
303:
304: /**
305: * Returns the key for a series.
306: *
307: * @param series the series index (in the range <code>0</code> to
308: * <code>getSeriesCount() - 1</code>).
309: *
310: * @return The series key.
311: *
312: * @throws IndexOutOfBoundsException if <code>series</code> is outside the
313: * specified range.
314: */
315: public Comparable getSeriesKey(int series) {
316: Map map = (Map) this .list.get(series);
317: return (Comparable) map.get("key");
318: }
319:
320: /**
321: * Returns the number of data items for a series.
322: *
323: * @param series the series index (in the range <code>0</code> to
324: * <code>getSeriesCount() - 1</code>).
325: *
326: * @return The item count.
327: *
328: * @throws IndexOutOfBoundsException if <code>series</code> is outside the
329: * specified range.
330: */
331: public int getItemCount(int series) {
332: return getBins(series).size();
333: }
334:
335: /**
336: * Returns the X value for a bin. This value won't be used for plotting
337: * histograms, since the renderer will ignore it. But other renderers can
338: * use it (for example, you could use the dataset to create a line
339: * chart).
340: *
341: * @param series the series index (in the range <code>0</code> to
342: * <code>getSeriesCount() - 1</code>).
343: * @param item the item index (zero based).
344: *
345: * @return The start value.
346: *
347: * @throws IndexOutOfBoundsException if <code>series</code> is outside the
348: * specified range.
349: */
350: public Number getX(int series, int item) {
351: List bins = getBins(series);
352: HistogramBin bin = (HistogramBin) bins.get(item);
353: double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
354: return new Double(x);
355: }
356:
357: /**
358: * Returns the y-value for a bin (calculated to take into account the
359: * histogram type).
360: *
361: * @param series the series index (in the range <code>0</code> to
362: * <code>getSeriesCount() - 1</code>).
363: * @param item the item index (zero based).
364: *
365: * @return The y-value.
366: *
367: * @throws IndexOutOfBoundsException if <code>series</code> is outside the
368: * specified range.
369: */
370: public Number getY(int series, int item) {
371: List bins = getBins(series);
372: HistogramBin bin = (HistogramBin) bins.get(item);
373: double total = getTotal(series);
374: double binWidth = getBinWidth(series);
375:
376: if (this .type == HistogramType.FREQUENCY) {
377: return new Double(bin.getCount());
378: } else if (this .type == HistogramType.RELATIVE_FREQUENCY) {
379: return new Double(bin.getCount() / total);
380: } else if (this .type == HistogramType.SCALE_AREA_TO_1) {
381: return new Double(bin.getCount() / (binWidth * total));
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: }
|