diff artifacts/src/main/java/org/dive4elements/river/exports/DiagramGenerator.java @ 7116:3c7471b929d1

Merge branch generator-refectoring into default.
author Andre Heinecke <aheinecke@intevation.de>
date Tue, 24 Sep 2013 12:29:44 +0200
parents 7b88b87958ac
children 038a04e001d7
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artifacts/src/main/java/org/dive4elements/river/exports/DiagramGenerator.java	Tue Sep 24 12:29:44 2013 +0200
@@ -0,0 +1,1119 @@
+/* Copyright (C) 2013 by Bundesanstalt für Gewässerkunde
+ * Software engineering by Intevation GmbH
+ *
+ * This file is Free Software under the GNU AGPL (>=v3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out the
+ * documentation coming with Dive4Elements River for details.
+ */
+
+package org.dive4elements.river.exports;
+
+import java.awt.Color;
+import java.awt.Font;
+
+import java.text.NumberFormat;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.ImageIcon;
+
+import org.apache.log4j.Logger;
+
+import org.dive4elements.artifactdatabase.state.ArtifactAndFacet;
+import org.dive4elements.artifactdatabase.state.Facet;
+
+import org.dive4elements.river.artifacts.D4EArtifact;
+
+import org.dive4elements.river.exports.process.Processor;
+
+import org.dive4elements.river.jfree.AnnotationHelper;
+import org.dive4elements.river.jfree.AxisDataset;
+import org.dive4elements.river.jfree.Bounds;
+import org.dive4elements.river.jfree.DoubleBounds;
+import org.dive4elements.river.jfree.StyledAreaSeriesCollection;
+
+import org.dive4elements.river.themes.ThemeDocument;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.LegendItem;
+
+import org.jfree.chart.annotations.XYAnnotation;
+import org.jfree.chart.annotations.XYImageAnnotation;
+
+import org.jfree.chart.axis.LogarithmicAxis;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.ValueAxis;
+
+import org.jfree.chart.plot.Marker;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+
+import org.jfree.data.Range;
+
+import org.jfree.data.general.Series;
+
+import org.jfree.data.xy.XYDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+
+/**
+ * The main diagram creation class.
+ *
+ * This class is the glue between output processors and facets.
+ * The generator creates one diagram and calls the appropiate
+ * processors for the state and 
+ *
+ * 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>
+ */
+public class DiagramGenerator extends ChartGenerator2 {
+
+    public static final int AXIS_SPACE = 5;
+
+    /** The logger that is used in this generator. */
+    private static Logger logger = Logger.getLogger(DiagramGenerator.class);
+
+    protected List<Marker> domainMarkers = new ArrayList<Marker>();
+
+    protected List<Marker> valueMarkers = new ArrayList<Marker>();
+
+    /** The max X range to include all X values of all series for each axis. */
+    protected Map<Integer, Bounds> xBounds;
+
+    /** The max Y range to include all Y values of all series for each axis. */
+    protected Map<Integer, Bounds> yBounds;
+
+    /** Whether or not the plot is inverted (left-right). */
+    private boolean inverted;
+
+    protected DiagramAttributes.Instance diagramAttributes;
+
+    public DiagramGenerator() {
+        super();
+
+        xBounds  = new HashMap<Integer, Bounds>();
+        yBounds  = new HashMap<Integer, Bounds>();
+    }
+
+    @Override
+    public void setup(Object config) {
+
+        if (!(config instanceof DiagramAttributes)) {
+            logger.error("invalid config type");
+            return;
+        }
+        DiagramAttributes da = (DiagramAttributes)config;
+        diagramAttributes = da.new Instance();
+    }
+
+    /**
+     * Generate the chart anew (including localized axis and all).
+     */
+    @Override
+    public JFreeChart generateChart() {
+        logger.debug("DiagramGenerator.generateChart");
+
+        JFreeChart chart = ChartFactory.createXYLineChart(
+            getChartTitle(),
+            "",
+            "",
+            null,
+            PlotOrientation.VERTICAL,
+            isLegendVisible(),
+            false,
+            false);
+
+        XYPlot plot = (XYPlot) chart.getPlot();
+        ValueAxis axis = createXAxis(getXAxisLabel());
+        plot.setDomainAxis(axis);
+
+        chart.setBackgroundPaint(Color.WHITE);
+        plot.setBackgroundPaint(Color.WHITE);
+        addSubtitles(chart);
+        adjustPlot(plot);
+
+        //debugAxis(plot);
+
+        addDatasets(plot);
+
+        //debugDatasets(plot);
+
+        addMarkers(plot);
+
+        recoverEmptyPlot(plot);
+        preparePointRanges(plot);
+
+        //debugAxis(plot);
+
+        localizeAxes(plot);
+        adjustAxes(plot);
+        if (!(axis instanceof LogarithmicAxis)) {
+            // XXX:
+            // The auto zoom without a range tries
+            // to include 0 in a logarithmic axis
+            // which triggers a bug in jfreechart that causes
+            // the values to be drawn carthesian
+            autoZoom(plot);
+        }
+
+        //debugAxis(plot);
+
+        // These have to go after the autozoom.
+        AnnotationHelper.addAnnotationsToRenderer(annotations, plot,
+                getChartSettings(), datasets);
+
+        // Add a logo (maybe).
+        addLogo(plot);
+
+        aggregateLegendEntries(plot);
+
+        return chart;
+    }
+
+    public String getOutName() {
+        return outName;
+    }
+
+    /**
+     * Return left most data points x value (on first axis).
+     */
+    protected double getLeftX() {
+        if (inverted) {
+            return (Double)getXBounds(0).getUpper();
+        }
+        return (Double)getXBounds(0).getLower();
+    }
+
+
+    /**
+     * Return right most data points x value (on first axis).
+     */
+    protected double getRightX() {
+        if (inverted) {
+            return (Double)getXBounds(0).getLower();
+        }
+        return (Double)getXBounds(0).getUpper();
+    }
+
+
+    /** Add a logo as background annotation to plot. */
+    protected void addLogo(XYPlot plot) {
+        String logo = showLogo();
+        if (logo  == null) {
+            logger.debug("No logo to show chosen");
+            return;
+        }
+
+        ImageIcon imageIcon = null;
+        if (logo.equals("none")) {
+            return;
+        }
+        /*
+         If you want to add images, remember to change code in these places:
+         flys-artifacts:
+         DiagramGenerator.java
+         Timeseries*Generator.java and
+         in the flys-client projects Chart*Propert*Editor.java.
+         Also, these images have to be put in
+         flys-artifacts/src/main/resources/images/
+         flys-client/src/main/webapp/images/
+         */
+        java.net.URL imageURL;
+        if (logo.equals("Intevation")) {
+            imageURL = DiagramGenerator.class.getResource("/images/intevation.png");
+        }
+        else { // TODO else if ...
+            imageURL = DiagramGenerator.class.getResource("/images/bfg_logo.gif");
+        }
+        imageIcon = new ImageIcon(imageURL);
+
+
+        double xPos = 0d, yPos = 0d;
+
+        String placeh = logoHPlace();
+        String placev = logoVPlace();
+
+        if (placev == null || placev.equals("none")) {
+            placev = "top";
+        }
+        if (placev.equals("top")) {
+            yPos = (Double)getYBounds(0).getUpper();
+        }
+        else if (placev.equals("bottom")) {
+            yPos = (Double)getYBounds(0).getLower();
+        }
+        else if (placev.equals("center")) {
+            yPos = ((Double)getYBounds(0).getUpper() + (Double)getYBounds(0).getLower())/2d;
+        }
+        else {
+            logger.debug("Unknown place-v value: " + placev);
+        }
+
+        if (placeh == null || placeh.equals("none")) {
+            placeh = "center";
+        }
+        if (placeh.equals("left")) {
+            xPos = getLeftX();
+        }
+        else if (placeh.equals("right")) {
+            xPos = getRightX();
+        }
+        else if (placeh.equals("center")) {
+            xPos = ((Double)getXBounds(0).getUpper() + (Double)getXBounds(0).getLower())/2d;
+        }
+        else {
+            logger.debug("Unknown place-h value: " + placeh);
+        }
+
+        logger.debug("logo position: " + xPos + "/" + yPos);
+
+        org.jfree.ui.RectangleAnchor anchor
+            = org.jfree.ui.RectangleAnchor.TOP;
+        if (placev.equals("top")) {
+            if (placeh.equals("left")) {
+                anchor = org.jfree.ui.RectangleAnchor.TOP_LEFT;
+            }
+            else if (placeh.equals("right")) {
+                anchor = org.jfree.ui.RectangleAnchor.TOP_RIGHT;
+            }
+            else if (placeh.equals("center")) {
+                anchor = org.jfree.ui.RectangleAnchor.TOP;
+            }
+        }
+        else if (placev.equals("bottom")) {
+            if (placeh.equals("left")) {
+                anchor = org.jfree.ui.RectangleAnchor.BOTTOM_LEFT;
+            }
+            else if (placeh.equals("right")) {
+                anchor = org.jfree.ui.RectangleAnchor.BOTTOM_RIGHT;
+            }
+            else if (placeh.equals("center")) {
+                anchor = org.jfree.ui.RectangleAnchor.BOTTOM;
+            }
+        }
+        else if (placev.equals("center")) {
+            if (placeh.equals("left")) {
+                anchor = org.jfree.ui.RectangleAnchor.LEFT;
+            }
+            else if (placeh.equals("right")) {
+                anchor = org.jfree.ui.RectangleAnchor.RIGHT;
+            }
+            else if (placeh.equals("center")) {
+                anchor = org.jfree.ui.RectangleAnchor.CENTER;
+            }
+        }
+
+        XYAnnotation xyannotation =
+            new XYImageAnnotation(xPos, yPos, imageIcon.getImage(), anchor);
+        plot.getRenderer().addAnnotation(xyannotation, org.jfree.ui.Layer.BACKGROUND);
+    }
+
+
+    protected NumberAxis createXAxis(String label) {
+        return new NumberAxis(label);
+    }
+
+
+    @Override
+    protected Series getSeriesOf(XYDataset dataset, int idx) {
+        return ((XYSeriesCollection) dataset).getSeries(idx);
+    }
+
+
+    @Override
+    protected AxisDataset createAxisDataset(int idx) {
+        logger.debug("Create new AxisDataset for index: " + idx);
+        return new AxisDataset(idx);
+    }
+
+
+    /**
+     * Put debug output about datasets.
+     */
+    public void debugDatasets(XYPlot plot) {
+        logger.debug("Number of datasets: " + plot.getDatasetCount());
+        for (int i = 0, P = plot.getDatasetCount(); i < P; i++) {
+            if (plot.getDataset(i) == null) {
+                logger.debug("Dataset #" + i + " is null");
+                continue;
+            }
+            logger.debug("Dataset #" + i + ":" + plot.getDataset(i));
+            XYSeriesCollection series = (XYSeriesCollection) plot.getDataset(i);
+            logger.debug("X-Extend of Dataset: " + series.getSeries(0).getMinX()
+                    + " " + series.getSeries(0).getMaxX());
+            logger.debug("Y-Extend of Dataset: " + series.getSeries(0).getMinY()
+                    + " " + series.getSeries(0).getMaxY());
+        }
+    }
+
+
+    /**
+     * Put debug output about axes.
+     */
+    public void debugAxis(XYPlot plot) {
+        logger.debug("...............");
+        for (int i = 0, P =  plot.getRangeAxisCount(); i < P; i++) {
+            if (plot.getRangeAxis(i) == null)
+                logger.debug("Range-Axis #" + i + " == null");
+            else {
+                logger.debug("Range-Axis " + i + " != null [" +
+                    plot.getRangeAxis(i).getRange().getLowerBound() +
+                    "  " + plot.getRangeAxis(i).getRange().getUpperBound() +
+                    "]");
+            }
+        }
+        for (int i = 0, P =  plot.getDomainAxisCount(); i < P; i++) {
+            if (plot.getDomainAxis(i) == null)
+                logger.debug("Domain-Axis #" + i + " == null");
+            else {
+                logger.debug("Domain-Axis " + i + " != null [" +
+                    plot.getDomainAxis(i).getRange().getLowerBound() +
+                    "  " + plot.getDomainAxis(i).getRange().getUpperBound() +
+                    "]");
+            }
+        }
+        logger.debug("...............");
+    }
+
+    /**
+     * Registers an area to be drawn.
+     * @param area Area to be drawn.
+     * @param axisName Name of the axis.
+     * @param visible Whether or not to be visible (important for range calculations).
+     */
+    public void addAreaSeries(StyledAreaSeriesCollection area, String axisName, boolean visible) {
+        addAreaSeries(area, diagramAttributes.getAxisIndex(axisName), visible);
+    }
+
+    /**
+     * Registers an area to be drawn.
+     * @param area Area to be drawn.
+     * @param index 'axis index'
+     * @param visible Whether or not to be visible (important for range calculations).
+     */
+    public void addAreaSeries(StyledAreaSeriesCollection area, int index, boolean visible) {
+        if (area == null) {
+            logger.warn("Cannot yet render above/under curve.");
+            return;
+        }
+
+        AxisDataset axisDataset = (AxisDataset) getAxisDataset(index);
+
+        if (visible) {
+            axisDataset.addArea(area);
+        }
+        else {
+            /* No range merging, for areas extending to infinity this
+             * causes problems. */
+        }
+    }
+
+    /**
+     * Add given series if visible, if not visible adjust ranges (such that
+     * all points in data would be plotted once visible).
+     * @param series   the data series to include in plot.
+     * @param index    index of the axis.
+     * @param visible  whether or not the data should be plotted.
+     */
+    public void addAxisSeries(XYSeries series, int index, boolean visible) {
+        if (series == null) {
+            return;
+        }
+
+        logger.debug("Y Range of XYSeries: " +
+            series.getMinY() + " | " + series.getMaxY());
+
+        addAxisDataset(new XYSeriesCollection(series), index, visible);
+    }
+
+    /**
+     * Add given series if visible, if not visible adjust ranges (such that
+     * all points in data would be plotted once visible).
+     * @param series   the data series to include in plot.
+     * @param axisName name of the axis.
+     * @param visible  whether or not the data should be plotted.
+     */
+    public void addAxisSeries(XYSeries series, String axisName, boolean visible) {
+        addAxisSeries(series, diagramAttributes.getAxisIndex(axisName), visible);
+    }
+
+
+    /**
+     * Add the given vertical marker to the chart.
+     */
+    public void addDomainMarker(Marker marker) {
+        addDomainMarker(marker, true);
+    }
+
+
+    /**
+     * Add the given vertical marker to the chart.<b>Note:</b> the marker is
+     * added to the chart only if it is not null and if <i>visible</i> is true.
+     * @param marker The marker that should be added to the chart.
+     * @param visible The visibility of the marker.
+     */
+    public void addDomainMarker(Marker marker, boolean visible) {
+        if (visible && marker != null) {
+            domainMarkers.add(marker);
+        }
+    }
+
+
+    /**
+     * Add the given vertical marker to the chart.
+     */
+    public void addValueMarker(Marker marker) {
+        addValueMarker(marker, true);
+    }
+
+
+    /**
+     * Add the given horizontal marker to the chart.<b>Note:</b> the marker is
+     * added to the chart only if it is not null and if <i>visible</i> is true.
+     * @param marker The marker that should be added to the chart.
+     * @param visible The visibility of the marker.
+     */
+    public void addValueMarker(Marker marker, boolean visible) {
+        if (visible && marker != null) {
+            valueMarkers.add(marker);
+        }
+    }
+
+
+    protected void addMarkers(XYPlot plot) {
+        for(Marker marker : domainMarkers) {
+            plot.addDomainMarker(marker);
+        }
+        for(Marker marker : valueMarkers) {
+            plot.addRangeMarker(marker);
+        }
+    }
+
+
+    /**
+     * Effect: extend range of x axis to include given limits.
+     *
+     * @param bounds the given ("minimal") bounds.
+     * @param index index of axis to be merged.
+     */
+    @Override
+    protected void combineXBounds(Bounds bounds, int index) {
+        if (!(bounds instanceof DoubleBounds)) {
+            logger.warn("Unsupported Bounds type: " + bounds.getClass());
+            return;
+        }
+
+        DoubleBounds dBounds = (DoubleBounds) bounds;
+
+        if (dBounds == null
+            || Double.isNaN((Double) dBounds.getLower())
+            || Double.isNaN((Double) dBounds.getUpper())) {
+            return;
+        }
+
+        Bounds old = getXBounds(index);
+
+        if (old != null) {
+            dBounds = (DoubleBounds) dBounds.combine(old);
+        }
+
+        setXBounds(index, dBounds);
+    }
+
+
+    @Override
+    protected void combineYBounds(Bounds bounds, int index) {
+        if (!(bounds instanceof DoubleBounds)) {
+            logger.warn("Unsupported Bounds type: " + bounds.getClass());
+            return;
+        }
+
+        DoubleBounds dBounds = (DoubleBounds) bounds;
+
+        if (dBounds == null
+            || Double.isNaN((Double) dBounds.getLower())
+            || Double.isNaN((Double) dBounds.getUpper())) {
+            return;
+        }
+
+        Bounds old = getYBounds(index);
+
+        if (old != null) {
+            dBounds = (DoubleBounds) dBounds.combine(old);
+        }
+
+        setYBounds(index, dBounds);
+    }
+
+
+    /**
+     * If no data is visible, draw at least empty axis.
+     */
+    private void recoverEmptyPlot(XYPlot plot) {
+        if (plot.getRangeAxis() == null) {
+            logger.debug("debug: No range axis");
+            plot.setRangeAxis(createYAxis(0));
+        }
+    }
+
+
+    /**
+     * Expands X axes if only a point is shown.
+     */
+    private void preparePointRanges(XYPlot plot) {
+        for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
+
+            Integer key = Integer.valueOf(i);
+            Bounds  b   = getXBounds(key);
+
+
+            if (b != null && b.getLower().equals(b.getUpper())) {
+                logger.debug("Check whether to expand a x axis.i ("+b.getLower() + "-" + b.getUpper()+")");
+                setXBounds(key, ChartHelper.expandBounds(b, 5));
+            }
+        }
+    }
+
+
+    /**
+     * This method zooms the plot to the specified ranges in the attribute
+     * document or to the ranges specified by the min/max values in the
+     * datasets. <b>Note:</b> We determine the range manually if no zoom ranges
+     * are given, because JFreeCharts auto-zoom adds a margin to the left and
+     * right of the data area.
+     *
+     * @param plot The XYPlot.
+     */
+    protected void autoZoom(XYPlot plot) {
+        logger.debug("Zoom to specified ranges.");
+
+        Range xrange = getDomainAxisRange();
+        Range yrange = getValueAxisRange();
+
+        ValueAxis xAxis = plot.getDomainAxis();
+
+        Range fixedXRange = getRangeForAxisFromSettings("X");
+        if (fixedXRange != null) {
+            xAxis.setRange(fixedXRange);
+        }
+        else {
+            zoomX(plot, xAxis, getXBounds(0), xrange);
+        }
+
+        for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
+            ValueAxis yaxis = plot.getRangeAxis(i);
+
+            if (yaxis instanceof IdentifiableNumberAxis) {
+                IdentifiableNumberAxis idAxis = (IdentifiableNumberAxis) yaxis;
+
+                Range fixedRange = getRangeForAxisFromSettings(idAxis.getId());
+                if (fixedRange != null) {
+                    yaxis.setRange(fixedRange);
+                    continue;
+                }
+            }
+
+            if (yaxis == null) {
+                logger.debug("Zoom problem: no Y Axis for index: " + i);
+                continue;
+            }
+
+            logger.debug("Prepare zoom settings for y axis at index: " + i);
+            zoomY(plot, yaxis, getYBounds(Integer.valueOf(i)), yrange);
+        }
+    }
+
+
+    protected Range getDomainAxisRange() {
+        String[] ranges = getDomainAxisRangeFromRequest();
+
+        if (ranges == null || ranges.length < 2) {
+            logger.debug("No zoom range for domain axis specified.");
+            return null;
+        }
+
+        if (ranges[0].length() > 0 && ranges[1].length() > 0) {
+            try {
+                double from = Double.parseDouble(ranges[0]);
+                double to   = Double.parseDouble(ranges[1]);
+
+                if (from == 0 && to == 0) {
+                    logger.debug("No range specified. Lower and upper X == 0");
+                    return null;
+                }
+
+                if (from > to) {
+                    double tmp = to;
+                    to         = from;
+                    from       = tmp;
+                }
+
+                return new Range(from, to);
+            }
+            catch (NumberFormatException nfe) {
+                logger.warn("Wrong values for domain axis range.");
+            }
+        }
+
+        return null;
+    }
+
+
+    protected Range getValueAxisRange() {
+        String[] ranges = getValueAxisRangeFromRequest();
+
+        if (ranges == null || ranges.length < 2) {
+            logger.debug("No range specified. Lower and upper Y == 0");
+            return null;
+        }
+
+        if (ranges[0].length() > 0 && ranges[1].length() > 0) {
+            try {
+                double from = Double.parseDouble(ranges[0]);
+                double to   = Double.parseDouble(ranges[1]);
+
+                if (from == 0 && to == 0) {
+                    logger.debug("No range specified. Lower and upper Y == 0");
+                    return null;
+                }
+
+                return from > to
+                       ? new Range(to, from)
+                       : new Range(from, to);
+            }
+            catch (NumberFormatException nfe) {
+                logger.warn("Wrong values for value axis range.");
+            }
+        }
+
+        return null;
+    }
+
+
+    protected boolean zoomX(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) {
+        return zoom(plot, axis, bounds, x);
+    }
+
+
+    protected boolean zoomY(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) {
+        return zoom(plot, axis, bounds, x);
+    }
+
+
+    /**
+     * Zooms the x axis to the range specified in the attribute document.
+     *
+     * @param plot  The XYPlot.
+     * @param axis  The axis the shoud be modified.
+     * @param bounds The whole range specified by a dataset.
+     * @param x     A user defined range (null permitted).
+     *
+     * @return true, if a zoom range was specified, otherwise false.
+     */
+    protected boolean zoom(XYPlot plot, ValueAxis axis, Bounds bounds, Range x) {
+
+        if (bounds == null) {
+            return false;
+        }
+
+        if (x != null) {
+            Bounds computed = calculateZoom(bounds, x);
+            computed.applyBounds(axis, AXIS_SPACE);
+
+            logger.debug("Zoom axis to: " + computed);
+
+            return true;
+        }
+
+        bounds.applyBounds(axis, AXIS_SPACE);
+        return false;
+    }
+
+    /**
+     * Calculates the start and end km for zoomed charts.
+     * @param bounds    The given total bounds (unzoomed).
+     * @param range     The range specifying the zoom.
+     *
+     * @return The start and end km for the zoomed chart.
+     */
+    protected Bounds calculateZoom(Bounds bounds, Range range) {
+        double min  = bounds.getLower().doubleValue();
+        double max  = bounds.getUpper().doubleValue();
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Minimum is: " + min);
+            logger.debug("Maximum is: " + max);
+            logger.debug("Lower zoom is: " + range.getLowerBound());
+            logger.debug("Upper zoom is: " + range.getUpperBound());
+        }
+
+        double diff = max > min ? max - min : min - max;
+
+        DoubleBounds computed = new DoubleBounds(
+            min + range.getLowerBound() * diff,
+            min + range.getUpperBound() * diff);
+        return computed;
+    }
+
+    /**
+     * Extract the minimum and maximum values for x and y axes
+     * which are stored in <i>xRanges</i> and <i>yRanges</i>.
+     *
+     * @param index The index of the y-Axis.
+     *
+     * @return a Range[] as follows: [x-Range, y-Range].
+     */
+    @Override
+    public Range[] getRangesForAxis(int index) {
+        logger.debug("getRangesForAxis " + index);
+
+        Bounds rx = getXBounds(Integer.valueOf(0));
+        Bounds ry = getYBounds(Integer.valueOf(index));
+
+        if (rx == null) {
+            logger.warn("Range for x axis not set." +
+                        " Using default values: 0 - 1.");
+            rx = new DoubleBounds(0, 1);
+        }
+        if (ry == null) {
+            logger.warn("Range for y" + index +
+                        " axis not set. Using default values: 0 - 1.");
+            ry = new DoubleBounds(0, 1);
+        }
+
+        return new Range[] {
+            new Range(rx.getLower().doubleValue(), rx.getUpper().doubleValue()),
+            new Range(ry.getLower().doubleValue(), ry.getUpper().doubleValue())
+        };
+    }
+
+
+    /** Get X (usually horizontal) extent for given axis. */
+    @Override
+    public Bounds getXBounds(int axis) {
+        return xBounds.get(axis);
+    }
+
+
+    /** Set X (usually horizontal) extent for given axis. */
+    @Override
+    protected void setXBounds(int axis, Bounds bounds) {
+        if (bounds.getLower() == bounds.getUpper()) {
+            xBounds.put(axis, ChartHelper.expandBounds(bounds, 5d));
+        }
+        else {
+            xBounds.put(axis, bounds);
+        }
+    }
+
+
+    /** Get Y (usually vertical) extent for given axis. */
+    @Override
+    public Bounds getYBounds(int axis) {
+        return yBounds.get(axis);
+    }
+
+
+    /** Set Y (usually vertical) extent for given axis. */
+    @Override
+    protected void setYBounds(int axis, Bounds bounds) {
+        yBounds.put(axis, bounds);
+    }
+
+
+    /**
+     * Adjusts the axes of a plot. This method sets the <i>labelFont</i> of the
+     * X axis and applies the inversion if inverted is true.
+     *
+     * (Duplicate in TimeseriesChartGenerator)
+     *
+     * @param plot The XYPlot of the chart.
+     */
+    protected void adjustAxes(XYPlot plot) {
+        ValueAxis xaxis = plot.getDomainAxis();
+
+        ChartSettings chartSettings = getChartSettings();
+        if (chartSettings == null) {
+            return;
+        }
+
+        Font labelFont = new Font(
+            DEFAULT_FONT_NAME,
+            Font.BOLD,
+            getXAxisLabelFontSize());
+
+        xaxis.setLabelFont(labelFont);
+        xaxis.setTickLabelFont(labelFont);
+
+        logger.debug("Adjusting xAxis. Inverted?: " + inverted);
+        if (inverted) {
+            xaxis.setInverted(true);
+        }
+    }
+
+
+    /**
+     * This method walks over all axes (domain and range) of <i>plot</i> and
+     * calls localizeDomainAxis() for domain axes or localizeRangeAxis() for
+     * range axes.
+     *
+     * @param plot The XYPlot.
+     */
+    private void localizeAxes(XYPlot plot) {
+        for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
+            ValueAxis axis = plot.getDomainAxis(i);
+
+            if (axis != null) {
+                localizeDomainAxis(axis);
+            }
+            else {
+                logger.warn("Domain axis at " + i + " is null.");
+            }
+        }
+
+        for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
+            ValueAxis axis = plot.getRangeAxis(i);
+
+            if (axis != null) {
+                localizeRangeAxis(axis);
+            }
+            else {
+                logger.warn("Range axis at " + i + " is null.");
+            }
+        }
+    }
+
+
+    /**
+     * Overrides the NumberFormat with the NumberFormat for the current locale
+     * that is provided by getLocale().
+     *
+     * @param domainAxis The domain axis that needs localization.
+     */
+    protected void localizeDomainAxis(ValueAxis domainAxis) {
+        NumberFormat nf = NumberFormat.getInstance(getLocale());
+        ((NumberAxis) domainAxis).setNumberFormatOverride(nf);
+    }
+
+
+    /**
+     * Overrides the NumberFormat with the NumberFormat for the current locale
+     * that is provided by getLocale().
+     *
+     * @param rangeAxis The domain axis that needs localization.
+     */
+    protected void localizeRangeAxis(ValueAxis rangeAxis) {
+        NumberFormat nf = NumberFormat.getInstance(getLocale());
+        ((NumberAxis) rangeAxis).setNumberFormatOverride(nf);
+    }
+
+
+    /**
+     * Create a hash from a legenditem.
+     * This hash can then be used to merge legend items labels.
+     * @return hash for given legenditem to identify mergeables.
+     */
+    public static String legendItemHash(LegendItem li) {
+        // TODO Do proper implementation. Ensure that only mergable sets are created.
+        // getFillPaint()
+        // getFillPaintTransformer()
+        // getLabel()
+        // getLine()
+        // getLinePaint()
+        // getLineStroke()
+        // getOutlinePaint()
+        // getOutlineStroke()
+        // Shape getShape()
+        // String getToolTipText()
+        // String getURLText()
+        // boolean isLineVisible()
+        // boolean isShapeFilled()
+        // boolean isShapeOutlineVisible()
+        // boolean isShapeVisible()
+        String hash = li.getLinePaint().toString();
+        String label = li.getLabel();
+        if (label.startsWith("W (") || label.startsWith("W(")) {
+            hash += "-W-";
+        }
+        else if (label.startsWith("Q(") || label.startsWith("Q (")) {
+            hash += "-Q-";
+        }
+
+        // WQ.java holds example of using regex Matcher/Pattern.
+
+        return hash;
+    }
+
+    /** True if x axis has been inverted. */
+    public boolean isInverted() {
+        return inverted;
+    }
+
+
+    /** Set to true if x axis should be inverted.
+     * This can not be set to false afterwards. */
+    public void setInverted(boolean value) {
+        /* One request to invert dominates. */
+        if (!inverted) {
+            inverted = value;
+        }
+    }
+
+    @Override
+    public String getDefaultChartTitle() {
+        DiagramAttributes.Title dTitle = diagramAttributes.getTitle();
+        if (dTitle == null) {
+            return "Title not configured in conf.xml";
+        }
+
+        return dTitle.evaluate((D4EArtifact)getMaster(), context);
+    }
+
+    @Override
+    public String getDefaultChartSubtitle() {
+        DiagramAttributes.Title dTitle = diagramAttributes.getSubtitle();
+        if (dTitle == null) {
+            return "Subtitle not configured in conf.xml";
+        }
+
+        return dTitle.evaluate((D4EArtifact)getMaster(), context);
+    }
+
+    /**
+     * Get internationalized label for the x axis.
+     */
+    @Override
+    protected String getDefaultXAxisLabel() {
+        DiagramAttributes.Title dTitle = diagramAttributes.getDomainAxisTitle();
+        if (dTitle == null) {
+            return "Domain Axis Title not configured in conf.xml";
+        }
+
+        return dTitle.evaluate((D4EArtifact)getMaster(), context);
+    }
+
+    @Override
+    protected String getDefaultYAxisLabel(String axisName) {
+        Processor pr = diagramAttributes.getProcessorForAxisName(axisName);
+        return pr == null ? "" : pr.getAxisLabel(this);
+    }
+
+
+    /**
+     * Creates a list of Section for the chart's Y axes.
+     *
+     * @return a list of Y axis sections.
+     */
+    protected List<AxisSection> buildYAxisSections() {
+        List<AxisSection> axisSections = new ArrayList<AxisSection>();
+
+        List<DiagramAttributes.AxisAttributes> axesAttrs = diagramAttributes.getAxesAttributes();
+
+        for (int i = 0, n = axesAttrs.size(); i < n; i++) {
+            AxisSection ySection = new AxisSection();
+            String axisName = diagramAttributes.getAxisName(i);
+            ySection.setIdentifier(axisName);
+            ySection.setLabel(getYAxisLabel(axisName));
+            ySection.setFontSize(14);
+            ySection.setFixed(false);
+
+            // XXX We are able to find better default ranges that [0,0], the
+            // only problem is, that we do NOT have a better range than [0,0]
+            // for each axis, because the initial chart will not have a dataset
+            // for each axis set!
+            ySection.setUpperRange(0d);
+            ySection.setLowerRange(0d);
+
+            axisSections.add(ySection);
+        }
+
+        return axisSections;
+    }
+
+    /**
+     * Returns the Y-Axis label of a chart at position <i>pos</i>.
+     *
+     * @return the Y-Axis label of a chart at position <i>0</i>.
+     */
+    protected String getYAxisLabel(String axisName) {
+        ChartSettings chartSettings = getChartSettings();
+        if (chartSettings == null) {
+            return getDefaultYAxisLabel(axisName);
+        }
+        AxisSection as = chartSettings.getAxisSection(axisName);
+        if (as != null) {
+            String label = as.getLabel();
+            if (label != null) {
+                return label;
+            }
+        }
+
+        return getDefaultYAxisLabel(axisName);
+    }
+
+    protected String axisIndexToName(int index) {
+        return diagramAttributes.getAxisName(index);
+    }
+
+    /** Add the acutal data to the diagram according to the processors.
+     * For every outable facets, this function is
+     * called and handles the data accordingly. */
+    @Override
+    public void doOut(
+        ArtifactAndFacet bundle,
+        ThemeDocument    theme,
+        boolean          visible
+    ) {
+        String facetName = bundle.getFacetName();
+        Facet facet = bundle.getFacet();
+
+        /* A conservative security check */
+        if (facetName == null || facet == null) {
+            /* Can't happen,.. */
+            logger.error("doOut called with null facet.");
+            return;
+        }
+
+        logger.debug("DoOut for facet: " + facetName);
+
+        boolean found = false;
+        List<Processor> prL = diagramAttributes.getProcessors();
+        for (Processor pr: prL) {
+            if (pr.canHandle(facetName)) {
+                found = true;
+                pr.doOut(this, bundle, theme, visible);
+            }
+        }
+        if (!found) {
+            logger.warn("No processor found for: " + facetName);
+            if (logger.isDebugEnabled()) {
+                logger.debug("Configured processors for this diagram are:");
+                for (Processor pr: prL) {
+                    logger.debug(pr.getClass().getName());
+                }
+            }
+        }
+    }
+
+    @Override
+    protected NumberAxis createYAxis(int index) {
+        NumberAxis axis = super.createYAxis(index);
+
+        if (diagramAttributes.getAxesAttributes().get(index).includeZero()) {
+            axis.setAutoRangeIncludesZero(true);
+        }
+        return axis;
+    }
+}

http://dive4elements.wald.intevation.org