Mercurial > dive4elements > river
view flys-artifacts/src/main/java/de/intevation/flys/exports/XYChartGenerator.java @ 1931:7c52e9cb2a72
Allow more than two datasets and more flexibility with axes in plots. Based on patch by S. Teichmann.
flys-artifacts/trunk@3312 c6561f87-3c4e-4783-a992-168aeb5c3f6f
author | Felix Wolfsteller <felix.wolfsteller@intevation.de> |
---|---|
date | Thu, 24 Nov 2011 07:20:46 +0000 |
parents | de0c2bbb27f9 |
children | 17e18948fe5e |
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.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. */ public void createAxes(XYPlot plot) { logger.debug("XYChartGenerator.createAxes"); Integer last = datasets.lastKey(); if (last != null) { for (int i = last; i >= 0; --i) { if (datasets.containsKey(i)) { plot.setRangeAxis(i, createYAxis(i)); } } } } /** * 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); } } 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) { for (Map.Entry<Integer, List<XYDataset>> entry: datasets.entrySet()) { int axis = entry.getKey(); for (XYDataset dataset: entry.getValue()) { if (dataset instanceof XYSeriesCollection) { applyThemes(plot, (XYSeriesCollection)dataset, axis); } } } } protected void applyThemes(XYPlot plot, XYSeriesCollection dataset, int i) { LegendItemCollection lic = new LegendItemCollection(); LegendItemCollection anno = plot.getFixedLegendItems(); XYLineAndShapeRenderer r = getRenderer(plot, i); for (int s = 0, num = dataset.getSeriesCount(); s < num; s++) { XYSeries series = dataset.getSeries(s); if (series instanceof StyledXYSeries) { ((StyledXYSeries) series).applyTheme(r, s); } // 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 (series.getItemCount() == 1) { r.setSeriesShapesVisible(s, true); } lic.add(r.getLegendItem(i, s)); } if (anno != null) { lic.addAll(anno); } plot.setFixedLegendItems(lic); plot.setRenderer(i, r); } protected XYLineAndShapeRenderer getRenderer(XYPlot plot, int idx) { XYLineAndShapeRenderer r = (XYLineAndShapeRenderer) plot.getRenderer(idx); if (r != null) { return r; } if (idx == 0) { logger.warn("No default renderer set!"); return new XYLineAndShapeRenderer(); } r = (XYLineAndShapeRenderer) plot.getRenderer(0); try { return (XYLineAndShapeRenderer) r.clone(); } catch (CloneNotSupportedException cnse) { logger.warn(cnse, cnse); } logger.warn("No applicalable renderer found!"); return new XYLineAndShapeRenderer(); } /** * 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 :