view flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java @ 1934:e4a516ca2e86

When having axis in diagrams, always have one on the left. flys-artifacts/trunk@3315 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author Felix Wolfsteller <felix.wolfsteller@intevation.de>
date Fri, 25 Nov 2011 08:42:44 +0000
parents 17e18948fe5e
children 5b51f5232661
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.TreeMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;

import org.w3c.dom.Document;

import org.apache.log4j.Logger;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.axis.AxisLocation;
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.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.data.xy.XYDataset;

import org.jfree.ui.RectangleInsets;

import de.intevation.artifactdatabase.state.Facet;

import de.intevation.flys.exports.ChartExportHelper;
import de.intevation.flys.jfree.FLYSAnnotation;
import de.intevation.flys.jfree.StickyAxisAnnotation;

import de.intevation.flys.utils.ThemeAccess;

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

    /** Map of datasets ("index"). */
    protected SortedMap<Integer, List<XYDataset>> datasets;

    /** List of annotations to insert in plot. */
    protected List<FLYSAnnotation> annotations;

    /** The max X range to include all X values of all series for each axis. */
    protected Map<Integer, Range> xRanges;

    /** The max Y range to include all Y values of all series for each axis. */
    protected Map<Integer, Range> yRanges;

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


    public XYChartGenerator() {
        xRanges  = new HashMap<Integer, Range>();
        yRanges  = new HashMap<Integer, Range>();
        datasets = new TreeMap<Integer, List<XYDataset>>();
    }


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

        String format = getFormat();
        int[]  size   = getSize();

        context.putContextValue("chart.width",  size[0]);
        context.putContextValue("chart.height", size[1]);

        if (format.equals(ChartExportHelper.FORMAT_PNG)) {
            context.putContextValue("chart.image.format", "png");

            ChartExportHelper.exportImage(
                out,
                chart,
                context);
        }
        else if (format.equals(ChartExportHelper.FORMAT_PDF)) {
            context.putContextValue("chart.marginLeft",   5f);
            context.putContextValue("chart.marginRight",  5f);
            context.putContextValue("chart.marginTop",    5f);
            context.putContextValue("chart.marginBottom", 5f);
            context.putContextValue(
                "chart.page.format",
                ChartExportHelper.DEFAULT_PAGE_SIZE);

            ChartExportHelper.exportPDF(
                out,
                chart,
                context);
        }
        else if (format.equals(ChartExportHelper.FORMAT_SVG)) {
            context.putContextValue(
                "chart.encoding",
                ChartExportHelper.DEFAULT_ENCODING);

            ChartExportHelper.exportSVG(
                out,
                chart,
                context);
        }
    }


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

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

        XYPlot plot = (XYPlot) chart.getPlot();
        chart.setBackgroundPaint(Color.WHITE);
        plot.setBackgroundPaint(Color.WHITE);

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

        removeEmptyRangeAxes(plot);
        createAxes(plot);
        adjustAxes(plot);

        preparePointRanges(plot);
        autoZoom(plot);

        applyThemes(plot);

        return chart;
    }


    /**
     * Add datasets to plot.
     * @param plot plot to add datasets to.
     */
    protected void addDatasets(XYPlot plot) {
        int count = 0;
        for (Map.Entry<Integer, List<XYDataset>> entry: datasets.entrySet()) {
            List<Integer> axisList = new ArrayList<Integer>(1);
            axisList.add(entry.getKey());
            for (XYDataset dataset: entry.getValue()) {
                int index = count++;
                plot.setDataset(index, dataset);
                plot.mapDatasetToRangeAxes(index, axisList);
            }
        }
    }


    /**
     * Add given series.
     * @param series the dataseries to include in plot.
     * @param index  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;
        }

        if (visible) {
            XYSeriesCollection collection = new XYSeriesCollection(series);

            List<XYDataset> dataset = datasets.get(index);

            if (dataset == null) {
                dataset = new ArrayList<XYDataset>();
                datasets.put(index, dataset);
            }

            dataset.add(collection);
        }

        // Do this also when not visible to have axis scaled by default such
        // that every data-point could be seen (except for annotations).
        combineXRanges(new Range(series.getMinX(), series.getMaxX()), 0);
        combineYRanges(new Range(series.getMinY(), series.getMaxY()), index);
    }

    /**
     * Effect: extend range of x axis to include given limits.
     * @param range the given ("minimal") range.
     * @param index index of axis to be merged.
     */
    private void combineXRanges(Range range, int index) {

        Range old = xRanges.get(index);

        if (old != null) {
            range = Range.combine(old, range);
        }

        xRanges.put(index, range);
    }


    /**
     * @param range the new range.
     */
    private void combineYRanges(Range range, int index) {

        Range old = yRanges.get(index);

        if (old != null) {
            range = Range.combine(old, range);
        }

        yRanges.put(index, range);
    }


    /**
     * Adds annotations to list (if visible is true).
     */
    public void addAnnotations(FLYSAnnotation annotation, boolean visible) {
        if (!visible) {
            return;
        }

        if (annotations == null) {
            annotations = new ArrayList<FLYSAnnotation>();
        }

        annotations.add(annotation);
    }


    /**
     * Create y-axes, ensure that the first axis (with data) is on the left.
     */
    public void createAxes(XYPlot plot) {
        logger.debug("XYChartGenerator.createAxes");

        Integer last = datasets.lastKey();
        int i            = 0;
        int firstVisible = 0;

        if (last != null) {
            firstVisible = i = last;
            for (; i >= 0; --i) {
                if (datasets.containsKey(i)) {
                    plot.setRangeAxis(i, createYAxis(i));
                    firstVisible = i;
                }
            }
            plot.setRangeAxisLocation(firstVisible, AxisLocation.TOP_OR_LEFT);
        }
    }

    /**
     * Create Y (range) axis for given index.
     * Shall be overriden by subclasses.
     */
    protected NumberAxis createYAxis(int index) {
        NumberAxis axis = new NumberAxis("default axis");
        return axis;
    }

    private void removeEmptyRangeAxes(XYPlot plot) {
        Integer last = datasets.lastKey();

        if (last != null) {
            for (int i = last-1; i >= 0; --i) {
                if (!datasets.containsKey(i)) {
                    plot.setRangeAxis(i, null);
                }
            }
        }
    }


    /**
     * Expands X and Y axes if only a point is shown.
     */
    private void preparePointRanges(XYPlot plot) {
        for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
            Integer key = Integer.valueOf(i);

            Range r = xRanges.get(key);
            if (r != null && r.getLowerBound() == r.getUpperBound()) {
                xRanges.put(key, expandRange(r, 5));
            }
        }

        for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
            Integer key = Integer.valueOf(i);

            Range r = yRanges.get(key);
            if (r != null && r.getLowerBound() == r.getUpperBound()) {
                yRanges.put(key, expandRange(r, 5));
            }
        }
    }


    /**
     * Expand range by percent.
     */
    public static Range expandRange(Range range, double percent) {
        if (range == null) {
            return null;
        }

        double value  = range.getLowerBound();
        double expand = value / 100 * percent;

        return expand != 0
            ? new Range(value-expand, value+expand)
            : new Range(-0.01 * percent, 0.01 * percent);
    }


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

        zoomX(plot, plot.getDomainAxis(), xRanges.get(0), xrange);

        for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
            ValueAxis yaxis = plot.getRangeAxis(i);

            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, yRanges.get(Integer.valueOf(i)), 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 (range == null) {
            return false;
        }

        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
     * 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].
     */
    public Range[] getRangesForDataset(int index) {
        return new Range[] {
            xRanges.get(Integer.valueOf(0)),
            yRanges.get(Integer.valueOf(index))
        };
    }


    protected void addAnnotations(XYPlot plot) {
        plot.clearAnnotations();

        if (annotations == null) {
            logger.debug("No Annotations given.");
            return;
        }

        LegendItemCollection lic = new LegendItemCollection();

        int idx = 0;
        if (plot.getRangeAxis(idx) == null && plot.getRangeAxisCount() >= 2) {
            idx = 1;
        }

        XYItemRenderer renderer = plot.getRenderer(idx);

        for (FLYSAnnotation fa: annotations) {
            Document theme = fa.getTheme();

            ThemeAccess themeAccess = new ThemeAccess(theme);

            Color color   = themeAccess.parseLineColorField();
            int lineWidth = themeAccess.parseLineWidth();
            lic.add(new LegendItem(fa.getLabel(), color));

            for (XYTextAnnotation ta: fa.getAnnotations()) {
                if(ta instanceof StickyAxisAnnotation) {
                    StickyAxisAnnotation sta = (StickyAxisAnnotation)ta;
                    sta.applyTheme(themeAccess);
                    renderer.addAnnotation(sta);
                }
                else {
                    ta.setPaint(color);
                    ta.setOutlineStroke(new BasicStroke((float) lineWidth));
                    renderer.addAnnotation(ta);
                }
            }

            // TODO Do after loop?
            plot.setFixedLegendItems(lic);
        }
    }


    /**
     * Adjusts the axes of a plot (the first axis does not include zero).
     *
     * @param plot The XYPlot of the chart.
     */
    protected void adjustAxes(XYPlot plot) {
        NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
        if (yAxis == null) {
            logger.warn("No Axis to setAutoRangeIncludeZero.");
        }
        else {
            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));
    }


    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) {
        int idx = 0;

        for (Map.Entry<Integer, List<XYDataset>> entry: datasets.entrySet()) {
            for (XYDataset dataset: entry.getValue()) {
                if (dataset instanceof XYSeriesCollection) {
                    idx = applyThemes(plot, (XYSeriesCollection)dataset, idx);
                }
            }
        }
    }


    /**
     * @param idx "index" of dataset/series (first dataset to be drawn has
     *            index 0), correlates with renderer index.
     * @return idx increased by number of items addded.
     */
    protected int applyThemes(XYPlot plot, XYSeriesCollection series, int idx) {
        LegendItemCollection lic  = new LegendItemCollection();
        LegendItemCollection anno = plot.getFixedLegendItems();

        XYLineAndShapeRenderer renderer = getRenderer(plot, idx);
        int retidx = idx;

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

            if (serie instanceof StyledXYSeries) {
                ((StyledXYSeries) serie).applyTheme(renderer, s);
            }

            // special case: if there is just one single item, we need to enable
            // points for this series, otherwise we would not see anything in
            // the chart area.
            if (serie.getItemCount() == 1) {
                renderer.setSeriesShapesVisible(s, true);
            }

            LegendItem li = renderer.getLegendItem(idx, s);
            if (li != null) {
                lic.add(li);
            }
            else {
                logger.warn("Could not get LegentItem for renderer: "
                     + idx + ", series-idx " + s);
            }
            retidx++;
        }

        if (anno != null) {
            lic.addAll(anno);
        }

        plot.setFixedLegendItems(lic);

        plot.setRenderer(idx, renderer);

        return retidx;
    }


    /**
     * Get renderer, from plot or cloned default renderer otherwise.
     */
    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();
    }


    /**
     * Register annotations like MainValues for later plotting
     *
     * @param o     list of annotations (data of facet).
     * @param facet The facet. This facet does NOT support any data objects. Use
     * FLYSArtifact.getNativeFacet() instead to retrieve a Facet which supports
     * data.
     * @param theme   Theme document for given annotations.
     * @param visible The visibility of the annotations.
     */
    protected void doAnnotations(
        FLYSAnnotation annotations,
        Facet facet,
        Document theme,
        boolean visible
    ){
        logger.debug("doAnnotations");

        // Add all annotations to our annotation pool.
        annotations.setTheme(theme);
        annotations.setLabel(facet.getDescription());
        addAnnotations(annotations, visible);
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org