Mercurial > dive4elements > gnv-client
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 : |