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

Bugfix: #68 Set number format of chart plot axes based on the CallMeta instance for each request. flys-artifacts/trunk@2832 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Ingo Weinzierl <ingo.weinzierl@intevation.de>
date Tue, 27 Sep 2011 10:06:19 +0000
parents 0221451a24fe
children 69929c471646
line wrap: on
line source
package de.intevation.flys.exports;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Stroke;

import java.io.IOException;

import java.text.NumberFormat;

import org.apache.log4j.Logger;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

import org.jfree.ui.RectangleInsets;

import de.intevation.flys.exports.ChartExportHelper;


/**
 * An abstract base class for creating XY charts.
 *
 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
 */
public abstract class XYChartGenerator extends ChartGenerator {

    /** The logger that is used in this generator. */
    private static Logger logger = Logger.getLogger(XYChartGenerator.class);


    /** SeriesCollection used for the first axis. */
    protected XYSeriesCollection first;

    /** SeriesCollection used for the second axis. */
    protected XYSeriesCollection second;

    public static final Color DEFAULT_GRID_COLOR      = Color.GRAY;
    public static final float DEFAULT_GRID_LINE_WIDTH = 0.3f;


    /**
     * Returns the title of a chart.
     *
     * @return the title of a chart.
     */
    protected abstract String getChartTitle();

    /**
     * Returns the X-Axis label of a chart.
     *
     * @return the X-Axis label of a chart.
     */
    protected abstract String getXAxisLabel();

    /**
     * Returns the Y-Axis label of a chart.
     *
     * @return the Y-Axis label of a chart.
     */
    protected abstract String getYAxisLabel();


    public void generate()
    throws IOException
    {
        logger.debug("XYChartGenerator.generate");

        JFreeChart chart = generateChart();

        int[] size = getSize();

        ChartExportHelper.exportImage(
            out,
            chart,
            "png",
            size[0], size[1]);
    }


    public JFreeChart generateChart() {
        logger.debug("XYChartGenerator.generateChart");

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

        chart.setBackgroundPaint(Color.WHITE);
        chart.getPlot().setBackgroundPaint(Color.WHITE);

        XYPlot plot = (XYPlot) chart.getPlot();

        addDatasets(plot);
        addSubtitles(chart);
        adjustPlot(plot);
        adjustAxes(plot);
        localizeAxes(plot);

        removeEmptyRangeAxes(plot);

        autoZoom(plot);

        applyThemes(plot);

        return chart;
    }


    protected void addDatasets(XYPlot plot) {
        if (first != null) {
            logger.debug("Set the first axis dataset.");
            plot.setDataset(0, first);
        }
        if (second != null) {
            logger.debug("Set the second axis dataset.");
            plot.setDataset(1, second);
        }
    }


    public void addFirstAxisSeries(XYSeries series) {
        if (first == null) {
            first = new XYSeriesCollection();
        }

        if (series != null) {
            first.addSeries(series);
        }
    }


    public void addSecondAxisSeries(XYSeries series) {
        if (second == null) {
            second = new XYSeriesCollection();
        }

        if (series != null) {
            second.addSeries(series);
        }
    }


    private void removeEmptyRangeAxes(XYPlot plot) {
        if (first == null) {
            plot.setRangeAxis(0, null);
        }

        if (second == null) {
            plot.setRangeAxis(1, null);
        }
    }


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

