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