changeset 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 2730d17df021
children 0fa53fa65401
files flys-artifacts/ChangeLog flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java
diffstat 2 files changed, 193 insertions(+), 130 deletions(-) [+]
line wrap: on
line diff
--- a/flys-artifacts/ChangeLog	Fri Nov 25 18:52:11 2011 +0000
+++ b/flys-artifacts/ChangeLog	Mon Nov 28 14:36:56 2011 +0000
@@ -1,3 +1,13 @@
+2011-11-28  Felix Wolfsteller <felix.wolfsteller@intevation.de>
+
+	Overhaul dataset/axis/renderer housekeeping in Mother of all
+	ChartGenerators.
+
+	* src/main/java/de/intevation/flys/exports/XYChartGenerator.java:
+	  Refactored, keep axis/rendering relevant information in objects
+	  of new class AxisDataset. Removed some obsolete code while adding
+	  documentation.
+
 2011-11-25	Sascha L. Teichmann	<sascha.teichmann@intevation.de>
 
 	* src/main/java/de/intevation/flys/artifacts/model/WstValueTable.java(relateWs):
--- a/flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java	Fri Nov 25 18:52:11 2011 +0000
+++ b/flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java	Mon Nov 28 14:36:56 2011 +0000
@@ -49,15 +49,67 @@
 /**
  * An abstract base class for creating XY charts.
  *
+ * With respect to datasets, ranges and axis, there are following requirements:
+ * <ul>
+ *   <li> First in, first drawn: "Early" datasets should be of lower Z-Oder
+ *        than later ones (only works per-axis). </li>
+ *   <li> Visible axis should initially show the range of all datasets that
+ *        show data for this axis (even invisible ones). Motivation: Once
+ *        a dataset (theme) has been activated, it should be on screen. </li>
+ *   <li> There should always be a Y-Axis on the "left". </li>
+ * </ul>
+ *
  * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
  */
 public abstract class XYChartGenerator extends ChartGenerator {
 
+    private class AxisDataset {
+        /** Symbolic integer, but also coding the priority (0 goes first). */
+        protected int axisSymbol;
+        /** List of assigned datasets (in order). */
+        protected List<XYDataset> datasets;
+        /** Range to use to include all given datasets. */
+        protected Range range;
+
+        /** Create AxisDataset. */
+        public AxisDataset(int symb) {
+            this.axisSymbol = symb;
+            datasets        = new ArrayList<XYDataset>();
+        }
+
+        /** Merge (or create given range with range so far (if any). */
+        private void mergeRanges(Range subRange) {
+            if (range == null) {
+                range = subRange;
+                return;
+            }
+            range = Range.combine(range, subRange);
+        }
+
+        /** Add a dataset, include its range. */
+        public void addDataset(XYSeries dataset) {
+            this.datasets.add(new XYSeriesCollection(dataset));
+            includeRange(dataset);
+        }
+
+        /** Adjust range to include given dataset. */
+        public void includeRange(XYSeries dataset) {
+            mergeRanges(new Range(dataset.getMinY(), dataset.getMaxY()));
+        }
+
+        /** True if no datasets given. */
+        public boolean isEmpty() {
+            return this.datasets.isEmpty();
+        }
+
+    }
+
+
     /** The logger that is used in this generator. */
     private static Logger logger = Logger.getLogger(XYChartGenerator.class);
 
     /** Map of datasets ("index"). */
-    protected SortedMap<Integer, List<XYDataset>> datasets;
+    protected SortedMap<Integer, AxisDataset> datasets;
 
     /** List of annotations to insert in plot. */
     protected List<FLYSAnnotation> annotations;
@@ -75,7 +127,7 @@
     public XYChartGenerator() {
         xRanges  = new HashMap<Integer, Range>();
         yRanges  = new HashMap<Integer, Range>();
-        datasets = new TreeMap<Integer, List<XYDataset>>();
+        datasets = new TreeMap<Integer, AxisDataset>();
     }
 
 
@@ -86,6 +138,7 @@
      */
     protected abstract String getChartTitle();
 
+
     /**
      * Returns the X-Axis label of a chart.
      *
@@ -93,6 +146,7 @@
      */
     protected abstract String getXAxisLabel();
 
+
     /**
      * Returns the Y-Axis label of a chart.
      *
@@ -100,7 +154,9 @@
      */
     protected abstract String getYAxisLabel();
 
-
+    /**
+     * Generate chart.
+     */
     public void generate()
     throws IOException
     {
@@ -168,25 +224,62 @@
         XYPlot plot = (XYPlot) chart.getPlot();
         chart.setBackgroundPaint(Color.WHITE);
         plot.setBackgroundPaint(Color.WHITE);
-
-        addDatasets(plot);
-        addAnnotations(plot);
         addSubtitles(chart);
         adjustPlot(plot);
-        localizeAxes(plot);
 
-        createAxes(plot);
-        adjustAxes(plot);
+        //debugAxis(plot);
+
+        addDatasets(plot);
+
+        //debugDatasets(plot);
 
         recoverEmptyPlot(plot);
+        preparePointRanges(plot);
 
-        preparePointRanges(plot);
+        addAnnotationsToRenderer(plot);
+
+        //debugAxis(plot);
+
+        localizeAxes(plot);
+        adjustAxes(plot);
         autoZoom(plot);
 
-        applyThemes(plot);
-        removeEmptyRangeAxes(plot);
+        return chart;
+    }
 
-        return chart;
+
+    /**
+     * Put debug output about datasets.
+     */
+    public void debugDatasets(XYPlot plot) {
+        logger.debug("Number of datasets: " + plot.getDatasetCount());
+        for (int i = 0; i < plot.getDatasetCount(); i++) {
+            if (plot.getDataset(i) == null) {
+                logger.debug("Dataset #" + i + " is null");
+                continue;
+            }
+            logger.debug("Dataset #" + i + ":" + plot.getDataset(i));
+        }
+    }
+
+
+    /**
+     * Put debug output about axes.
+     */
+    public void debugAxis(XYPlot plot) {
+        logger.debug("...............");
+        for (int i = 0; i < plot.getRangeAxisCount(); i++) {
+            if (plot.getRangeAxis(i) == null)
+                logger.debug("Axis #" + i + " == null");
+            else {
+                logger.debug("Axis " + i + " != null [" +
+                    plot.getRangeAxis(i).getRange().getLowerBound() +
+                    "  " + plot.getRangeAxis(i).getRange().getUpperBound() +
+                    "]");
+            }
+
+        }
+        logger.debug("...............");
     }
 
 
@@ -195,14 +288,30 @@
      * @param plot plot to add datasets to.
      */
     protected void addDatasets(XYPlot plot) {
-        int count = 0;
-        for (Map.Entry<Integer, List<XYDataset>> entry: datasets.entrySet()) {
-            List<Integer> axisList = new ArrayList<Integer>(1);
-            axisList.add(entry.getKey());
-            for (XYDataset dataset: entry.getValue()) {
-                int index = count++;
-                plot.setDataset(index, dataset);
-                plot.mapDatasetToRangeAxes(index, axisList);
+        // AxisDatasets are sorted, but some might be empty.
+        // Thus, generate numbering on the fly.
+        int axisIndex    = 0;
+        int datasetIndex = 0;
+        for (Map.Entry<Integer, AxisDataset> entry: datasets.entrySet()) {
+            if (!entry.getValue().isEmpty()) {
+                // Add axis and range information.
+                AxisDataset axisDataset = entry.getValue();
+                NumberAxis axis = createYAxis(entry.getKey());
+
+                plot.setRangeAxis(axisIndex, axis);
+                if (axis.getAutoRangeIncludesZero()) {
+                    axisDataset.range = Range.expandToInclude(axisDataset.range, 0d);
+                }
+                yRanges.put(axisIndex, expandPointRange(axisDataset.range));
+
+                // Add contained datasets, mapping to axis.
+                for (XYDataset dataset: axisDataset.datasets) {
+                    plot.setDataset(datasetIndex, dataset);
+                    plot.mapDatasetToRangeAxis(datasetIndex, axisIndex);
+                    applyThemes(plot, (XYSeriesCollection) dataset, datasetIndex);
+                    datasetIndex++;
+                }
+                axisIndex++;
             }
         }
     }
@@ -220,25 +329,26 @@
             return;
         }
 
-        if (visible) {
-            XYSeriesCollection collection = new XYSeriesCollection(series);
-
-            List<XYDataset> dataset = datasets.get(index);
+        AxisDataset axisDataset = datasets.get(index);
 
-            if (dataset == null) {
-                dataset = new ArrayList<XYDataset>();
-                datasets.put(index, dataset);
-            }
-
-            dataset.add(collection);
+        if (axisDataset == null) {
+            axisDataset = new AxisDataset(index);
+            datasets.put(index, axisDataset);
         }
 
-        // Do this also when not visible to have axis scaled by default such
-        // that every data-point could be seen (except for annotations).
+        if (visible) {
+            axisDataset.addDataset(series);
+        }
+        else {
+            // Do this also when not visible to have axis scaled by default such
+            // that every data-point could be seen (except for annotations).
+            axisDataset.includeRange(series);
+        }
+
         combineXRanges(new Range(series.getMinX(), series.getMaxX()), 0);
-        combineYRanges(new Range(series.getMinY(), series.getMaxY()), index);
     }
 
+
     /**
      * Effect: extend range of x axis to include given limits.
      * @param range the given ("minimal") range.
@@ -257,21 +367,6 @@
 
 
     /**
-     * @param range the new range.
-     */
-    private void combineYRanges(Range range, int index) {
-
-        Range old = yRanges.get(index);
-
-        if (old != null) {
-            range = Range.combine(old, range);
-        }
-
-        yRanges.put(index, range);
-    }
-
-
-    /**
      * Adds annotations to list (if visible is true).
      */
     public void addAnnotations(FLYSAnnotation annotation, boolean visible) {
@@ -288,42 +383,15 @@
 
 
     /**
-     * Create y-axes, ensure that the first axis (with data) is on the left.
-     */
-    public void createAxes(XYPlot plot) {
-        logger.debug("XYChartGenerator.createAxes");
-
-        if (datasets.isEmpty()) {
-            plot.setRangeAxis(0, createYAxis(0));
-        }
-        else {
-            Integer last = datasets.lastKey();
-            int i            = 0;
-            int firstVisible = 0;
-    
-            if (last != null) {
-                firstVisible = i = last;
-                for (; i >= 0; --i) {
-                    if (datasets.containsKey(i)) {
-                        plot.setRangeAxis(i, createYAxis(i));
-                        firstVisible = i;
-                    }
-                }
-                plot.setRangeAxisLocation(firstVisible, AxisLocation.TOP_OR_LEFT);
-            }
-        }
-    }
-
-
-    /**
      * Create Y (range) axis for given index.
      * Shall be overriden by subclasses.
      */
     protected NumberAxis createYAxis(int index) {
-        NumberAxis axis = new NumberAxis("default axis");
+        NumberAxis axis = new NumberAxis(getYAxisLabel());
         return axis;
     }
 
+
     /**
      * If no data is visible, draw at least empty axis.
      */
@@ -332,33 +400,26 @@
             logger.debug("debug: No range axis");
             plot.setRangeAxis(createYAxis(0));
         }
-
-    }
-
-    /**
-     * Remove Axes which do not have data on them.
-     */
-    private void removeEmptyRangeAxes(XYPlot plot) {
-        if (datasets.isEmpty()) {
-            return;
-        }
-        Integer last = datasets.lastKey();
-
-        if (last != null) {
-            for (int i = last-1; i >= 0; --i) {
-                if (!datasets.containsKey(i)) {
-                    plot.setRangeAxis(i, null);
-                }
-            }
-        }
     }
 
 
     /**
-     * Expands X and Y axes if only a point is shown.
+     * Expands a given range if it collapses into one point.
+     */
+    private Range expandPointRange(Range range) {
+        if (range.getLowerBound() == range.getUpperBound()) {
+            return expandRange(range, 5);
+        }
+        return range;
+    }
+
+
+    /**
+     * Expands X axes if only a point is shown.
      */
     private void preparePointRanges(XYPlot plot) {
         for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
+            logger.debug("Check whether to expand a x axis.");
             Integer key = Integer.valueOf(i);
 
             Range r = xRanges.get(key);
@@ -366,15 +427,6 @@
                 xRanges.put(key, expandRange(r, 5));
             }
         }