        for (int i = 0, num = plot.getDatasetCount(); i < num; i++) {
            XYDataset dataset = plot.getDataset(i);

            if (dataset == null) {
                continue;
            }

            Range[]   ranges  = getRangesForDataset(dataset);

            if (i == 0) {
                ValueAxis xaxis = plot.getDomainAxis();
                zoomX(plot, xaxis, ranges[0], xrange);
            }

            ValueAxis yaxis = plot.getRangeAxis(i);

            if (yaxis == null) {
                continue;
            }

            zoomY(plot, yaxis, ranges[1], yrange);
        }
    }


    protected boolean zoomX(XYPlot plot, ValueAxis axis, Range range, Range x) {
        return zoom(plot, axis, range, x);
    }


    protected boolean zoomY(XYPlot plot, ValueAxis axis, Range range, Range x) {
        return zoom(plot, axis, range, 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 range 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, Range range, Range x) {
        if (x != null) {
            double min  = range.getLowerBound();
            double max  = range.getUpperBound();
            double diff = max > min ? max - min : min - max;

            Range computed = new Range(
                min + x.getLowerBound() * diff,
                min + x.getUpperBound() * diff);

            axis.setRangeWithMargins(computed);

            logger.debug("Zoom axis to: " + computed);

            return true;
        }

        axis.setRangeWithMargins(range);
        return false;
    }


    /**
     * This method extracts the minimum and maximum values for x and y axes.
     *
     * @param dataset The dataset that should be observed.
     *
     * @return a Range[] as follows: [x-Range, y-Range].
     */
    public static Range[] getRangesForDataset(XYDataset dataset) {
        double[] xr = new double[] { Double.MAX_VALUE, -Double.MAX_VALUE };
        double[] yr = new double[] { Double.MAX_VALUE, -Double.MAX_VALUE };

        if (dataset != null) {
            int sCount = dataset.getSeriesCount();

            for (int i = 0; i < sCount; i++) {
                int iCount = dataset.getItemCount(i);
    
                for (int j = 0; j < iCount; j++) {
                    double x = dataset.getX(i, j).doubleValue();
                    double y = dataset.getY(i, j).doubleValue();
    
                    if (!Double.isNaN(x)) {
                        xr[0] = xr[0] < x ? xr[0] : x;
                        xr[1] = xr[1] > x ? xr[1] : x;
                    }
    
                    if (!Double.isNaN(y)) {
                        yr[0] = yr[0] < y ? yr[0] : y;
                        yr[1] = yr[1] > y ? yr[1] : y;
                    }
                }
            }
        }

        // this is only required, if there are no items in the dataset.
        xr[0] = xr[0] < xr[1] ? xr[0] : xr[1];
        xr[1] = xr[1] > xr[0] ? xr[1] : xr[0];
        yr[0] = yr[0] < yr[1] ? yr[0] : yr[1];
        yr[1] = yr[1] > yr[0] ? yr[1] : yr[0];

        return new Range[] {new Range(xr[0], xr[1]), new Range(yr[0], yr[1])};
    }


    /**
     * Adjusts the axes of a plot.
     *
     * @param plot The XYPlot of the chart.
     */
    protected void adjustAxes(XYPlot plot) {
        NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();

        yAxis.setAutoRangeIncludesZero(false);
    }


    protected void adjustPlot(XYPlot plot) {
        Stroke gridStroke = new BasicStroke(
            DEFAULT_GRID_LINE_WIDTH,
            BasicStroke.CAP_BUTT,
            BasicStroke.JOIN_MITER,
            3.0f,
            new float[] { 3.0f },
            0.0f);

        plot.setDomainGridlineStroke(gridStroke);
        plot.setDomainGridlinePaint(DEFAULT_GRID_COLOR);
        plot.setDomainGridlinesVisible(true);

        plot.setRangeGridlineStroke(gridStroke);
        plot.setRangeGridlinePaint(DEFAULT_GRID_COLOR);
        plot.setRangeGridlinesVisible(true);

        plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));

        if (plot.getDataset(0) != null) {
            plot.mapDatasetToRangeAxis(0, 0);
        }

        if (plot.getDataset(1) != null) {
            plot.mapDatasetToRangeAxis(1, 1);
        }
    }


    protected void addSubtitles(JFreeChart chart) {
        // override this method in subclasses that need subtitles
    }


    /**
     * 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 domainAxis The domain axis that needs localization.
     */
    protected void localizeRangeAxis(ValueAxis rangeAxis) {
        NumberFormat nf = NumberFormat.getInstance(getLocale());
        ((NumberAxis) rangeAxis).setNumberFormatOverride(nf);
    }


    protected void applyThemes(XYPlot plot) {
        if (first != null) {
            applyThemes(plot, first, 0);
        }

        if (second != null) {
            applyThemes(plot, second, 1);
        }
    }


    protected void applyThemes(XYPlot plot, XYSeriesCollection dataset, int i) {
        XYLineAndShapeRenderer r = getRenderer(plot, i);

        for (int s = 0, num = dataset.getSeriesCount(); s < num; s++) {
            XYSeries series = dataset.getSeries(s);

            if (series instanceof StyledXYSeries) {
                ((StyledXYSeries) series).applyTheme(r, s);
            }
        }

        plot.setRenderer(i, r);
    }


    protected XYLineAndShapeRenderer getRenderer(XYPlot plot, int idx) {
        XYLineAndShapeRenderer r =
            (XYLineAndShapeRenderer) plot.getRenderer(idx);

        if (r != null) {
            return r;
        }

        if (idx == 0) {
            logger.warn("No default renderer set!");
            return new XYLineAndShapeRenderer();
        }

        r = (XYLineAndShapeRenderer) plot.getRenderer(0);

        try {
            return (XYLineAndShapeRenderer) r.clone();
        }
        catch (CloneNotSupportedException cnse) {
            logger.warn(cnse, cnse);
        }

        logger.warn("No applicalable renderer found!");

        return new XYLineAndShapeRenderer();
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org