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,
081 Cloneable, PublicCloneable,
082 Serializable {
083
084 /** For serialization. */
085 private static final long serialVersionUID = -6341668077370231153L;
086
087 /** A list of maps. */
088 private List list;
089
090 /** The histogram type. */
091 private HistogramType type;
092
093 /**
094 * Creates a new (empty) dataset with a default type of
095 * {@link HistogramType}.FREQUENCY.
096 */
097 public HistogramDataset() {
098 this.list = new ArrayList();
099 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 }