package org.activityinfo.server.report.renderer; /* * #%L * ActivityInfo Server * %% * Copyright (C) 2009 - 2013 UNICEF * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import com.google.code.appengine.awt.*; import com.google.code.appengine.awt.geom.Rectangle2D; import com.google.code.appengine.awt.image.BufferedImage; import com.google.code.appengine.imageio.ImageIO; import org.activityinfo.legacy.shared.exception.ReportModelException; import org.activityinfo.legacy.shared.reports.Theme; import org.activityinfo.legacy.shared.reports.content.PivotChartContent; import org.activityinfo.legacy.shared.reports.content.PivotTableData; import org.activityinfo.legacy.shared.reports.model.CategoryProperties; import org.activityinfo.legacy.shared.reports.model.Dimension; import org.activityinfo.legacy.shared.reports.model.PivotChartReportElement; import org.activityinfo.legacy.shared.reports.model.PivotChartReportElement.Type; import org.activityinfo.server.report.output.StorageProvider; import org.activityinfo.server.report.output.TempStorage; import org.krysalis.jcharts.Chart; import org.krysalis.jcharts.axisChart.AxisChart; import org.krysalis.jcharts.chartData.AxisChartDataSet; import org.krysalis.jcharts.chartData.ChartDataException; import org.krysalis.jcharts.chartData.DataSeries; import org.krysalis.jcharts.chartData.PieChartDataSet; import org.krysalis.jcharts.chartData.interfaces.IAxisDataSeries; import org.krysalis.jcharts.encoders.JPEGEncoder; import org.krysalis.jcharts.nonAxisChart.PieChart2D; import org.krysalis.jcharts.properties.*; import org.krysalis.jcharts.properties.util.ChartFont; import org.krysalis.jcharts.properties.util.ChartStroke; import org.krysalis.jcharts.types.ChartType; import org.krysalis.jcharts.types.PieLabelType; import java.io.IOException; import java.io.OutputStream; import java.util.Collection; import java.util.List; /** * Pivot Chart Generator that uses JChart to render the chart as an image * * @author Alex Bertram */ public class ChartRendererJC { // one of the available logical font families names from org.apache.harmony.awt.gl.font.FontManager private static final String SANS_SERIF = "SansSerif"; public String renderToUrl(PivotChartReportElement element, boolean includeTitle, StorageProvider istorageProvider, int width, int height, int dpi) throws IOException { try { Chart chart = createChart(element, includeTitle, width, height, dpi); TempStorage storage = istorageProvider.allocateTemporaryFile(null, "activityinfo.jpg"); JPEGEncoder.encode(chart, 0.75f, storage.getOutputStream()); return storage.getUrl(); } catch (Exception e) { e.printStackTrace(); return null; } } public void render(PivotChartReportElement element, OutputStream os) throws IOException { BufferedImage image = renderImage(element, true, 640, 480, 96); ImageIO.write(image, "PNG", os); } public BufferedImage renderImage(PivotChartReportElement element, boolean includeTitle, int width, int height, int dpi) { try { Chart chart = createChart(element, includeTitle, width, height, dpi); BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); chart.setGraphics2D(bufferedImage.createGraphics()); chart.render(); return bufferedImage; } catch (IOException e) { throw new ReportModelException(e, element); } catch (ChartDataException e) { throw new ReportModelException(e, element); } catch (PropertyException e) { throw new ReportModelException(e, element); } } public void render(PivotChartReportElement element, boolean includeTitle, Graphics2D g2d, int width, int height, int dpi) { try { Chart chart = createChart(element, includeTitle, width, height, dpi); chart.setGraphics2D(g2d); chart.render(); } catch (Exception e) { throw new RuntimeException("Rendering of chart entitled '" + element.getTitle() + "' failed", e); } } /** * Creates a JChart from the given PivotChartElement and its generated * PivotChartContent * * @param element The underlying PivotChartElement * @param includeTitle True if the title should be rendered as part of the image * @param width Width in pixels * @param height Heights in pixels * @param dpi Resolution (dots-per-inch) to use in the calculation of font * sizes * @return A JChart <code>Chart</code> * @throws IOException * @throws ChartDataException */ protected Chart createChart(PivotChartReportElement element, boolean includeTitle, int width, int height, int dpi) throws IOException, ChartDataException, PropertyException { if (element.getType() == PivotChartReportElement.Type.Pie) { return createPieChart(element, includeTitle, width, height, dpi); } else { return createAxisChart(element, includeTitle, width, height, dpi); } } /** * Creates a JChart PieChart from our report element */ protected Chart createPieChart(PivotChartReportElement element, boolean includeTitle, int width, int height, int dpi) throws IOException, ChartDataException { PivotTableData table = element.getContent().getData(); List<PivotTableData.Axis> categories = table.getRootCategory().getLeaves(); PivotTableData.Axis series = table.getRootSeries().getLeaves().get(0); PieChart2DProperties pieProps = new PieChart2DProperties(); pieProps.setValueLabelFont(new ChartFont(new Font(SANS_SERIF, Font.PLAIN, fontSize(10, dpi)), Color.black)); pieProps.setShowGrouping(true); pieProps.setPieLabelType(PieLabelType.VALUE_LABELS); pieProps.setBorderChartStroke(ChartStroke.DEFAULT_ZERO_LINE); PieChartDataSet dataSet = new PieChartDataSet(includeTitle ? element.getTitle() : null, toDataArray(categories, series), toLabelArray(element.getContent(), categories), computePaints(categories), pieProps); LegendProperties legendProps = computeLegendProperties(element, dpi, categories); ChartProperties chartProps = computeChartProperties(dpi); return new PieChart2D(dataSet, legendProps, chartProps, width, height); } private Paint[] computePaints(List<PivotTableData.Axis> categories) { Paint[] paints = getDefaultPaints(categories.size()); Dimension dim = categories.get(0).getDimension(); if (dim == null) { return paints; } for (int i = 0; i != categories.size(); ++i) { CategoryProperties props = dim.getCategories().get(categories.get(i).getCategory()); if (props != null && props.getColor() != null) { paints[i] = new Color(props.getColor()); } } return paints; } protected Chart createAxisChart(PivotChartReportElement element, boolean includeTitle, int width, int height, int dpi) throws IOException, ChartDataException, PropertyException { PivotTableData table = element.getContent().getData(); // find our categories and series leaves List<PivotTableData.Axis> categories = table.getRootRow().getLeaves(); List<PivotTableData.Axis> series = table.getRootColumn().getLeaves(); IAxisDataSeries dataSeries = new DataSeries(toLabelArray(element.getContent(), categories), element.getContent().getXAxisTitle(), element.getContent().getYAxisTitle(), includeTitle ? element.getTitle() : null); dataSeries.addIAxisPlotDataSet(new AxisChartDataSet(toDataArray(categories, series), toLabelArray(element.getContent(), series), computePaints(series), computeChartType(element), computeAxisChartProperties(dpi, element))); return new AxisChart(dataSeries, computeChartProperties(dpi), computeAxisProperties(dpi, element.getContent()), computeLegendProperties(element, dpi, series), width, height); } private ChartType computeChartType(PivotChartReportElement element) { switch (element.getType()) { case Line: return ChartType.LINE; case StackedBar: return ChartType.BAR_STACKED; case Bar: case ClusteredBar: default: return ChartType.BAR_CLUSTERED; } } protected String[] toLabelArray(PivotChartContent content, Collection<PivotTableData.Axis> leaves) { String[] labels = new String[leaves.size()]; int i = 0; for (PivotTableData.Axis leaf : leaves) { labels[i] = leaf.flattenLabel(); if (labels[i].length() == 0) { labels[i] = content.getYAxisTitle(); } i++; } return labels; } protected double[][] toDataArray(List<PivotTableData.Axis> categories, List<PivotTableData.Axis> series) { double[][] seriesData = new double[series.size()][]; for (int i = 0; i != series.size(); ++i) { double[] categoryData = new double[categories.size()]; int j = 0; for (PivotTableData.Axis category : categories) { PivotTableData.Cell cell = category.getCell(series.get(i)); if (cell == null || cell.getValue() == null) { categoryData[j++] = 0; } else { categoryData[j++] = cell.getValue(); } } seriesData[i] = categoryData; } return seriesData; } /** * Builds a single dimensional array from a 2d PivotTableData. * * @param categories Leaf categories * @param series Leaf series * @return A one dimensional data array. */ protected double[] toDataArray(List<PivotTableData.Axis> categories, PivotTableData.Axis series) { double[] data = new double[categories.size()]; for (int i = 0; i != categories.size(); ++i) { PivotTableData.Cell cell = categories.get(i).getCell(series); if (cell != null && cell.getValue() != 0) { data[i] = cell.getValue(); } } return data; } protected ChartProperties computeChartProperties(int dpi) { ChartProperties p = new ChartProperties(); ChartFont titleFont = new ChartFont(new Font(SANS_SERIF, Font.PLAIN, fontSize(12, dpi)), Color.black); p.setTitleFont(titleFont); return p; } protected Paint[] getDefaultPaints(int count) { String[] accents = Theme.getAccents(); Paint[] paints = new Paint[count]; for (int i = 0; i != paints.length; ++i) { paints[i] = Color.decode(accents[i % accents.length]); } return paints; } /** * Caclulates the font size in pixels given the font size in points and the * resolution (dots-per-inch) * * @param pt Font size in points (1 in = 72 points) * @param dpi Resolution in dots-per-inch (pixels-per-inch) * @return the font-size in pixels */ private int fontSize(int pt, int dpi) { return (int) ((pt) / 72f * (dpi)); } protected AxisProperties computeAxisProperties(int dpi, PivotChartContent content) throws PropertyException { ChartFont axisScaleFont = new ChartFont(new Font(SANS_SERIF, Font.PLAIN, fontSize(10, dpi)), Color.black); ChartFont axisTitleFont = new ChartFont(new Font(SANS_SERIF, Font.PLAIN, fontSize(11, dpi)), Color.black); AxisProperties p = new AxisProperties(false); LabelAxisProperties x = (LabelAxisProperties) p.getXAxisProperties(); x.setScaleChartFont(axisScaleFont); x.setAxisTitleChartFont(axisTitleFont); DataAxisProperties y = (DataAxisProperties) p.getYAxisProperties(); y.setScaleChartFont(axisScaleFont); y.setUserDefinedScale(content.getYMin(), content.getYStep()); y.setAxisTitleChartFont(axisTitleFont); y.setShowGridLines(1); return p; } protected LegendProperties computeLegendProperties(PivotChartReportElement element, int dpi, List<PivotTableData.Axis> series) { // if there is only series, there is no need for a legend if (series.size() <= 1) { return null; } ChartFont font = new ChartFont(new Font(SANS_SERIF, Font.PLAIN, fontSize(10, dpi)), Color.BLACK); LegendProperties p = new LegendProperties(); p.setChartFont(font); p.setBorderStroke(null); p.setIconBorderPaint(null); if (element.getType() == Type.Pie) { p.setPlacement(LegendProperties.RIGHT); } else { p.setPlacement(maxLegendTextLength(series) > 30 ? LegendProperties.BOTTOM : LegendProperties.RIGHT); } p.setNumColumns(1); return p; } private int maxLegendTextLength(List<PivotTableData.Axis> series) { int max = 0; for (PivotTableData.Axis axis : series) { max = Math.max(max, axis.flattenLabel().length()); } return max; } protected AxisChartTypeProperties computeAxisChartProperties(int dpi, PivotChartReportElement element) { if (element.getType() == PivotChartReportElement.Type.Line) { return computeLineChartProperties(element, dpi); } else { BarChartProperties p; switch (element.getType()) { case StackedBar: p = new StackedBarChartProperties(); break; case Bar: case ClusteredBar: p = new ClusteredBarChartProperties(); break; default: throw new IllegalArgumentException(element.getType().toString()); } p.setShowOutlinesFlag(false); return p; } } private AxisChartTypeProperties computeLineChartProperties(PivotChartReportElement element, int dpi) { List<PivotTableData.Axis> series = element.getContent().getData().getRootSeries().getLeaves(); Stroke[] stroke = new Stroke[series.size()]; Shape[] shape = new Shape[series.size()]; int si = 0; for (PivotTableData.Axis s : series) { stroke[si] = new BasicStroke(2f / 72f * dpi); shape[si] = new Rectangle2D.Double(0, 0, 4f / 72f * dpi, 4f / 72f * dpi); si++; } return new LineChartProperties(stroke, shape); } }