view flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java @ 4620:4a30d1d62caf

Extracted method to calculate the zoomed bound of a chart.
author Raimund Renkert <rrenkert@intevation.de>
date Mon, 03 Dec 2012 16:55:45 +0100
parents 0eca080fc162
children 31327b3b2455
line wrap: on
line source
package de.intevation.flys.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.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.w3c.dom.Document;

import de.intevation.artifactdatabase.state.ArtifactAndFacet;
import de.intevation.flys.jfree.Bounds;
import de.intevation.flys.jfree.CollisionFreeXYTextAnnotation;
import de.intevation.flys.jfree.DoubleBounds;
import de.intevation.flys.jfree.FLYSAnnotation;
import de.intevation.flys.jfree.StyledAreaSeriesCollection;
import de.intevation.flys.jfree.StyledXYSeries;


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

    public class XYAxisDataset implements 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;

        /** Index of axis in plot. */
        protected int plotAxisIndex;

        /** Create AxisDataset. */
        public XYAxisDataset(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) {
            // Avoid merging NaNs, as they take min/max place forever.
            if (subRange == null ||
                Double.isNaN(subRange.getLowerBound()) ||
                Double.isNaN(subRange.getUpperBound())) {
                return;
            }
            if (range == null) {
                range = subRange;
                return;
            }
            range = Range.combine(range, subRange);
        }


        /** Add a dataset to internal list for this axis. */
        @Override
        public void addDataset(XYDataset dataset) {
            datasets.add(dataset);
            includeYRange(((XYSeriesCollection) dataset).getSeries(0));
        }

        /** Add a dataset, include its range. */
        public void addDataset(XYSeries series) {
            addDataset(new XYSeriesCollection(series));
        }


        /** Set Range for this axis. */
        @Override
        public void setRange(Range range) {
            this.range = range;
        }


        /** Get Range for this axis. */
        @Override
        public Range getRange() {
            return range;
        }


        /** Get Array of Datasets. */
        @Override
        public XYDataset[] getDatasets() {
            return datasets.toArray(new XYDataset[datasets.size()]);
        }


        /** Add a Dataset that describes an area. */
        public void addArea(StyledAreaSeriesCollection series) {
            this.datasets.add(series);
            List<?> allSeries = series.getSeries();
            for (int n = 0; n < allSeries.size(); n++) {
                includeYRange((XYSeries)allSeries.get(n));
            }
        }

        /** True if to be rendered as area. */
        @Override
        public boolean isArea(XYDataset series) {
            return (series instanceof StyledAreaSeriesCollection);
        }

        /** Adjust range to include given dataset. */
        public void includeYRange(XYSeries dataset) {
            mergeRanges(new Range(dataset.getMinY(), dataset.getMaxY()));
        }

        /** True if no datasets given. */
        @Override
        public boolean isEmpty() {
            return this.datasets.isEmpty();
        }

        /** Set the 'real' axis index that this axis is mapped to. */
        @Override
        public void setPlotAxisIndex(int axisIndex) {
            this.plotAxisIndex = axisIndex;
        }

        /** Get the 'real' axis index that this axis is mapped to. */
        @Override
        public int getPlotAxisIndex() {
            return this.plotAxisIndex;
        }
    } // class AxisDataset

    /** Enumerator over existing axes. */
    @Override
    protected abstract YAxisWalker getYAxisWalker();

    public static final int AXIS_SPACE = 5;

    /** The logger that is used in this generator. */
    private static Logger logger = Logger.getLogger(XYChartGenerator.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;

    public XYChartGenerator() {
        super();

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


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

        JFreeChart chart = ChartFactory.createXYLineChart(
            getChartTitle(),
            getXAxisLabel(),
            getYAxisLabel(0),
            null,
            PlotOrientation.VERTICAL,
            isLegendVisible(),
            false,
            false);

        XYPlot plot = (XYPlot) chart.getPlot();
        plot.setDomainAxis(createXAxis(getXAxisLabel()));

        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);
        autoZoom(plot);

        //debugAxis(plot);

        // These have to go after the autozoom.
        addAnnotationsToRenderer(plot);

        // 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:
         XYChartGenerator.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 = XYChartGenerator.class.getResource("/images/intevation.png");
        }
        else { // TODO else if ...
            imageURL = XYChartGenerator.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 XYAxisDataset for index: " + idx);
        return new XYAxisDataset(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 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;
        }

        XYAxisDataset axisDataset = (XYAxisDataset) getAxisDataset(index);

        if (visible) {
            axisDataset.addArea(area);
        }
        else {
            // TODO only range merging.
        }
        //TODO range merging.
    }


    /**
     * 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  ('symbolic') index of the series and of its 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);

        XYAxisDataset axisDataset = (XYAxisDataset) getAxisDataset(index);

        if (!visible) {
            // Do this also when not visible to have axis scaled by default such
            // that every data-point could be seen (except for annotations).
            axisDataset.includeYRange(series);
        }
    }


    /**
     * 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,
        Document   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.");
        }

        FLYSAnnotation annotations = new FLYSAnnotation(null, null, null, theme);
        annotations.setTextAnnotations(xy);

        // Do not generate second legend entry. (null was passed for the aand before).
        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;
    }


}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org