001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, 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-2008, 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 * Changes
038 * -------
039 * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG);
040 * 07-Jul-2003 : Changed package and added Javadocs (DG);
041 * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW);
042 * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG);
043 * 01-Mar-2004 : Added equals() and clone() methods and implemented
044 * Serializable. Also added new addSeries() method (DG);
045 * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG);
046 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
047 * getYValue() (DG);
048 * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron
049 * Hayne (DG);
050 * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG);
051 * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG);
052 * ------------- JFREECHART 1.0.x ---------------------------------------------
053 * 03-Aug-2006 : Improved precision of bin boundary calculation (DG);
054 * 07-Sep-2006 : Fixed bug 1553088 (DG);
055 * 22-May-2008 : Implemented clone() method override (DG);
056 *
057 */
058
059 package org.jfree.data.statistics;
060
061 import java.io.Serializable;
062 import java.util.ArrayList;
063 import java.util.HashMap;
064 import java.util.List;
065 import java.util.Map;
066
067 import org.jfree.data.general.DatasetChangeEvent;
068 import org.jfree.data.xy.AbstractIntervalXYDataset;
069 import org.jfree.data.xy.IntervalXYDataset;
070 import org.jfree.util.ObjectUtilities;
071 import org.jfree.util.PublicCloneable;
072
073 /**
074 * A dataset that can be used for creating histograms.
075 *
076 * @see SimpleHistogramDataset
077 */
078 public class HistogramDataset extends AbstractIntervalXYDataset
079 implements IntervalXYDataset, Cloneable, PublicCloneable,
080 Serializable {
081
082 /** For serialization. */
083 private static final long serialVersionUID = -6341668077370231153L;
084
085 /** A list of maps. */
086 private List list;
087
088 /** The histogram type. */
089 private HistogramType type;
090
091 /**
092 * Creates a new (empty) dataset with a default type of
093 * {@link HistogramType}.FREQUENCY.
094 */
095 public HistogramDataset() {
096 this.list = new ArrayList();
097 this.type = HistogramType.FREQUENCY;
098 }
099
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 }