ingo@369: package de.intevation.flys.exports;
ingo@369:
ingo@419: import java.awt.BasicStroke;
ingo@369: import java.awt.Color;
felix@2020: import java.awt.Paint;
ingo@419: import java.awt.Stroke;
felix@2020: import java.awt.TexturePaint;
felix@2020:
felix@2020: import java.awt.geom.Rectangle2D;
felix@2020:
felix@2020: import java.awt.image.BufferedImage;
ingo@369:
ingo@369: import java.io.IOException;
ingo@369:
ingo@1645: import java.text.NumberFormat;
ingo@1645:
ingo@1679: import java.util.ArrayList;
ingo@1684: import java.util.HashMap;
felix@1931: import java.util.TreeMap;
ingo@1679: import java.util.List;
ingo@1684: import java.util.Map;
felix@1931: import java.util.SortedMap;
ingo@1679:
ingo@1679: import org.w3c.dom.Document;
ingo@1679:
ingo@369: import org.apache.log4j.Logger;
ingo@369:
ingo@369: import org.jfree.chart.ChartFactory;
ingo@369: import org.jfree.chart.JFreeChart;
ingo@1679: import org.jfree.chart.LegendItem;
ingo@1679: import org.jfree.chart.LegendItemCollection;
ingo@1679: import org.jfree.chart.annotations.XYTextAnnotation;
ingo@369: import org.jfree.chart.axis.NumberAxis;
ingo@652: import org.jfree.chart.axis.ValueAxis;
ingo@369: import org.jfree.chart.plot.PlotOrientation;
ingo@369: import org.jfree.chart.plot.XYPlot;
ingo@1679: import org.jfree.chart.renderer.xy.XYItemRenderer;
ingo@924: import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
ingo@652: import org.jfree.data.Range;
ingo@923: import org.jfree.data.xy.XYSeries;
ingo@923: import org.jfree.data.xy.XYSeriesCollection;
felix@1931: import org.jfree.data.xy.XYDataset;
ingo@654:
ingo@654: import org.jfree.ui.RectangleInsets;
ingo@369:
felix@1849: import de.intevation.artifactdatabase.state.Facet;
felix@2005: import de.intevation.flys.jfree.StableXYDifferenceRenderer;
ingo@1990: import de.intevation.artifactdatabase.state.Section;
ingo@1986: import de.intevation.artifactdatabase.state.Settings;
felix@1849:
felix@2005:
ingo@369: import de.intevation.flys.exports.ChartExportHelper;
ingo@1679: import de.intevation.flys.jfree.FLYSAnnotation;
raimund@1738: import de.intevation.flys.jfree.StickyAxisAnnotation;
ingo@369:
sascha@1754: import de.intevation.flys.utils.ThemeAccess;
ingo@369:
ingo@369: /**
ingo@369: * An abstract base class for creating XY charts.
ingo@369: *
felix@1940: * With respect to datasets, ranges and axis, there are following requirements:
felix@1940: *
felix@1940: * - First in, first drawn: "Early" datasets should be of lower Z-Oder
felix@1940: * than later ones (only works per-axis).
felix@1940: * - Visible axis should initially show the range of all datasets that
felix@1940: * show data for this axis (even invisible ones). Motivation: Once
felix@1940: * a dataset (theme) has been activated, it should be on screen.
felix@1940: * - There should always be a Y-Axis on the "left".
felix@1940: *
felix@1940: *
ingo@369: * @author Ingo Weinzierl
ingo@369: */
ingo@369: public abstract class XYChartGenerator extends ChartGenerator {
ingo@369:
felix@2020: // TODO Consider storing the renderer here.
felix@1940: private class AxisDataset {
felix@1940: /** Symbolic integer, but also coding the priority (0 goes first). */
felix@1940: protected int axisSymbol;
felix@1940: /** List of assigned datasets (in order). */
felix@1940: protected List datasets;
felix@1940: /** Range to use to include all given datasets. */
felix@1940: protected Range range;
felix@1940:
felix@1940: /** Create AxisDataset. */
felix@1940: public AxisDataset(int symb) {
felix@1940: this.axisSymbol = symb;
felix@1940: datasets = new ArrayList();
felix@1940: }
felix@1940:
felix@1940: /** Merge (or create given range with range so far (if any). */
felix@1940: private void mergeRanges(Range subRange) {
felix@1959: // Avoid merging NaNs, as they take min/max place forever.
felix@1959: if (subRange == null ||
felix@1959: Double.isNaN(subRange.getLowerBound()) ||
felix@1959: Double.isNaN(subRange.getUpperBound())) {
felix@1959: return;
felix@1959: }
felix@1940: if (range == null) {
felix@1940: range = subRange;
felix@1940: return;
felix@1940: }
felix@1940: range = Range.combine(range, subRange);
felix@1940: }
felix@1940:
felix@1940: /** Add a dataset, include its range. */
felix@1940: public void addDataset(XYSeries dataset) {
felix@1940: this.datasets.add(new XYSeriesCollection(dataset));
felix@1958: includeYRange(dataset);
felix@1940: }
felix@1940:
felix@2020: public void addArea(StyledAreaSeriesCollection series) {
felix@2020: this.datasets.add(series);
felix@2005: }
felix@2005:
felix@2005: /** True if to be renedered as area. */
felix@2005: public boolean isArea(XYSeriesCollection series) {
felix@2020: return (series instanceof StyledAreaSeriesCollection);
felix@2005: }
felix@2005:
felix@1940: /** Adjust range to include given dataset. */
felix@1958: public void includeYRange(XYSeries dataset) {
felix@1940: mergeRanges(new Range(dataset.getMinY(), dataset.getMaxY()));
felix@1940: }
felix@1940:
felix@1940: /** True if no datasets given. */
felix@1940: public boolean isEmpty() {
felix@1940: return this.datasets.isEmpty();
felix@1940: }
felix@2005: } // class AxisDataset
felix@1940:
felix@1940:
ingo@2000: /**
ingo@2000: * A mini interface that allows to walk over the YAXIS enums defined in
ingo@2000: * subclasses.
ingo@2000: */
ingo@2000: public interface YAxisWalker {
ingo@2000: int length();
ingo@2000: String getId(int idx);
ingo@2000: }
ingo@2000:
ingo@2000:
felix@2020: /** Override to make axis information available. */
ingo@2000: protected YAxisWalker getYAxisWalker() {
ingo@2000: return new YAxisWalker() {
felix@2020: /** Get number of items. */
ingo@2000: @Override
ingo@2000: public int length() {
ingo@2000: return 0;
ingo@2000: }
ingo@2000:
felix@2020: /** Get identifier for this index. */
ingo@2000: @Override
ingo@2000: public String getId(int idx) {
ingo@2000: return null;
ingo@2000: }
ingo@2000: };
ingo@2000: }
ingo@2000:
ingo@2000:
felix@1048: /** The logger that is used in this generator. */
ingo@654: private static Logger logger = Logger.getLogger(XYChartGenerator.class);
ingo@369:
felix@1931: /** Map of datasets ("index"). */
felix@1940: protected SortedMap datasets;
ingo@923:
ingo@1679: /** List of annotations to insert in plot. */
ingo@1679: protected List annotations;
ingo@1679:
felix@1685: /** The max X range to include all X values of all series for each axis. */
ingo@1684: protected Map xRanges;
ingo@1684:
felix@1685: /** The max Y range to include all Y values of all series for each axis. */
ingo@1684: protected Map yRanges;
ingo@1684:
ingo@419: public static final Color DEFAULT_GRID_COLOR = Color.GRAY;
ingo@419: public static final float DEFAULT_GRID_LINE_WIDTH = 0.3f;
ingo@419:
ingo@419:
felix@1931: public XYChartGenerator() {
felix@1931: xRanges = new HashMap();
felix@1931: yRanges = new HashMap();
felix@1940: datasets = new TreeMap();
felix@1931: }
felix@1931:
felix@1931:
ingo@369: /**
ingo@369: * Returns the title of a chart.
ingo@369: *
ingo@369: * @return the title of a chart.
ingo@369: */
ingo@369: protected abstract String getChartTitle();
ingo@369:
felix@1940:
ingo@369: /**
ingo@1989: * This method always returns null. If a concrete subclass of this class
ingo@1989: * requires a chart subtitle, this subclass can easily override this method.
ingo@1989: *
ingo@1989: * @return always null.
ingo@1989: */
ingo@1989: protected String getChartSubtitle() {
ingo@1989: // overridden this method in subclasses that need subtitles
ingo@1989: return null;
ingo@1989: }
ingo@1989:
ingo@1989:
ingo@1989: /**
ingo@1990: * This method is used to determine, if the chart's legend is visible or
ingo@1990: * not. Note: this method always returns true.
ingo@1990: *
ingo@1990: * @return true, if the legend should be visible, otherwise false.
ingo@1990: */
ingo@1990: protected boolean isLegendVisible() {
ingo@1990: return true;
ingo@1990: }
ingo@1990:
ingo@1990:
ingo@1990: /**
ingo@1990: * This method is used to determine the font size of the chart's legend.
ingo@1990: * Note: this method always returns 12.
ingo@1990: *
ingo@1990: * @return 12.
ingo@1990: */
ingo@1990: protected int getLegendFontSize() {
ingo@1990: return 12;
ingo@1990: }
ingo@1990:
ingo@1990:
ingo@1990: /**
ingo@1989: * This method is used to determine if the resulting chart should display
ingo@1989: * grid lines or not. Note: this method always returns true!
ingo@1989: *
ingo@1989: * @return true, if the chart should display grid lines, otherwise false.
ingo@1989: */
ingo@1989: protected boolean isGridVisible() {
ingo@1989: return true;
ingo@1989: }
ingo@1989:
ingo@1989:
ingo@1989: /**
ingo@369: * Returns the X-Axis label of a chart.
ingo@369: *
ingo@369: * @return the X-Axis label of a chart.
ingo@369: */
ingo@369: protected abstract String getXAxisLabel();
ingo@369:
felix@1940:
ingo@369: /**
ingo@369: * Returns the Y-Axis label of a chart.
ingo@369: *
ingo@369: * @return the Y-Axis label of a chart.
ingo@369: */
ingo@369: protected abstract String getYAxisLabel();
ingo@369:
ingo@2000:
ingo@2000: /**
ingo@2000: * Returns the Y-Axis label of a chart at position pos.
ingo@2000: *
ingo@2000: * @return the Y-Axis label of a chart at position 0.
ingo@2000: */
ingo@2000: protected String getYAxisLabel(int pos) {
ingo@2000: return getYAxisLabel();
ingo@2000: }
ingo@2000:
felix@1940: /**
felix@1940: * Generate chart.
felix@1940: */
ingo@369: public void generate()
ingo@369: throws IOException
ingo@369: {
ingo@369: logger.debug("XYChartGenerator.generate");
ingo@369:
ingo@653: JFreeChart chart = generateChart();
ingo@653:
ingo@1735: String format = getFormat();
ingo@1735: int[] size = getSize();
ingo@653:
felix@1930: context.putContextValue("chart.width", size[0]);
ingo@1735: context.putContextValue("chart.height", size[1]);
ingo@1735:
ingo@1735: if (format.equals(ChartExportHelper.FORMAT_PNG)) {
ingo@1735: context.putContextValue("chart.image.format", "png");
ingo@1735:
ingo@1735: ChartExportHelper.exportImage(
ingo@1735: out,
ingo@1735: chart,
ingo@1735: context);
ingo@1735: }
ingo@1735: else if (format.equals(ChartExportHelper.FORMAT_PDF)) {
felix@1930: context.putContextValue("chart.marginLeft", 5f);
felix@1930: context.putContextValue("chart.marginRight", 5f);
felix@1930: context.putContextValue("chart.marginTop", 5f);
ingo@1735: context.putContextValue("chart.marginBottom", 5f);
ingo@1735: context.putContextValue(
ingo@1735: "chart.page.format",
ingo@1735: ChartExportHelper.DEFAULT_PAGE_SIZE);
ingo@1735:
ingo@1735: ChartExportHelper.exportPDF(
ingo@1735: out,
ingo@1735: chart,
ingo@1735: context);
ingo@1735: }
ingo@1735: else if (format.equals(ChartExportHelper.FORMAT_SVG)) {
ingo@1735: context.putContextValue(
ingo@1735: "chart.encoding",
ingo@1735: ChartExportHelper.DEFAULT_ENCODING);
ingo@1735:
ingo@1735: ChartExportHelper.exportSVG(
ingo@1735: out,
ingo@1735: chart,
ingo@1735: context);
ingo@1735: }
ingo@653: }
ingo@653:
ingo@653:
felix@1930: /**
felix@1930: * Generate the chart anew (including localized axis and all).
felix@1930: */
ingo@653: public JFreeChart generateChart() {
ingo@653: logger.debug("XYChartGenerator.generateChart");
ingo@653:
ingo@369: JFreeChart chart = ChartFactory.createXYLineChart(
ingo@369: getChartTitle(),
ingo@369: getXAxisLabel(),
ingo@369: getYAxisLabel(),
ingo@375: null,
ingo@369: PlotOrientation.VERTICAL,
ingo@369: true,
ingo@369: false,
ingo@369: false);
ingo@369:
felix@1931: XYPlot plot = (XYPlot) chart.getPlot();
ingo@369: chart.setBackgroundPaint(Color.WHITE);
felix@1931: plot.setBackgroundPaint(Color.WHITE);
ingo@414: addSubtitles(chart);
ingo@419: adjustPlot(plot);
ingo@923:
felix@1940: //debugAxis(plot);
felix@1940:
felix@1940: addDatasets(plot);
felix@1940:
felix@1940: //debugDatasets(plot);
ingo@923:
felix@1935: recoverEmptyPlot(plot);
felix@1940: preparePointRanges(plot);
felix@1935:
felix@1940: addAnnotationsToRenderer(plot);
felix@1940:
felix@1940: //debugAxis(plot);
felix@1940:
felix@1940: localizeAxes(plot);
felix@1940: adjustAxes(plot);
ingo@673: autoZoom(plot);
ingo@652:
felix@1940: return chart;
felix@1940: }
ingo@924:
felix@1940:
felix@1940: /**
felix@1940: * Put debug output about datasets.
felix@1940: */
felix@1940: public void debugDatasets(XYPlot plot) {
felix@1940: logger.debug("Number of datasets: " + plot.getDatasetCount());
felix@1940: for (int i = 0; i < plot.getDatasetCount(); i++) {
felix@1940: if (plot.getDataset(i) == null) {
felix@1940: logger.debug("Dataset #" + i + " is null");
felix@1940: continue;
felix@1940: }
felix@1940: logger.debug("Dataset #" + i + ":" + plot.getDataset(i));
felix@1958: XYSeriesCollection series = (XYSeriesCollection) plot.getDataset(i);
felix@1958: logger.debug("X-Extend of Dataset: " + series.getSeries(0).getMinX()
felix@1958: + " " + series.getSeries(0).getMaxX());
felix@1958: logger.debug("Y-Extend of Dataset: " + series.getSeries(0).getMinY()
felix@1958: + " " + series.getSeries(0).getMaxY());
felix@1940: }
felix@1940: }
felix@1940:
felix@1940:
felix@1940: /**
felix@1940: * Put debug output about axes.
felix@1940: */
felix@1940: public void debugAxis(XYPlot plot) {
felix@1940: logger.debug("...............");
felix@1940: for (int i = 0; i < plot.getRangeAxisCount(); i++) {
felix@1940: if (plot.getRangeAxis(i) == null)
felix@2005: logger.debug("Range-Axis #" + i + " == null");
felix@1940: else {
felix@2005: logger.debug("Range-Axis " + i + " != null [" +
felix@1940: plot.getRangeAxis(i).getRange().getLowerBound() +
felix@1940: " " + plot.getRangeAxis(i).getRange().getUpperBound() +
felix@1940: "]");
felix@1940: }
felix@1940:
felix@1940: }
felix@1940: logger.debug("...............");
ingo@369: }
ingo@369:
ingo@369:
felix@1685: /**
felix@1931: * Add datasets to plot.
felix@1685: * @param plot plot to add datasets to.
felix@1685: */
ingo@923: protected void addDatasets(XYPlot plot) {
felix@1940: // AxisDatasets are sorted, but some might be empty.
felix@1940: // Thus, generate numbering on the fly.
felix@1940: int axisIndex = 0;
felix@1940: int datasetIndex = 0;
felix@1940: for (Map.Entry entry: datasets.entrySet()) {
felix@1940: if (!entry.getValue().isEmpty()) {
felix@1940: // Add axis and range information.
felix@1940: AxisDataset axisDataset = entry.getValue();
felix@1940: NumberAxis axis = createYAxis(entry.getKey());
felix@1940:
felix@1940: plot.setRangeAxis(axisIndex, axis);
felix@1940: if (axis.getAutoRangeIncludesZero()) {
felix@1940: axisDataset.range = Range.expandToInclude(axisDataset.range, 0d);
felix@1940: }
felix@1940: yRanges.put(axisIndex, expandPointRange(axisDataset.range));
felix@1940:
felix@1940: // Add contained datasets, mapping to axis.
felix@1940: for (XYDataset dataset: axisDataset.datasets) {
felix@1940: plot.setDataset(datasetIndex, dataset);
felix@1940: plot.mapDatasetToRangeAxis(datasetIndex, axisIndex);
felix@2005: applyThemes(plot, (XYSeriesCollection) dataset,
felix@2005: datasetIndex,
felix@2005: axisDataset.isArea((XYSeriesCollection)dataset));
felix@1940: datasetIndex++;
felix@1940: }
felix@1940: axisIndex++;
ingo@1684: }
ingo@923: }
ingo@923: }
ingo@923:
ingo@923:
felix@1931: /**
felix@2005: * Registers an area to be drawn.
felix@2005: * @param lower the lower curve to draw the area from.
felix@2005: * @param upper the upper curve to draw the ara from.
felix@2005: */
felix@2020: public void addAreaSeries(StyledAreaSeriesCollection area, int index, boolean visible) {
felix@2020: if (area == null) {
felix@2005: logger.warn("Cannot yet render above/under curve.");
felix@2005: return;
felix@2005: }
felix@2005: AxisDataset axisDataset = datasets.get(index);
felix@2005:
felix@2005: if (axisDataset == null) {
felix@2005: axisDataset = new AxisDataset(index);
felix@2005: datasets.put(index, axisDataset);
felix@2005: }
felix@2005:
felix@2020: if (visible) {
felix@2020: axisDataset.addArea(area);
felix@2020: }
felix@2005: else {
felix@2005: // TODO only range merging.
felix@2005: }
felix@2005: //TODO range merging.
felix@2005: }
felix@2005:
felix@2020:
felix@2005: /**
felix@1935: * Add given series if visible, if not visible adjust ranges (such that
felix@1935: * all points in data would be plotted once visible).
felix@1931: * @param series the dataseries to include in plot.
felix@1931: * @param index index of the series and of its axis.
felix@1931: * @param visible whether or not the data should be plotted.
felix@1931: */
felix@1931: public void addAxisSeries(XYSeries series, int index, boolean visible) {
felix@1931: if (series == null) {
ingo@1684: return;
ingo@1684: }
ingo@1684:
felix@1940: AxisDataset axisDataset = datasets.get(index);
felix@1931:
felix@1940: if (axisDataset == null) {
felix@1940: axisDataset = new AxisDataset(index);
felix@1940: datasets.put(index, axisDataset);
ingo@1684: }
ingo@1684:
felix@2005: logger.debug("addAxisSeries: extent X " + series.getMinX() + " : " + series.getMaxX()
felix@2005: + " extent y " + series.getMinY() + " : " + series.getMaxY());
felix@1958:
felix@1940: if (visible) {
felix@1940: axisDataset.addDataset(series);
felix@1940: }
felix@1940: else {
felix@1940: // Do this also when not visible to have axis scaled by default such
felix@1940: // that every data-point could be seen (except for annotations).
felix@1958: axisDataset.includeYRange(series);
felix@1940: }
felix@1940:
felix@1931: combineXRanges(new Range(series.getMinX(), series.getMaxX()), 0);
felix@1931: }
felix@1931:
felix@1940:
felix@1931: /**
felix@1931: * Effect: extend range of x axis to include given limits.
felix@1931: * @param range the given ("minimal") range.
felix@1931: * @param index index of axis to be merged.
felix@1931: */
felix@1931: private void combineXRanges(Range range, int index) {
felix@1931:
felix@1959: if (range == null
felix@1959: || Double.isNaN(range.getLowerBound())
felix@1959: || Double.isNaN(range.getUpperBound())) {
felix@1959: return;
felix@1959: }
felix@1959:
felix@1931: Range old = xRanges.get(index);
felix@1931:
felix@1931: if (old != null) {
felix@1931: range = Range.combine(old, range);
felix@1931: }
felix@1931:
felix@1931: xRanges.put(index, range);
ingo@1684: }
ingo@1684:
ingo@1684:
felix@1930: /**
felix@1711: * Adds annotations to list (if visible is true).
felix@1711: */
ingo@1684: public void addAnnotations(FLYSAnnotation annotation, boolean visible) {
ingo@1684: if (!visible) {
ingo@1684: return;
ingo@1684: }
ingo@1684:
ingo@1679: if (annotations == null) {
ingo@1679: annotations = new ArrayList();
ingo@1679: }
ingo@1679:
ingo@1679: annotations.add(annotation);
ingo@1679: }
ingo@1679:
ingo@1679:
felix@1931: /**
felix@1931: * Create Y (range) axis for given index.
felix@1931: * Shall be overriden by subclasses.
felix@1931: */
felix@1931: protected NumberAxis createYAxis(int index) {
felix@1940: NumberAxis axis = new NumberAxis(getYAxisLabel());
felix@1943: axis.setAutoRangeIncludesZero(false);
felix@1931: return axis;
felix@1931: }
felix@1931:
felix@1940:
felix@1935: /**
felix@1935: * If no data is visible, draw at least empty axis.
felix@1935: */
felix@1935: private void recoverEmptyPlot(XYPlot plot) {
felix@1935: if (plot.getRangeAxis() == null) {
felix@1935: logger.debug("debug: No range axis");
felix@1935: plot.setRangeAxis(createYAxis(0));
felix@1935: }
ingo@923: }
ingo@923:
ingo@923:
felix@1931: /**
felix@1940: * Expands a given range if it collapses into one point.
felix@1983: * @param Range to be expanded if upper == lower bound.
felix@1940: */
felix@1940: private Range expandPointRange(Range range) {
felix@1983: if (range != null && range.getLowerBound() == range.getUpperBound()) {
felix@1940: return expandRange(range, 5);
felix@1940: }
felix@1940: return range;
felix@1940: }
felix@1940:
felix@1940:
felix@1940: /**
felix@1940: * Expands X axes if only a point is shown.
felix@1931: */
ingo@1686: private void preparePointRanges(XYPlot plot) {
ingo@1686: for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
felix@1940: logger.debug("Check whether to expand a x axis.");
sascha@1698: Integer key = Integer.valueOf(i);
ingo@1686:
ingo@1686: Range r = xRanges.get(key);
ingo@1687: if (r != null && r.getLowerBound() == r.getUpperBound()) {
ingo@1686: xRanges.put(key, expandRange(r, 5));
ingo@1686: }
ingo@1686: }
ingo@1686: }
ingo@1686:
ingo@1686:
felix@1931: /**
felix@1931: * Expand range by percent.
felix@1931: */
ingo@1686: public static Range expandRange(Range range, double percent) {
ingo@1686: if (range == null) {
ingo@1686: return null;
ingo@1686: }
ingo@1686:
ingo@1686: double value = range.getLowerBound();
ingo@1686: double expand = value / 100 * percent;
ingo@1686:
ingo@1686: return expand != 0
ingo@1686: ? new Range(value-expand, value+expand)
ingo@1686: : new Range(-0.01 * percent, 0.01 * percent);
ingo@1686: }
ingo@1686:
ingo@1686:
ingo@653: /**
ingo@654: * This method zooms the plot to the specified ranges in the attribute
ingo@654: * document or to the ranges specified by the min/max values in the
ingo@654: * datasets. Note: We determine the range manually if no zoom ranges
ingo@654: * are given, because JFreeCharts auto-zoom adds a margin to the left and
ingo@654: * right of the data area.
ingo@653: *
ingo@653: * @param plot The XYPlot.
ingo@653: */
ingo@673: protected void autoZoom(XYPlot plot) {
ingo@652: logger.debug("Zoom to specified ranges.");
ingo@654:
ingo@673: Range xrange = getDomainAxisRange();
ingo@673: Range yrange = getValueAxisRange();
ingo@654:
ingo@1699: zoomX(plot, plot.getDomainAxis(), xRanges.get(0), xrange);
ingo@923:
ingo@1699: for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
ingo@673: ValueAxis yaxis = plot.getRangeAxis(i);
ingo@654:
ingo@673: if (yaxis == null) {
ingo@1699: logger.debug("Zoom problem: no Y Axis for index: " + i);
ingo@673: continue;
ingo@673: }
ingo@673:
ingo@1699: logger.debug("Prepare zoom settings for y axis at index: " + i);
ingo@1699: zoomY(plot, yaxis, yRanges.get(Integer.valueOf(i)), yrange);
ingo@654: }
ingo@653: }
ingo@652:
ingo@653:
ingo@718: protected boolean zoomX(XYPlot plot, ValueAxis axis, Range range, Range x) {
ingo@718: return zoom(plot, axis, range, x);
ingo@718: }
ingo@718:
ingo@718:
ingo@718: protected boolean zoomY(XYPlot plot, ValueAxis axis, Range range, Range x) {
ingo@718: return zoom(plot, axis, range, x);
ingo@718: }
ingo@718:
ingo@718:
ingo@653: /**
ingo@653: * Zooms the x axis to the range specified in the attribute document.
ingo@653: *
felix@1958: * @param plot The XYPlot.
felix@1958: * @param axis The axis the shoud be modified.
ingo@673: * @param range The whole range specified by a dataset.
felix@1958: * @param x A user defined range (null permitted).
ingo@654: *
ingo@654: * @return true, if a zoom range was specified, otherwise false.
ingo@653: */
ingo@673: protected boolean zoom(XYPlot plot, ValueAxis axis, Range range, Range x) {
sascha@1736:
sascha@1736: if (range == null) {
sascha@1736: return false;
sascha@1736: }
sascha@1736:
ingo@673: if (x != null) {
ingo@673: double min = range.getLowerBound();
ingo@673: double max = range.getUpperBound();
ingo@673: double diff = max > min ? max - min : min - max;
ingo@652:
ingo@673: Range computed = new Range(
ingo@673: min + x.getLowerBound() * diff,
ingo@673: min + x.getUpperBound() * diff);
ingo@673:
ingo@717: axis.setRangeWithMargins(computed);
ingo@673:
ingo@673: logger.debug("Zoom axis to: " + computed);
ingo@654:
ingo@654: return true;
ingo@652: }
ingo@654:
ingo@717: axis.setRangeWithMargins(range);
ingo@654: return false;
ingo@654: }
ingo@654:
ingo@654:
ingo@654: /**
ingo@1684: * This method extracts the minimum and maximum values for x and y axes
ingo@1684: * which are stored in xRanges and yRanges.
ingo@654: *
ingo@1684: * @param index The index of the y-Axis.
ingo@654: *
ingo@654: * @return a Range[] as follows: [x-Range, y-Range].
ingo@654: */
felix@1944: public Range[] getRangesForAxis(int index) {
felix@1944: logger.debug("getRangesForAxis " + index);
ingo@1684: return new Range[] {
sascha@1698: xRanges.get(Integer.valueOf(0)),
sascha@1698: yRanges.get(Integer.valueOf(index))
ingo@1684: };
ingo@652: }
ingo@652:
ingo@652:
felix@1940: /**
felix@1940: * Add annotations to Renderer.
felix@1940: */
felix@1940: protected void addAnnotationsToRenderer(XYPlot plot) {
ingo@1679: plot.clearAnnotations();
ingo@1679:
ingo@1679: if (annotations == null) {
ingo@1679: logger.debug("No Annotations given.");
ingo@1679: return;
ingo@1679: }
ingo@1679:
ingo@1679: LegendItemCollection lic = new LegendItemCollection();
felix@1940: LegendItemCollection old = plot.getFixedLegendItems();
ingo@1679:
felix@1940: XYItemRenderer renderer = plot.getRenderer(0);
ingo@1679:
ingo@1679: for (FLYSAnnotation fa: annotations) {
ingo@1679: Document theme = fa.getTheme();
ingo@1679:
sascha@1754: ThemeAccess themeAccess = new ThemeAccess(theme);
ingo@1679:
sascha@1754: Color color = themeAccess.parseLineColorField();
sascha@1754: int lineWidth = themeAccess.parseLineWidth();
ingo@1679: lic.add(new LegendItem(fa.getLabel(), color));
ingo@1679:
ingo@1679: for (XYTextAnnotation ta: fa.getAnnotations()) {
raimund@1738: if(ta instanceof StickyAxisAnnotation) {
raimund@1738: StickyAxisAnnotation sta = (StickyAxisAnnotation)ta;
sascha@1754: sta.applyTheme(themeAccess);
raimund@1738: renderer.addAnnotation(sta);
raimund@1738: }
raimund@1738: else {
raimund@1738: ta.setPaint(color);
raimund@1738: ta.setOutlineStroke(new BasicStroke((float) lineWidth));
raimund@1738: renderer.addAnnotation(ta);
raimund@1738: }
ingo@1679: }
ingo@1679:
ingo@1679: }
felix@1940:
felix@1940: // (Re-)Add prior legend entries.
felix@1940: if (old != null) {
felix@1940: old.addAll(lic);
felix@1940: }
felix@1940: else {
felix@1940: old = lic;
felix@1940: }
felix@1940:
felix@1940: plot.setFixedLegendItems(old);
ingo@1679: }
ingo@1679:
ingo@1679:
ingo@369: /**
felix@1931: * Adjusts the axes of a plot (the first axis does not include zero).
felix@1940: * To be overridden by children.
ingo@369: * @param plot The XYPlot of the chart.
ingo@369: */
ingo@369: protected void adjustAxes(XYPlot plot) {
felix@1940: /*
ingo@369: NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
felix@1931: if (yAxis == null) {
felix@1931: logger.warn("No Axis to setAutoRangeIncludeZero.");
felix@1931: }
felix@1931: else {
felix@1931: yAxis.setAutoRangeIncludesZero(false);
felix@1931: }
felix@1940: */
ingo@369: }
ingo@414:
ingo@414:
felix@1940: /**
felix@1940: * Set some Stroke/Grid defaults.
felix@1940: */
ingo@419: protected void adjustPlot(XYPlot plot) {
ingo@419: Stroke gridStroke = new BasicStroke(
ingo@419: DEFAULT_GRID_LINE_WIDTH,
ingo@419: BasicStroke.CAP_BUTT,
ingo@419: BasicStroke.JOIN_MITER,
ingo@419: 3.0f,
ingo@419: new float[] { 3.0f },
ingo@419: 0.0f);
ingo@419:
ingo@419: plot.setDomainGridlineStroke(gridStroke);
ingo@419: plot.setDomainGridlinePaint(DEFAULT_GRID_COLOR);
ingo@419: plot.setDomainGridlinesVisible(true);
ingo@419:
ingo@419: plot.setRangeGridlineStroke(gridStroke);
ingo@419: plot.setRangeGridlinePaint(DEFAULT_GRID_COLOR);
ingo@419: plot.setRangeGridlinesVisible(true);
ingo@654:
ingo@654: plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));
ingo@419: }
ingo@419:
ingo@419:
felix@2020: /** Override to handle subtitle adding. */
ingo@414: protected void addSubtitles(JFreeChart chart) {
ingo@414: // override this method in subclasses that need subtitles
ingo@414: }
ingo@924:
ingo@924:
ingo@1645: /**
ingo@1645: * This method walks over all axes (domain and range) of plot and
ingo@1645: * calls localizeDomainAxis() for domain axes or localizeRangeAxis() for
ingo@1645: * range axes.
ingo@1645: *
ingo@1645: * @param plot The XYPlot.
ingo@1645: */
ingo@1645: private void localizeAxes(XYPlot plot) {
ingo@1645: for (int i = 0, num = plot.getDomainAxisCount(); i < num; i++) {
ingo@1645: ValueAxis axis = plot.getDomainAxis(i);
ingo@1645:
ingo@1645: if (axis != null) {
ingo@1645: localizeDomainAxis(axis);
ingo@1645: }
ingo@1645: else {
ingo@1645: logger.warn("Domain axis at " + i + " is null.");
ingo@1645: }
ingo@1645: }
ingo@1645:
ingo@1645: for (int i = 0, num = plot.getRangeAxisCount(); i < num; i++) {
ingo@1645: ValueAxis axis = plot.getRangeAxis(i);
ingo@1645:
ingo@1645: if (axis != null) {
ingo@1645: localizeRangeAxis(axis);
ingo@1645: }
ingo@1645: else {
ingo@1645: logger.warn("Range axis at " + i + " is null.");
ingo@1645: }
ingo@1645: }
ingo@1645: }
ingo@1645:
ingo@1645:
ingo@1645: /**
ingo@1645: * Overrides the NumberFormat with the NumberFormat for the current locale
ingo@1645: * that is provided by getLocale().
ingo@1645: *
ingo@1645: * @param domainAxis The domain axis that needs localization.
ingo@1645: */
ingo@1645: protected void localizeDomainAxis(ValueAxis domainAxis) {
ingo@1645: NumberFormat nf = NumberFormat.getInstance(getLocale());
ingo@1645: ((NumberAxis) domainAxis).setNumberFormatOverride(nf);
ingo@1645: }
ingo@1645:
ingo@1645:
ingo@1645: /**
ingo@1645: * Overrides the NumberFormat with the NumberFormat for the current locale
ingo@1645: * that is provided by getLocale().
ingo@1645: *
ingo@1645: * @param domainAxis The domain axis that needs localization.
ingo@1645: */
ingo@1645: protected void localizeRangeAxis(ValueAxis rangeAxis) {
ingo@1645: NumberFormat nf = NumberFormat.getInstance(getLocale());
ingo@1645: ((NumberAxis) rangeAxis).setNumberFormatOverride(nf);
ingo@1645: }
ingo@1645:
ingo@1645:
felix@1932: /**
felix@1932: * @param idx "index" of dataset/series (first dataset to be drawn has
felix@1932: * index 0), correlates with renderer index.
felix@2005: * @param isArea true if the series describes an area and shall be rendered
felix@2005: * as such.
felix@1932: * @return idx increased by number of items addded.
felix@1932: */
felix@2005: protected int applyThemes(
felix@2005: XYPlot plot,
felix@2005: XYSeriesCollection series,
felix@2005: int idx,
felix@2005: boolean isArea
felix@2005: ) {
ingo@1679: LegendItemCollection lic = new LegendItemCollection();
ingo@1679: LegendItemCollection anno = plot.getFixedLegendItems();
ingo@1679:
felix@1932: int retidx = idx;
ingo@924:
felix@2005: if (isArea) {
felix@2020: logger.debug("Registering an 'area'renderer at idx: " + idx);
felix@2020: StyledAreaSeriesCollection area = (StyledAreaSeriesCollection) series;
felix@2020:
felix@2020: StableXYDifferenceRenderer dRenderer = new StableXYDifferenceRenderer();
felix@2020: if (area.getMode() == StyledAreaSeriesCollection.FILL_MODE.UNDER) {
felix@2020: dRenderer.setPositivePaint(createTransparentPaint());
felix@2020: }
felix@2020: plot.setRenderer(idx, dRenderer);
felix@2020:
felix@2020: area.applyTheme(dRenderer);
felix@2020:
felix@2020: LegendItem legendItem = dRenderer.getLegendItem(idx, 0);
felix@2020: if (legendItem != null) {
felix@2020: lic.add(legendItem);
felix@2020: }
felix@2020: else {
felix@2020: logger.warn("Could not get LegentItem for renderer: "
felix@2020: + idx + ", series-idx " + 0);
felix@2020: }
felix@2020: if (anno != null) {
felix@2020: lic.addAll(anno);
felix@2020: }
felix@2020: plot.setFixedLegendItems(lic);
felix@2020: return retidx + 1;
felix@2005: }
felix@2005:
felix@2020: XYLineAndShapeRenderer renderer = getRenderer(plot, idx);
felix@2020:
felix@1932: for (int s = 0, num = series.getSeriesCount(); s < num; s++) {
felix@1932: XYSeries serie = series.getSeries(s);
ingo@924:
felix@1932: if (serie instanceof StyledXYSeries) {
felix@1932: ((StyledXYSeries) serie).applyTheme(renderer, s);
ingo@924: }
ingo@1679:
ingo@1686: // special case: if there is just one single item, we need to enable
ingo@1686: // points for this series, otherwise we would not see anything in
ingo@1686: // the chart area.
felix@1932: if (serie.getItemCount() == 1) {
felix@1932: renderer.setSeriesShapesVisible(s, true);
ingo@1686: }
ingo@1686:
felix@2020: LegendItem legendItem = renderer.getLegendItem(idx, s);
felix@2020: if (legendItem != null) {
felix@2020: lic.add(legendItem);
felix@1932: }
felix@1932: else {
felix@1932: logger.warn("Could not get LegentItem for renderer: "
felix@2020: + idx + ", series-idx " + s);
felix@1932: }
felix@2020: // TODO: why that? isnt renderer set per dataset not per series?
felix@1932: retidx++;
ingo@924: }
ingo@924:
ingo@1679: if (anno != null) {
ingo@1679: lic.addAll(anno);
ingo@1679: }
ingo@1679:
ingo@1679: plot.setFixedLegendItems(lic);
ingo@1679:
felix@1932: plot.setRenderer(idx, renderer);
felix@1932:
felix@1932: return retidx;
ingo@924: }
ingo@924:
ingo@924:
felix@2020: /** Returns a transparently textured paint. */
felix@2020: // TODO why not use a transparent color?
felix@2020: protected static Paint createTransparentPaint() {
felix@2020: BufferedImage texture = new BufferedImage(
felix@2020: 1, 1, BufferedImage.TYPE_4BYTE_ABGR);
felix@2020:
felix@2020: return new TexturePaint(
felix@2020: texture, new Rectangle2D.Double(0d, 0d, 0d, 0d));
felix@2020: }
felix@2020:
felix@2020:
felix@1932: /**
felix@1932: * Get renderer, from plot or cloned default renderer otherwise.
felix@1932: */
ingo@924: protected XYLineAndShapeRenderer getRenderer(XYPlot plot, int idx) {
felix@2020: logger.debug("getRenderer: " + idx);
felix@2034:
ingo@924: XYLineAndShapeRenderer r =
ingo@924: (XYLineAndShapeRenderer) plot.getRenderer(idx);
ingo@924:
ingo@924: if (r != null) {
ingo@924: return r;
ingo@924: }
ingo@924:
felix@2034: // Need a new renderer.
ingo@924: if (idx == 0) {
ingo@924: logger.warn("No default renderer set!");
ingo@924: return new XYLineAndShapeRenderer();
ingo@924: }
ingo@924:
felix@2034: // 'Default' (first) renderer is an area-renderer.
felix@2034: XYItemRenderer renderer = (XYItemRenderer) plot.getRenderer(0);
felix@2034: if (renderer instanceof StableXYDifferenceRenderer) {
felix@2034: return new XYLineAndShapeRenderer();
felix@2034: }
felix@2034:
felix@2034: r = (XYLineAndShapeRenderer) renderer;
ingo@924:
ingo@924: try {
ingo@924: return (XYLineAndShapeRenderer) r.clone();
ingo@924: }
ingo@924: catch (CloneNotSupportedException cnse) {
ingo@924: logger.warn(cnse, cnse);
ingo@924: }
ingo@924:
ingo@924: logger.warn("No applicalable renderer found!");
ingo@924:
ingo@924: return new XYLineAndShapeRenderer();
ingo@924: }
felix@1849:
felix@1849:
felix@1849: /**
felix@1861: * Register annotations like MainValues for later plotting
felix@1861: *
felix@1861: * @param o list of annotations (data of facet).
felix@1861: * @param facet The facet. This facet does NOT support any data objects. Use
felix@1861: * FLYSArtifact.getNativeFacet() instead to retrieve a Facet which supports
felix@1861: * data.
felix@1861: * @param theme Theme document for given annotations.
felix@1861: * @param visible The visibility of the annotations.
felix@1849: */
felix@1849: protected void doAnnotations(
felix@1849: FLYSAnnotation annotations,
felix@1849: Facet facet,
felix@1849: Document theme,
felix@1849: boolean visible
felix@1849: ){
felix@1849: logger.debug("doAnnotations");
felix@1849:
felix@1861: // Add all annotations to our annotation pool.
felix@1849: annotations.setTheme(theme);
felix@1849: annotations.setLabel(facet.getDescription());
felix@1849: addAnnotations(annotations, visible);
felix@1849: }
ingo@1986:
ingo@1986:
ingo@1986: /**
ingo@1986: * Returns an instance of ChartSettings with a chart specific section
ingo@1986: * but with no axes settings.
ingo@1986: *
ingo@1986: * @return an instance of ChartSettings.
ingo@1986: */
ingo@1986: public Settings getSettings() {
ingo@1986: ChartSettings settings = new ChartSettings();
ingo@1986:
ingo@1990: Section chartSection = buildChartSection();
ingo@1990: Section legendSection = buildLegendSection();
ingo@1990:
ingo@1990: settings.setChartSection(chartSection);
ingo@1990: settings.setLegendSection(legendSection);
ingo@1990:
ingo@1991: List axisSections = buildAxisSections();
ingo@1991: for (Section axisSection: axisSections) {
ingo@1991: settings.addAxisSection(axisSection);
ingo@1991: }
ingo@1991:
ingo@1990: return settings;
ingo@1990: }
ingo@1990:
ingo@1990:
ingo@1990: /**
ingo@1990: * Creates a new ChartSection.
ingo@1990: *
ingo@1990: * @return a new ChartSection.
ingo@1990: */
ingo@1990: protected Section buildChartSection() {
ingo@1986: ChartSection chartSection = new ChartSection();
ingo@1989: chartSection.setTitle(getChartTitle());
ingo@1989: chartSection.setSubtitle(getChartSubtitle());
ingo@1989: chartSection.setDisplayGird(isGridVisible());
ingo@1990: return chartSection;
ingo@1990: }
ingo@1986:
ingo@1986:
ingo@1990: /**
ingo@1990: * Creates a new LegendSection.
ingo@1990: *
ingo@1990: * @return a new LegendSection.
ingo@1990: */
ingo@1990: protected Section buildLegendSection() {
ingo@1990: LegendSection legendSection = new LegendSection();
ingo@1990: legendSection.setVisibility(isLegendVisible());
ingo@1990: legendSection.setFontSize(getLegendFontSize());
ingo@1990: return legendSection;
ingo@1986: }
ingo@1991:
ingo@1991:
ingo@1991: /**
ingo@2000: * Creates a list of Sections that contains all axes of the chart (including
ingo@2000: * X and Y axes).
ingo@2000: *
ingo@2000: * @return a list of Sections for each axis in this chart.
ingo@2000: */
ingo@2000: protected List buildAxisSections() {
ingo@2000: List axisSections = new ArrayList();
ingo@2000:
ingo@2000: axisSections.addAll(buildXAxisSections());
ingo@2000: axisSections.addAll(buildYAxisSections());
ingo@2000:
ingo@2000: return axisSections;
ingo@2000: }
ingo@2000:
ingo@2000:
ingo@2000: /**
ingo@2000: * Creates a new Section for chart's X axis.
ingo@1991: *
ingo@1997: * @return a List that contains a Section for the X axis.
ingo@1991: */
ingo@2000: protected List buildXAxisSections() {
ingo@1997: List axisSections = new ArrayList();
ingo@1997:
ingo@1997: String identifier = "X";
ingo@1997:
ingo@1997: AxisSection axisSection = new AxisSection();
ingo@1997: axisSection.setIdentifier(identifier);
ingo@1997: axisSection.setLabel(getXAxisLabel());
ingo@1997: axisSection.setFontSize(14);
ingo@1997: axisSection.setFixed(false);
ingo@1997:
ingo@1997: // XXX We are able to find better default ranges that [0,0], but the Y
ingo@1997: // axes currently have no better ranges set.
ingo@1997: axisSection.setUpperRange(0d);
ingo@1997: axisSection.setLowerRange(0d);
ingo@1997:
ingo@1997: axisSections.add(axisSection);
ingo@1997:
ingo@1997: return axisSections;
ingo@1991: }
ingo@2000:
ingo@2000:
ingo@2000: /**
ingo@2000: * Creates a list of Section for the chart's Y axes. This method makes use
ingo@2000: * of getYAxisWalker to be able to access all Y axes defined in
ingo@2000: * subclasses.
ingo@2000: *
ingo@2000: * @return a list of Y axis sections.
ingo@2000: */
ingo@2000: protected List buildYAxisSections() {
ingo@2000: List axisSections = new ArrayList();
ingo@2000:
ingo@2000: YAxisWalker walker = getYAxisWalker();
ingo@2000: for (int i = 0, n = walker.length(); i < n; i++) {
ingo@2000: AxisSection ySection = new AxisSection();
ingo@2000: ySection.setIdentifier(walker.getId(i));
ingo@2000: ySection.setLabel(getYAxisLabel(i));
ingo@2000: ySection.setFontSize(14);
ingo@2000: ySection.setFixed(false);
ingo@2000:
ingo@2000: // XXX We are able to find better default ranges that [0,0], the
ingo@2000: // only problem is, that we do NOT have a better range than [0,0]
ingo@2000: // for each axis, because the initial chart will not have a dataset
ingo@2000: // for each axis set!
ingo@2000: ySection.setUpperRange(0d);
ingo@2000: ySection.setLowerRange(0d);
ingo@2000:
ingo@2000: axisSections.add(ySection);
ingo@2000: }
ingo@2000:
ingo@2000: return axisSections;
ingo@2000: }
ingo@369: }
ingo@369: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :