comparison gnv-artifacts/src/main/java/de/intevation/gnv/chart/AdvancedHistogramDataset.java @ 1055:bb2679624c6a

Implemented a new histogram dataset that takes the width of a single bin as well as the number of bins for the histogram (issue288). gnv-artifacts/trunk@1130 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Ingo Weinzierl <ingo.weinzierl@intevation.de>
date Thu, 27 May 2010 07:41:14 +0000
parents
children 7e085bfb107b
comparison
equal deleted inserted replaced
1054:8430269ec73b 1055:bb2679624c6a
1 package de.intevation.gnv.chart;
2
3 import java.io.Serializable;
4 import java.util.ArrayList;
5 import java.util.HashMap;
6 import java.util.List;
7 import java.util.Map;
8
9 import org.apache.log4j.Logger;
10
11 import org.jfree.data.general.DatasetChangeEvent;
12 import org.jfree.data.xy.AbstractIntervalXYDataset;
13 import org.jfree.data.xy.IntervalXYDataset;
14
15 import org.jfree.data.statistics.HistogramType;
16 import org.jfree.data.statistics.HistogramBin;
17
18 import org.jfree.util.ObjectUtilities;
19 import org.jfree.util.PublicCloneable;
20
21 /**
22 * This class extends the functionality of the internal JFreeChart class
23 * <code>HistogramDataset</code>. <code>AdvancedHistogramDataset</code> takes
24 * the number of bins OR a bin width. It is mainly a copy of this class. The
25 * reason why there is no inheritance from JFreeChart's internal class is,
26 * that basic attributes have private access there.
27 *
28 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
29 */
30 public class AdvancedHistogramDataset
31 extends AbstractIntervalXYDataset
32 implements IntervalXYDataset, Cloneable, PublicCloneable, Serializable
33 {
34 /** A list of maps. */
35 protected List list;
36
37 /** The histogram type. */
38 protected HistogramType type;
39
40 /** The width of a single bin. */
41 protected double binWidth = -1;
42
43 /** The number of bins. */
44 protected int bins = -1;
45
46 /** The logger */
47 private static Logger logger =
48 Logger.getLogger(AdvancedHistogramDataset.class);
49
50
51 /**
52 * Creates a new (empty) dataset with a default type of
53 * {@link HistogramType}.FREQUENCY.
54 */
55 public AdvancedHistogramDataset() {
56 this.list = new ArrayList();
57 this.type = HistogramType.FREQUENCY;
58 }
59
60 /**
61 * Creates a new (empty) dataset with a default type of
62 * {@link HistogramType}.FREQUENCY.
63 */
64 public AdvancedHistogramDataset(int bins, double binWidth) {
65 this.list = new ArrayList();
66 this.type = HistogramType.FREQUENCY;
67 this.bins = bins;
68 this.binWidth = binWidth;
69 }
70
71 /**
72 * Returns the histogram type.
73 *
74 * @return The type (never <code>null</code>).
75 */
76 public HistogramType getType() {
77 return this.type;
78 }
79
80 /**
81 * Sets the histogram type and sends a {@link DatasetChangeEvent} to all
82 * registered listeners.
83 *
84 * @param type the type (<code>null</code> not permitted).
85 */
86 public void setType(HistogramType type) {
87 if (type == null) {
88 throw new IllegalArgumentException("Null 'type' argument");
89 }
90 this.type = type;
91 notifyListeners(new DatasetChangeEvent(this, this));
92 }
93
94 /**
95 * Adds a series to the dataset, using the specified number of bins.
96 *
97 * @param key the series key (<code>null</code> not permitted).
98 * @param values the values (<code>null</code> not permitted).
99 */
100 public void addSeries(Comparable key, double[] values) {
101 double minimum = getMinimum(values);
102 double maximum = getMaximum(values);
103 addSeries(key, values, minimum, maximum);
104 }
105
106 /**
107 * Adds a series to the dataset. Any data value less than minimum will be
108 * assigned to the first bin, and any data value greater than maximum will
109 * be assigned to the last bin. Values falling on the boundary of
110 * adjacent bins will be assigned to the higher indexed bin.
111 *
112 * @param key the series key (<code>null</code> not permitted).
113 * @param values the raw observations.
114 * @param bins the number of bins (must be at least 1).
115 * @param minimum the lower bound of the bin range.
116 * @param maximum the upper bound of the bin range.
117 */
118 public void addSeries(Comparable key,
119 double[] values,
120 double minimum,
121 double maximum) {
122
123 if (key == null) {
124 throw new IllegalArgumentException("Null 'key' argument.");
125 }
126 if (values == null) {
127 throw new IllegalArgumentException("Null 'values' argument.");
128 }
129 if (bins <= 0 && binWidth <= 0) {
130 throw new IllegalArgumentException(
131 "We need at least a bin width or the number of bins.");
132 }
133
134 // There is a binWidth given to calculate the number of bins in this
135 // case
136 if (bins <= 0) {
137 double tmp = (maximum - minimum) / binWidth;
138 bins = (int) tmp;
139 bins = tmp % 1 > 0 ? bins + 1 : bins;
140
141 double overlap = minimum + bins * binWidth - maximum;
142 tmp = minimum;
143 minimum -= overlap / 2;
144
145 logger.debug("There is an overlap of " + overlap);
146 logger.info("The lower bound is moved left from "
147 + tmp + " to " + minimum);
148
149 tmp = maximum;
150 maximum += overlap / 2;
151 logger.info("The upper bound is moved right from "
152 + tmp + " to " + maximum);
153 }
154
155 if (bins <= 0) {
156 throw new IllegalArgumentException(
157 "The 'bins' value must be at least 1.");
158 }
159
160 // in this case, there is a number of bins given, so we need to
161 // calculate the width of a single bin
162 if (binWidth <= 0)
163 binWidth = (maximum - minimum) / bins;
164
165 logger.info("bin width: " + binWidth);
166 logger.info("number of bins: " + bins);
167
168 double lower = minimum;
169 double upper;
170 List binList = new ArrayList(bins);
171 for (int i = 0; i < bins; i++) {
172 HistogramBin bin;
173 // make sure bins[bins.length]'s upper boundary ends at maximum
174 // to avoid the rounding issue. the bins[0] lower boundary is
175 // guaranteed start from min
176 if (i == bins - 1) {
177 bin = new HistogramBin(lower, maximum);
178 }
179 else {
180 upper = minimum + (i + 1) * binWidth;
181 bin = new HistogramBin(lower, upper);
182 lower = upper;
183 }
184 binList.add(bin);
185 }
186 // fill the bins
187 for (int i = 0; i < values.length; i++) {
188 int binIndex = bins - 1;
189 if (values[i] < maximum) {
190 double fraction = (values[i] - minimum) / (maximum - minimum);
191 if (fraction < 0.0) {
192 fraction = 0.0;
193 }
194 binIndex = (int) (fraction * bins);
195 // rounding could result in binIndex being equal to bins
196 // which will cause an IndexOutOfBoundsException - see bug
197 // report 1553088
198 if (binIndex >= bins) {
199 binIndex = bins - 1;
200 }
201 }
202 HistogramBin bin = (HistogramBin) binList.get(binIndex);
203 bin.incrementCount();
204 }
205 // generic map for each series
206 Map map = new HashMap();
207 map.put("key", key);
208 map.put("bins", binList);
209 map.put("values.length", new Integer(values.length));
210 map.put("bin width", new Double(binWidth));
211 this.list.add(map);
212 }
213
214 /**
215 * Returns the minimum value in an array of values.
216 *
217 * @param values the values (<code>null</code> not permitted and
218 * zero-length array not permitted).
219 *
220 * @return The minimum value.
221 */
222 private double getMinimum(double[] values) {
223 if (values == null || values.length < 1) {
224 throw new IllegalArgumentException(
225 "Null or zero length 'values' argument.");
226 }
227 double min = Double.MAX_VALUE;
228 for (int i = 0; i < values.length; i++) {
229 if (values[i] < min) {
230 min = values[i];
231 }
232 }
233 return min;
234 }
235
236 /**
237 * Returns the maximum value in an array of values.
238 *
239 * @param values the values (<code>null</code> not permitted and
240 * zero-length array not permitted).
241 *
242 * @return The maximum value.
243 */
244 private double getMaximum(double[] values) {
245 if (values == null || values.length < 1) {
246 throw new IllegalArgumentException(
247 "Null or zero length 'values' argument.");
248 }
249 double max = -Double.MAX_VALUE;
250 for (int i = 0; i < values.length; i++) {
251 if (values[i] > max) {
252 max = values[i];
253 }
254 }
255 return max;
256 }
257
258 /**
259 * Returns the bins for a series.
260 *
261 * @param series the series index (in the range <code>0</code> to
262 * <code>getSeriesCount() - 1</code>).
263 *
264 * @return A list of bins.
265 *
266 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
267 * specified range.
268 */
269 List getBins(int series) {
270 Map map = (Map) this.list.get(series);
271 return (List) map.get("bins");
272 }
273
274 /**
275 * Returns the total number of observations for a series.
276 *
277 * @param series the series index.
278 *
279 * @return The total.
280 */
281 private int getTotal(int series) {
282 Map map = (Map) this.list.get(series);
283 return ((Integer) map.get("values.length")).intValue();
284 }
285
286 /**
287 * Returns the bin width for a series.
288 *
289 * @param series the series index (zero based).
290 *
291 * @return The bin width.
292 */
293 private double getBinWidth(int series) {
294 if (binWidth > 0)
295 return binWidth;
296
297 Map map = (Map) this.list.get(series);
298 return ((Double) map.get("bin width")).doubleValue();
299 }
300
301 /**
302 * Returns the number of series in the dataset.
303 *
304 * @return The series count.
305 */
306 public int getSeriesCount() {
307 return this.list.size();
308 }
309
310 /**
311 * Returns the key for a series.
312 *
313 * @param series the series index (in the range <code>0</code> to
314 * <code>getSeriesCount() - 1</code>).
315 *
316 * @return The series key.
317 *
318 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
319 * specified range.
320 */
321 public Comparable getSeriesKey(int series) {
322 Map map = (Map) this.list.get(series);
323 return (Comparable) map.get("key");
324 }
325
326 /**
327 * Returns the number of data items for a series.
328 *
329 * @param series the series index (in the range <code>0</code> to
330 * <code>getSeriesCount() - 1</code>).
331 *
332 * @return The item count.
333 *
334 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
335 * specified range.
336 */
337 public int getItemCount(int series) {
338 return getBins(series).size();
339 }
340
341 /**
342 * Returns the X value for a bin. This value won't be used for plotting
343 * histograms, since the renderer will ignore it. But other renderers can
344 * use it (for example, you could use the dataset to create a line
345 * chart).
346 *
347 * @param series the series index (in the range <code>0</code> to
348 * <code>getSeriesCount() - 1</code>).
349 * @param item the item index (zero based).
350 *
351 * @return The start value.
352 *
353 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
354 * specified range.
355 */
356 public Number getX(int series, int item) {
357 List bins = getBins(series);
358 HistogramBin bin = (HistogramBin) bins.get(item);
359 double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
360 return new Double(x);
361 }
362
363 /**
364 * Returns the y-value for a bin (calculated to take into account the
365 * histogram type).
366 *
367 * @param series the series index (in the range <code>0</code> to
368 * <code>getSeriesCount() - 1</code>).
369 * @param item the item index (zero based).
370 *
371 * @return The y-value.
372 *
373 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
374 * specified range.
375 */
376 public Number getY(int series, int item) {
377 List bins = getBins(series);
378 HistogramBin bin = (HistogramBin) bins.get(item);
379 double total = getTotal(series);
380 double binWidth = getBinWidth(series);
381
382 if (this.type == HistogramType.FREQUENCY) {
383 return new Double(bin.getCount());
384 }
385 else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
386 return new Double(bin.getCount() / total);
387 }
388 else if (this.type == HistogramType.SCALE_AREA_TO_1) {
389 return new Double(bin.getCount() / (binWidth * total));
390 }
391 else { // pretty sure this shouldn't ever happen
392 throw new IllegalStateException();
393 }
394 }
395
396 /**
397 * Returns the start value for a bin.
398 *
399 * @param series the series index (in the range <code>0</code> to
400 * <code>getSeriesCount() - 1</code>).
401 * @param item the item index (zero based).
402 *
403 * @return The start value.
404 *
405 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
406 * specified range.
407 */
408 public Number getStartX(int series, int item) {
409 List bins = getBins(series);
410 HistogramBin bin = (HistogramBin) bins.get(item);
411 return new Double(bin.getStartBoundary());
412 }
413
414 /**
415 * Returns the end value for a bin.
416 *
417 * @param series the series index (in the range <code>0</code> to
418 * <code>getSeriesCount() - 1</code>).
419 * @param item the item index (zero based).
420 *
421 * @return The end value.
422 *
423 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
424 * specified range.
425 */
426 public Number getEndX(int series, int item) {
427 List bins = getBins(series);
428 HistogramBin bin = (HistogramBin) bins.get(item);
429 return new Double(bin.getEndBoundary());
430 }
431
432 /**
433 * Returns the start y-value for a bin (which is the same as the y-value,
434 * this method exists only to support the general form of the
435 * {@link IntervalXYDataset} interface).
436 *
437 * @param series the series index (in the range <code>0</code> to
438 * <code>getSeriesCount() - 1</code>).
439 * @param item the item index (zero based).
440 *
441 * @return The y-value.
442 *
443 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
444 * specified range.
445 */
446 public Number getStartY(int series, int item) {
447 return getY(series, item);
448 }
449
450 /**
451 * Returns the end y-value for a bin (which is the same as the y-value,
452 * this method exists only to support the general form of the
453 * {@link IntervalXYDataset} interface).
454 *
455 * @param series the series index (in the range <code>0</code> to
456 * <code>getSeriesCount() - 1</code>).
457 * @param item the item index (zero based).
458 *
459 * @return The Y value.
460 *
461 * @throws IndexOutOfBoundsException if <code>series</code> is outside the
462 * specified range.
463 */
464 public Number getEndY(int series, int item) {
465 return getY(series, item);
466 }
467
468 /**
469 * Tests this dataset for equality with an arbitrary object.
470 *
471 * @param obj the object to test against (<code>null</code> permitted).
472 *
473 * @return A boolean.
474 */
475 public boolean equals(Object obj) {
476 if (obj == this) {
477 return true;
478 }
479 if (!(obj instanceof AdvancedHistogramDataset)) {
480 return false;
481 }
482 AdvancedHistogramDataset that = (AdvancedHistogramDataset) obj;
483 if (!ObjectUtilities.equal(this.type, that.type)) {
484 return false;
485 }
486 if (!ObjectUtilities.equal(this.list, that.list)) {
487 return false;
488 }
489 return true;
490 }
491
492 /**
493 * Returns a clone of the dataset.
494 *
495 * @return A clone of the dataset.
496 *
497 * @throws CloneNotSupportedException if the object cannot be cloned.
498 */
499 public Object clone() throws CloneNotSupportedException {
500 AdvancedHistogramDataset clone = (AdvancedHistogramDataset) super.clone();
501 int seriesCount = getSeriesCount();
502 clone.list = new java.util.ArrayList(seriesCount);
503 for (int i = 0; i < seriesCount; i++) {
504 clone.list.add(new HashMap((Map) this.list.get(i)));
505 }
506 return clone;
507 }
508 }
509 // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :

http://dive4elements.wald.intevation.org