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: * 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: // !TODO what if its a differencerenderer?! felix@2020: logger.debug("getRenderer: " + idx); 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: ingo@924: if (idx == 0) { ingo@924: logger.warn("No default renderer set!"); ingo@924: return new XYLineAndShapeRenderer(); ingo@924: } ingo@924: ingo@924: r = (XYLineAndShapeRenderer) plot.getRenderer(0); 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 :