view artifacts/src/main/java/org/dive4elements/river/exports/DiagramGenerator.java @ 7076:7f600001c807 generator-refactoring

Add LTR inversion code to diagram generator. This code is used in serveral diagrams and as it modifies a whole diagram it should be central. (This should also make maintenance easier). This function can be called by processors to make sure that their data is plotted with an LTR waterflow.
author Andre Heinecke <aheinecke@intevation.de>
date Fri, 20 Sep 2013 16:33:22 +0200
parents 726d998dce29
children 3c4efd4b2c19
line wrap: on
line source
/* 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.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.annotations.XYTextAnnotation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.axis.LogarithmicAxis;
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;
import org.json.JSONArray;
import org.json.JSONException;

import org.dive4elements.artifactdatabase.state.ArtifactAndFacet;
import org.dive4elements.artifactdatabase.state.Facet;
import org.dive4elements.river.artifacts.D4EArtifact;
import org.dive4elements.river.artifacts.model.WKms;
import org.dive4elements.river.exports.process.Processor;
import org.dive4elements.river.jfree.AxisDataset;
import org.dive4elements.river.jfree.AnnotationHelper;
import org.dive4elements.river.jfree.Bounds;
import org.dive4elements.river.jfree.CollisionFreeXYTextAnnotation;
import org.dive4elements.river.jfree.DoubleBounds;
import org.dive4elements.river.jfree.RiverAnnotation;
import org.dive4elements.river.jfree.StyledAreaSeriesCollection;
import org.dive4elements.river.jfree.StyledXYSeries;
import org.dive4elements.river.themes.ThemeDocument;
import org.dive4elements.river.utils.DataUtil;

import org.w3c.dom.Element;

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

    public DiagramGenerator() {
        super();

        xBounds  = new HashMap<Integer, Bounds>();
        yBounds  = new HashMap<Integer, Bounds>();
    }

    @Override
    public void setup(Element config) {
        logger.debug("DiagramGenerator.setup");
        diagramAttributes = new DiagramAttributes(config);
    }

    /**
     * Generate the chart anew (including localized axis and all).
     */
    @Override
    public JFreeChart generateChart() {
        logger.debug("DiagramGenerator.generateChart");

        JFreeChart chart = ChartFactory.createXYLineChart(
            getChartTitle(),
            getXAxisLabel(),
            getYAxisLabel(0),
            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;
    }


    /**
     * Return left most data points x value (on first axis).
     * Shortcut, especially to be overridden in (LS) charts where
     * axis could be inverted.
     */
    protected double getLeftX() {
        return (Double)getXBounds(0).getLower();
    }


    /**
     * Return right most data points x value (on first axis).
     * Shortcut, especially to be overridden in (LS) charts where
     * axis could be inverted.
     */
    protected double getRightX() {
        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.
     *
     * (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);
    }


    /**
     * 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);
    }


    /**
     * Do Points out.
     */
    protected void doPoints(
        Object     o,
        ArtifactAndFacet aandf,
        ThemeDocument theme,
        boolean    visible,
        int        axisIndex
    ) {
        String seriesName = aandf.getFacetDescription();
        XYSeries series = new StyledXYSeries(seriesName, theme);

        // Add text annotations for single points.
        List<XYTextAnnotation> xy = new ArrayList<XYTextAnnotation>();

        try {
            JSONArray points = new JSONArray((String) o);
            for (int i = 0, P = points.length(); i < P; i++) {
                JSONArray array = points.getJSONArray(i);
                double x    = array.getDouble(0);
                double y    = array.getDouble(1);
                String name = array.getString(2);
                boolean act = array.getBoolean(3);
                if (!act) {
                    continue;
                }
                //logger.debug(" x " + x + " y " + y );
                series.add(x, y, false);
                xy.add(new CollisionFreeXYTextAnnotation(name, x, y));
            }
        }
        catch(JSONException e){
            logger.error("Could not decode json.");
        }

        RiverAnnotation annotation = new RiverAnnotation(null, null, null, theme);
        annotation.setTextAnnotations(xy);

        // Do not generate second legend entry. (null was passed for the aand before).
        if (visible) {
            annotations.add(annotation);
        }
//        doAnnotations(annotations, null, theme, visible);
        addAxisSeries(series, axisIndex, visible);
    }


    /**
     * 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 has been inverted. */
    public void setInverted(boolean inverted) {
        this.inverted = inverted;
    }

    @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() {
        return "TODO X axis label";
/*        D4EArtifact flys = (D4EArtifact) master;

        return msg(
            I18N_XAXIS_LABEL,
            I18N_XAXIS_LABEL_DEFAULT,
            new Object[] { RiverUtils.getRiver(flys).getName() }); */
    }

    @Override
    protected String getDefaultYAxisLabel(int index) {
        return "TODO Y Axis label";
/*        String label = "default";

        if (index == YAXIS.W.idx) {
            label = getWAxisLabel();
        }
        else if (index == YAXIS.Q.idx) {
            label = msg(getQAxisLabelKey(), getQAxisDefaultLabel());
        }
        else if (index == YAXIS.D.idx) {
            label = msg(I18N_WDIFF_YAXIS_LABEL, I18N_WDIFF_YAXIS_LABEL_DEFAULT);
        }

        return label;*/
    }


    /**
     * 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();
            ySection.setIdentifier(diagramAttributes.getAxisName(i));
            ySection.setLabel(getYAxisLabel(i));
            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;
    }

    protected String axisIndexToName(int index) {
        return diagramAttributes.getAxisName(index);
    }

    /** Guess if the axis should be inverted to ensure ltr diagram water flow.
     *
     * A processor should decide if it is appropiate to activate this
     * handling in a diagram by calling this function.
     *
     * Merke: In Deutschland fliesst Wasser in Diagrammen immer von
     * links nach rechts!!!
     */
    public void handleLTRWaterFlowInversion(WKms wkms)
    {
        boolean wsUp = wkms.guessWaterIncreasing();
        boolean kmUp = DataUtil.guessWaterIncreasing(wkms.allKms());
        int size = wkms.size();
        boolean inv = ((wsUp && kmUp) || (!wsUp && !kmUp)) && size > 1;

        if (logger.isDebugEnabled()) {
            logger.debug("handleLTRWaterFlowInversion: (Wkms)Values  : " + size);
            if (size > 0) {
                logger.debug("Start km: " + wkms.getKm(0));
                logger.debug("End   km: " + wkms.getKm(size-1));
            }
            logger.debug("wsUp: " + wsUp);
            logger.debug("kmUp: " + kmUp);
            if (size == 1) {
                logger.debug("Not inverting because we have just one km");
        }
            logger.debug("inv:  " + inv);
        }
        setInverted(inv);
    }

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

        for (Processor pr: diagramAttributes.getProcessors()) {
            if (pr.canHandle(facetName)) {
                pr.doOut(this, bundle, theme, visible);
            }
        }
    }
}

http://dive4elements.wald.intevation.org