-
-        for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
-            Integer key = Integer.valueOf(i);
-
-            Range r = yRanges.get(key);
-            if (r != null && r.getLowerBound() == r.getUpperBound()) {
-                yRanges.put(key, expandRange(r, 5));
-            }
-        }
     }
 
 
@@ -481,7 +533,9 @@
      *
      * @return a Range[] as follows: [x-Range, y-Range].
      */
+    // TODO When is this actually called? Is it valid as is?
     public Range[] getRangesForDataset(int index) {
+        logger.debug("getRangesForDataset " + index);
         return new Range[] {
             xRanges.get(Integer.valueOf(0)),
             yRanges.get(Integer.valueOf(index))
@@ -489,7 +543,10 @@
     }
 
 
-    protected void addAnnotations(XYPlot plot) {
+    /**
+     * Add annotations to Renderer.
+     */
+    protected void addAnnotationsToRenderer(XYPlot plot) {
         plot.clearAnnotations();
 
         if (annotations == null) {
@@ -498,13 +555,9 @@
         }
 
         LegendItemCollection lic = new LegendItemCollection();
+        LegendItemCollection old = plot.getFixedLegendItems();
 
-        int idx = 0;
-        if (plot.getRangeAxis(idx) == null && plot.getRangeAxisCount() >= 2) {
-            idx = 1;
-        }
-
-        XYItemRenderer renderer = plot.getRenderer(idx);
+        XYItemRenderer renderer = plot.getRenderer(0);
 
         for (FLYSAnnotation fa: annotations) {
             Document theme = fa.getTheme();
@@ -528,18 +581,27 @@
                 }
             }
 
-            // TODO Do after loop?
-            plot.setFixedLegendItems(lic);
         }
+
+        // (Re-)Add prior legend entries.
+        if (old != null) {
+            old.addAll(lic);
+        }
+        else {
+            old = lic;
+        }
+
+        plot.setFixedLegendItems(old);
     }
 
 
     /**
      * Adjusts the axes of a plot (the first axis does not include zero).
-     *
+     * To be overridden by children.
      * @param plot The XYPlot of the chart.
      */
     protected void adjustAxes(XYPlot plot) {
+        /*
         NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
         if (yAxis == null) {
             logger.warn("No Axis to setAutoRangeIncludeZero.");
@@ -547,9 +609,13 @@
         else {
             yAxis.setAutoRangeIncludesZero(false);
         }
+        */
     }
 
 
+    /**
+     * Set some Stroke/Grid defaults.
+     */
     protected void adjustPlot(XYPlot plot) {
         Stroke gridStroke = new BasicStroke(
             DEFAULT_GRID_LINE_WIDTH,
@@ -632,19 +698,6 @@
     }
 
 
-    protected void applyThemes(XYPlot plot) {
-        int idx = 0;
-
-        for (Map.Entry<Integer, List<XYDataset>> entry: datasets.entrySet()) {
-            for (XYDataset dataset: entry.getValue()) {
-                if (dataset instanceof XYSeriesCollection) {
-                    idx = applyThemes(plot, (XYSeriesCollection)dataset, idx);
-                }
-            }
-        }
-    }
-
-
     /**
      * @param idx "index" of dataset/series (first dataset to be drawn has
      *            index 0), correlates with renderer index.

http://dive4elements.wald.intevation.org