Mercurial > dive4elements > river
comparison flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java @ 1940:0d12e70766c8
Refactored XYChartGenerator to have better working multi-axes features.
flys-artifacts/trunk@3321 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author | Felix Wolfsteller <felix.wolfsteller@intevation.de> |
---|---|
date | Mon, 28 Nov 2011 14:36:56 +0000 |
parents | 5b51f5232661 |
children | 06d8d371d244 |
comparison
equal
deleted
inserted
replaced
1939:2730d17df021 | 1940:0d12e70766c8 |
---|---|
47 import de.intevation.flys.utils.ThemeAccess; | 47 import de.intevation.flys.utils.ThemeAccess; |
48 | 48 |
49 /** | 49 /** |
50 * An abstract base class for creating XY charts. | 50 * An abstract base class for creating XY charts. |
51 * | 51 * |
52 * With respect to datasets, ranges and axis, there are following requirements: | |
53 * <ul> | |
54 * <li> First in, first drawn: "Early" datasets should be of lower Z-Oder | |
55 * than later ones (only works per-axis). </li> | |
56 * <li> Visible axis should initially show the range of all datasets that | |
57 * show data for this axis (even invisible ones). Motivation: Once | |
58 * a dataset (theme) has been activated, it should be on screen. </li> | |
59 * <li> There should always be a Y-Axis on the "left". </li> | |
60 * </ul> | |
61 * | |
52 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a> | 62 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a> |
53 */ | 63 */ |
54 public abstract class XYChartGenerator extends ChartGenerator { | 64 public abstract class XYChartGenerator extends ChartGenerator { |
55 | 65 |
66 private class AxisDataset { | |
67 /** Symbolic integer, but also coding the priority (0 goes first). */ | |
68 protected int axisSymbol; | |
69 /** List of assigned datasets (in order). */ | |
70 protected List<XYDataset> datasets; | |
71 /** Range to use to include all given datasets. */ | |
72 protected Range range; | |
73 | |
74 /** Create AxisDataset. */ | |
75 public AxisDataset(int symb) { | |
76 this.axisSymbol = symb; | |
77 datasets = new ArrayList<XYDataset>(); | |
78 } | |
79 | |
80 /** Merge (or create given range with range so far (if any). */ | |
81 private void mergeRanges(Range subRange) { | |
82 if (range == null) { | |
83 range = subRange; | |
84 return; | |
85 } | |
86 range = Range.combine(range, subRange); | |
87 } | |
88 | |
89 /** Add a dataset, include its range. */ | |
90 public void addDataset(XYSeries dataset) { | |
91 this.datasets.add(new XYSeriesCollection(dataset)); | |
92 includeRange(dataset); | |
93 } | |
94 | |
95 /** Adjust range to include given dataset. */ | |
96 public void includeRange(XYSeries dataset) { | |
97 mergeRanges(new Range(dataset.getMinY(), dataset.getMaxY())); | |
98 } | |
99 | |
100 /** True if no datasets given. */ | |
101 public boolean isEmpty() { | |
102 return this.datasets.isEmpty(); | |
103 } | |
104 | |
105 } | |
106 | |
107 | |
56 /** The logger that is used in this generator. */ | 108 /** The logger that is used in this generator. */ |
57 private static Logger logger = Logger.getLogger(XYChartGenerator.class); | 109 private static Logger logger = Logger.getLogger(XYChartGenerator.class); |
58 | 110 |
59 /** Map of datasets ("index"). */ | 111 /** Map of datasets ("index"). */ |
60 protected SortedMap<Integer, List<XYDataset>> datasets; | 112 protected SortedMap<Integer, AxisDataset> datasets; |
61 | 113 |
62 /** List of annotations to insert in plot. */ | 114 /** List of annotations to insert in plot. */ |
63 protected List<FLYSAnnotation> annotations; | 115 protected List<FLYSAnnotation> annotations; |
64 | 116 |
65 /** The max X range to include all X values of all series for each axis. */ | 117 /** The max X range to include all X values of all series for each axis. */ |
73 | 125 |
74 | 126 |
75 public XYChartGenerator() { | 127 public XYChartGenerator() { |
76 xRanges = new HashMap<Integer, Range>(); | 128 xRanges = new HashMap<Integer, Range>(); |
77 yRanges = new HashMap<Integer, Range>(); | 129 yRanges = new HashMap<Integer, Range>(); |
78 datasets = new TreeMap<Integer, List<XYDataset>>(); | 130 datasets = new TreeMap<Integer, AxisDataset>(); |
79 } | 131 } |
80 | 132 |
81 | 133 |
82 /** | 134 /** |
83 * Returns the title of a chart. | 135 * Returns the title of a chart. |
84 * | 136 * |
85 * @return the title of a chart. | 137 * @return the title of a chart. |
86 */ | 138 */ |
87 protected abstract String getChartTitle(); | 139 protected abstract String getChartTitle(); |
88 | 140 |
141 | |
89 /** | 142 /** |
90 * Returns the X-Axis label of a chart. | 143 * Returns the X-Axis label of a chart. |
91 * | 144 * |
92 * @return the X-Axis label of a chart. | 145 * @return the X-Axis label of a chart. |
93 */ | 146 */ |
94 protected abstract String getXAxisLabel(); | 147 protected abstract String getXAxisLabel(); |
95 | 148 |
149 | |
96 /** | 150 /** |
97 * Returns the Y-Axis label of a chart. | 151 * Returns the Y-Axis label of a chart. |
98 * | 152 * |
99 * @return the Y-Axis label of a chart. | 153 * @return the Y-Axis label of a chart. |
100 */ | 154 */ |
101 protected abstract String getYAxisLabel(); | 155 protected abstract String getYAxisLabel(); |
102 | 156 |
103 | 157 /** |
158 * Generate chart. | |
159 */ | |
104 public void generate() | 160 public void generate() |
105 throws IOException | 161 throws IOException |
106 { | 162 { |
107 logger.debug("XYChartGenerator.generate"); | 163 logger.debug("XYChartGenerator.generate"); |
108 | 164 |
166 false); | 222 false); |
167 | 223 |
168 XYPlot plot = (XYPlot) chart.getPlot(); | 224 XYPlot plot = (XYPlot) chart.getPlot(); |
169 chart.setBackgroundPaint(Color.WHITE); | 225 chart.setBackgroundPaint(Color.WHITE); |
170 plot.setBackgroundPaint(Color.WHITE); | 226 plot.setBackgroundPaint(Color.WHITE); |
171 | |
172 addDatasets(plot); | |
173 addAnnotations(plot); | |
174 addSubtitles(chart); | 227 addSubtitles(chart); |
175 adjustPlot(plot); | 228 adjustPlot(plot); |
229 | |
230 //debugAxis(plot); | |
231 | |
232 addDatasets(plot); | |
233 | |
234 //debugDatasets(plot); | |
235 | |
236 recoverEmptyPlot(plot); | |
237 preparePointRanges(plot); | |
238 | |
239 addAnnotationsToRenderer(plot); | |
240 | |
241 //debugAxis(plot); | |
242 | |
176 localizeAxes(plot); | 243 localizeAxes(plot); |
177 | |
178 createAxes(plot); | |
179 adjustAxes(plot); | 244 adjustAxes(plot); |
180 | |
181 recoverEmptyPlot(plot); | |
182 | |
183 preparePointRanges(plot); | |
184 autoZoom(plot); | 245 autoZoom(plot); |
185 | 246 |
186 applyThemes(plot); | |
187 removeEmptyRangeAxes(plot); | |
188 | |
189 return chart; | 247 return chart; |
248 } | |
249 | |
250 | |
251 /** | |
252 * Put debug output about datasets. | |
253 */ | |
254 public void debugDatasets(XYPlot plot) { | |
255 logger.debug("Number of datasets: " + plot.getDatasetCount()); | |
256 for (int i = 0; i < plot.getDatasetCount(); i++) { | |
257 if (plot.getDataset(i) == null) { | |
258 logger.debug("Dataset #" + i + " is null"); | |
259 continue; | |
260 } | |
261 logger.debug("Dataset #" + i + ":" + plot.getDataset(i)); | |
262 } | |
263 } | |
264 | |
265 | |
266 /** | |
267 * Put debug output about axes. | |
268 */ | |
269 public void debugAxis(XYPlot plot) { | |
270 logger.debug("..............."); | |
271 for (int i = 0; i < plot.getRangeAxisCount(); i++) { | |
272 if (plot.getRangeAxis(i) == null) | |
273 logger.debug("Axis #" + i + " == null"); | |
274 else { | |
275 logger.debug("Axis " + i + " != null [" + | |
276 plot.getRangeAxis(i).getRange().getLowerBound() + | |
277 " " + plot.getRangeAxis(i).getRange().getUpperBound() + | |
278 "]"); | |
279 } | |
280 | |
281 } | |
282 logger.debug("..............."); | |
190 } | 283 } |
191 | 284 |
192 | 285 |
193 /** | 286 /** |
194 * Add datasets to plot. | 287 * Add datasets to plot. |
195 * @param plot plot to add datasets to. | 288 * @param plot plot to add datasets to. |
196 */ | 289 */ |
197 protected void addDatasets(XYPlot plot) { | 290 protected void addDatasets(XYPlot plot) { |
198 int count = 0; | 291 // AxisDatasets are sorted, but some might be empty. |
199 for (Map.Entry<Integer, List<XYDataset>> entry: datasets.entrySet()) { | 292 // Thus, generate numbering on the fly. |
200 List<Integer> axisList = new ArrayList<Integer>(1); | 293 int axisIndex = 0; |
201 axisList.add(entry.getKey()); | 294 int datasetIndex = 0; |
202 for (XYDataset dataset: entry.getValue()) { | 295 for (Map.Entry<Integer, AxisDataset> entry: datasets.entrySet()) { |
203 int index = count++; | 296 if (!entry.getValue().isEmpty()) { |
204 plot.setDataset(index, dataset); | 297 // Add axis and range information. |
205 plot.mapDatasetToRangeAxes(index, axisList); | 298 AxisDataset axisDataset = entry.getValue(); |
299 NumberAxis axis = createYAxis(entry.getKey()); | |
300 | |
301 plot.setRangeAxis(axisIndex, axis); | |
302 if (axis.getAutoRangeIncludesZero()) { | |
303 axisDataset.range = Range.expandToInclude(axisDataset.range, 0d); | |
304 } | |
305 yRanges.put(axisIndex, expandPointRange(axisDataset.range)); | |
306 | |
307 // Add contained datasets, mapping to axis. | |
308 for (XYDataset dataset: axisDataset.datasets) { | |
309 plot.setDataset(datasetIndex, dataset); | |
310 plot.mapDatasetToRangeAxis(datasetIndex, axisIndex); | |
311 applyThemes(plot, (XYSeriesCollection) dataset, datasetIndex); | |
312 datasetIndex++; | |
313 } | |
314 axisIndex++; | |
206 } | 315 } |
207 } | 316 } |
208 } | 317 } |
209 | 318 |
210 | 319 |
218 public void addAxisSeries(XYSeries series, int index, boolean visible) { | 327 public void addAxisSeries(XYSeries series, int index, boolean visible) { |
219 if (series == null) { | 328 if (series == null) { |
220 return; | 329 return; |
221 } | 330 } |
222 | 331 |
332 AxisDataset axisDataset = datasets.get(index); | |
333 | |
334 if (axisDataset == null) { | |
335 axisDataset = new AxisDataset(index); | |
336 datasets.put(index, axisDataset); | |
337 } | |
338 | |
223 if (visible) { | 339 if (visible) { |
224 XYSeriesCollection collection = new XYSeriesCollection(series); | 340 axisDataset.addDataset(series); |
225 | 341 } |
226 List<XYDataset> dataset = datasets.get(index); | 342 else { |
227 | 343 // Do this also when not visible to have axis scaled by default such |
228 if (dataset == null) { | 344 // that every data-point could be seen (except for annotations). |
229 dataset = new ArrayList<XYDataset>(); | 345 axisDataset.includeRange(series); |
230 datasets.put(index, dataset); | 346 } |
231 } | 347 |
232 | |
233 dataset.add(collection); | |
234 } | |
235 | |
236 // Do this also when not visible to have axis scaled by default such | |
237 // that every data-point could be seen (except for annotations). | |
238 combineXRanges(new Range(series.getMinX(), series.getMaxX()), 0); | 348 combineXRanges(new Range(series.getMinX(), series.getMaxX()), 0); |
239 combineYRanges(new Range(series.getMinY(), series.getMaxY()), index); | 349 } |
240 } | 350 |
241 | 351 |
242 /** | 352 /** |
243 * Effect: extend range of x axis to include given limits. | 353 * Effect: extend range of x axis to include given limits. |
244 * @param range the given ("minimal") range. | 354 * @param range the given ("minimal") range. |
245 * @param index index of axis to be merged. | 355 * @param index index of axis to be merged. |
255 xRanges.put(index, range); | 365 xRanges.put(index, range); |
256 } | 366 } |
257 | 367 |
258 | 368 |
259 /** | 369 /** |
260 * @param range the new range. | |
261 */ | |
262 private void combineYRanges(Range range, int index) { | |
263 | |
264 Range old = yRanges.get(index); | |
265 | |
266 if (old != null) { | |
267 range = Range.combine(old, range); | |
268 } | |
269 | |
270 yRanges.put(index, range); | |
271 } | |
272 | |
273 | |
274 /** | |
275 * Adds annotations to list (if visible is true). | 370 * Adds annotations to list (if visible is true). |
276 */ | 371 */ |
277 public void addAnnotations(FLYSAnnotation annotation, boolean visible) { | 372 public void addAnnotations(FLYSAnnotation annotation, boolean visible) { |
278 if (!visible) { | 373 if (!visible) { |
279 return; | 374 return; |
286 annotations.add(annotation); | 381 annotations.add(annotation); |
287 } | 382 } |
288 | 383 |
289 | 384 |
290 /** | 385 /** |
291 * Create y-axes, ensure that the first axis (with data) is on the left. | |
292 */ | |
293 public void createAxes(XYPlot plot) { | |
294 logger.debug("XYChartGenerator.createAxes"); | |
295 | |
296 if (datasets.isEmpty()) { | |
297 plot.setRangeAxis(0, createYAxis(0)); | |
298 } | |
299 else { | |
300 Integer last = datasets.lastKey(); | |
301 int i = 0; | |
302 int firstVisible = 0; | |
303 | |
304 if (last != null) { | |
305 firstVisible = i = last; | |
306 for (; i >= 0; --i) { | |
307 if (datasets.containsKey(i)) { | |
308 plot.setRangeAxis(i, createYAxis(i)); | |
309 firstVisible = i; | |
310 } | |
311 } | |
312 plot.setRangeAxisLocation(firstVisible, AxisLocation.TOP_OR_LEFT); | |
313 } | |
314 } | |
315 } | |
316 | |
317 | |
318 /** | |
319 * Create Y (range) axis for given index. | 386 * Create Y (range) axis for given index. |
320 * Shall be overriden by subclasses. | 387 * Shall be overriden by subclasses. |
321 */ | 388 */ |
322 protected NumberAxis createYAxis(int index) { | 389 protected NumberAxis createYAxis(int index) { |
323 NumberAxis axis = new NumberAxis("default axis"); | 390 NumberAxis axis = new NumberAxis(getYAxisLabel()); |
324 return axis; | 391 return axis; |
325 } | 392 } |
393 | |
326 | 394 |
327 /** | 395 /** |
328 * If no data is visible, draw at least empty axis. | 396 * If no data is visible, draw at least empty axis. |
329 */ | 397 */ |
330 private void recoverEmptyPlot(XYPlot plot) { | 398 private void recoverEmptyPlot(XYPlot plot) { |
331 if (plot.getRangeAxis() == null) { | 399 if (plot.getRangeAxis() == null) { |
332 logger.debug("debug: No range axis"); | 400 logger.debug("debug: No range axis"); |
333 plot.setRangeAxis(createYAxis(0)); | 401 plot.setRangeAxis(createYAxis(0)); |
334 } | 402 } |
335 | 403 } |
336 } | 404 |
337 | 405 |
338 /** | 406 /** |
339 * Remove Axes which do not have data on them. | 407 * Expands a given range if it collapses into one point. |
340 */ | 408 */ |
341 private void removeEmptyRangeAxes(XYPlot plot) { | 409 private Range expandPointRange(Range range) { |
342 if (datasets.isEmpty()) { | 410 if (range.getLowerBound() == range.getUpperBound()) { |
343 return; | 411 return expandRange(range, 5); |
344 } | 412 } |
345 Integer last = datasets.lastKey(); | 413 return range; |
346 | 414 } |
347 if (last != null) { | 415 |
348 for (int i = last-1; i >= 0; --i) { | 416 |
349 if (!datasets.containsKey(i)) { | 417 /** |
350 plot.setRangeAxis(i, null); | 418 * Expands X axes if only a point is shown. |
351 } | |
352 } | |
353 } | |
354 } | |
355 | |
356 | |
357 /** | |
358 * Expands X and Y axes if only a point is shown. | |
359 */ | 419 */ |
360 private void preparePointRanges(XYPlot plot) { | 420 private void preparePointRanges(XYPlot plot) { |
361 for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) { | 421 for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) { |
422 logger.debug("Check whether to expand a x axis."); | |
362 Integer key = Integer.valueOf(i); | 423 Integer key = Integer.valueOf(i); |
363 | 424 |
364 Range r = xRanges.get(key); | 425 Range r = xRanges.get(key); |
365 if (r != null && r.getLowerBound() == r.getUpperBound()) { | 426 if (r != null && r.getLowerBound() == r.getUpperBound()) { |
366 xRanges.put(key, expandRange(r, 5)); | 427 xRanges.put(key, expandRange(r, 5)); |
367 } | |
368 } | |
369 | |
370 for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) { | |
371 Integer key = Integer.valueOf(i); | |
372 | |
373 Range r = yRanges.get(key); | |
374 if (r != null && r.getLowerBound() == r.getUpperBound()) { | |
375 yRanges.put(key, expandRange(r, 5)); | |
376 } | 428 } |
377 } | 429 } |
378 } | 430 } |
379 | 431 |
380 | 432 |
479 * | 531 * |
480 * @param index The index of the y-Axis. | 532 * @param index The index of the y-Axis. |
481 * | 533 * |
482 * @return a Range[] as follows: [x-Range, y-Range]. | 534 * @return a Range[] as follows: [x-Range, y-Range]. |
483 */ | 535 */ |
536 // TODO When is this actually called? Is it valid as is? | |
484 public Range[] getRangesForDataset(int index) { | 537 public Range[] getRangesForDataset(int index) { |
538 logger.debug("getRangesForDataset " + index); | |
485 return new Range[] { | 539 return new Range[] { |
486 xRanges.get(Integer.valueOf(0)), | 540 xRanges.get(Integer.valueOf(0)), |
487 yRanges.get(Integer.valueOf(index)) | 541 yRanges.get(Integer.valueOf(index)) |
488 }; | 542 }; |
489 } | 543 } |
490 | 544 |
491 | 545 |
492 protected void addAnnotations(XYPlot plot) { | 546 /** |
547 * Add annotations to Renderer. | |
548 */ | |
549 protected void addAnnotationsToRenderer(XYPlot plot) { | |
493 plot.clearAnnotations(); | 550 plot.clearAnnotations(); |
494 | 551 |
495 if (annotations == null) { | 552 if (annotations == null) { |
496 logger.debug("No Annotations given."); | 553 logger.debug("No Annotations given."); |
497 return; | 554 return; |
498 } | 555 } |
499 | 556 |
500 LegendItemCollection lic = new LegendItemCollection(); | 557 LegendItemCollection lic = new LegendItemCollection(); |
501 | 558 LegendItemCollection old = plot.getFixedLegendItems(); |
502 int idx = 0; | 559 |
503 if (plot.getRangeAxis(idx) == null && plot.getRangeAxisCount() >= 2) { | 560 XYItemRenderer renderer = plot.getRenderer(0); |
504 idx = 1; | |
505 } | |
506 | |
507 XYItemRenderer renderer = plot.getRenderer(idx); | |
508 | 561 |
509 for (FLYSAnnotation fa: annotations) { | 562 for (FLYSAnnotation fa: annotations) { |
510 Document theme = fa.getTheme(); | 563 Document theme = fa.getTheme(); |
511 | 564 |
512 ThemeAccess themeAccess = new ThemeAccess(theme); | 565 ThemeAccess themeAccess = new ThemeAccess(theme); |
526 ta.setOutlineStroke(new BasicStroke((float) lineWidth)); | 579 ta.setOutlineStroke(new BasicStroke((float) lineWidth)); |
527 renderer.addAnnotation(ta); | 580 renderer.addAnnotation(ta); |
528 } | 581 } |
529 } | 582 } |
530 | 583 |
531 // TODO Do after loop? | 584 } |
532 plot.setFixedLegendItems(lic); | 585 |
533 } | 586 // (Re-)Add prior legend entries. |
587 if (old != null) { | |
588 old.addAll(lic); | |
589 } | |
590 else { | |
591 old = lic; | |
592 } | |
593 | |
594 plot.setFixedLegendItems(old); | |
534 } | 595 } |
535 | 596 |
536 | 597 |
537 /** | 598 /** |
538 * Adjusts the axes of a plot (the first axis does not include zero). | 599 * Adjusts the axes of a plot (the first axis does not include zero). |
539 * | 600 * To be overridden by children. |
540 * @param plot The XYPlot of the chart. | 601 * @param plot The XYPlot of the chart. |
541 */ | 602 */ |
542 protected void adjustAxes(XYPlot plot) { | 603 protected void adjustAxes(XYPlot plot) { |
604 /* | |
543 NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); | 605 NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); |
544 if (yAxis == null) { | 606 if (yAxis == null) { |
545 logger.warn("No Axis to setAutoRangeIncludeZero."); | 607 logger.warn("No Axis to setAutoRangeIncludeZero."); |
546 } | 608 } |
547 else { | 609 else { |
548 yAxis.setAutoRangeIncludesZero(false); | 610 yAxis.setAutoRangeIncludesZero(false); |
549 } | 611 } |
550 } | 612 */ |
551 | 613 } |
552 | 614 |
615 | |
616 /** | |
617 * Set some Stroke/Grid defaults. | |
618 */ | |
553 protected void adjustPlot(XYPlot plot) { | 619 protected void adjustPlot(XYPlot plot) { |
554 Stroke gridStroke = new BasicStroke( | 620 Stroke gridStroke = new BasicStroke( |
555 DEFAULT_GRID_LINE_WIDTH, | 621 DEFAULT_GRID_LINE_WIDTH, |
556 BasicStroke.CAP_BUTT, | 622 BasicStroke.CAP_BUTT, |
557 BasicStroke.JOIN_MITER, | 623 BasicStroke.JOIN_MITER, |
630 NumberFormat nf = NumberFormat.getInstance(getLocale()); | 696 NumberFormat nf = NumberFormat.getInstance(getLocale()); |
631 ((NumberAxis) rangeAxis).setNumberFormatOverride(nf); | 697 ((NumberAxis) rangeAxis).setNumberFormatOverride(nf); |
632 } | 698 } |
633 | 699 |
634 | 700 |
635 protected void applyThemes(XYPlot plot) { | |
636 int idx = 0; | |
637 | |
638 for (Map.Entry<Integer, List<XYDataset>> entry: datasets.entrySet()) { | |
639 for (XYDataset dataset: entry.getValue()) { | |
640 if (dataset instanceof XYSeriesCollection) { | |
641 idx = applyThemes(plot, (XYSeriesCollection)dataset, idx); | |
642 } | |
643 } | |
644 } | |
645 } | |
646 | |
647 | |
648 /** | 701 /** |
649 * @param idx "index" of dataset/series (first dataset to be drawn has | 702 * @param idx "index" of dataset/series (first dataset to be drawn has |
650 * index 0), correlates with renderer index. | 703 * index 0), correlates with renderer index. |
651 * @return idx increased by number of items addded. | 704 * @return idx increased by number of items addded. |
652 */ | 705 */ |