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 */

http://dive4elements.wald.intevation.org