Modifying a JasperReports chart at run time

To modify a report at run time, it is imperative to understand the overall process of how a jrxml ends up in its final form as an image / pdf / etc. The following steps briefly outline this process.

1. The .jrxml file is loaded into memory using a variant of JRXmlLoader’s loadXML() methods, passing it the location of the file as an argument. This method returns a JasperDesign object, which is the in-memory representation of the input jrxml, on which all the run-time modifications to the report would be made.

2. After the necessary modifications have been made, the JasperDesign file is validated and compiled using one of JasperCompileManager’ compile methods. Depending on the arguments passed, this method returns a JasperReport object or saves the generated .jasper file to the location specified as the second argument to the method. Note that when dynamic modifications of the report is not required, the jrxml could be directly compiled into a JasperReport object / .jasper file using the appropriate compile method.

3. The application then prepares the data necessary for the report as JRDataSource objects, and then calls a suitable method in the JasperFillManager class, passing it the data source and the previously compiled JasperReport object / .jasper file. The output of this step would be a JasperPrint object, which could be exported to various other user-friendly formats such as PDF and Html or serialized to disk.

As stated earlier, all the run-time modifications happen in step 1, in the JasperDesign object. To make these modifications, the programmer would directly make use of the JasperReports api, which provides various objects to represent each element of the jrxml. For example, parameters would be represented with the JRDesignParameter object, varibles would be represented as JRDesignVariable objects, and so on.
For a complete list of all available object representations of various design elements, please refer to the jasperreports docs available here. An example of creating a dynamic report directly in your code is available with each jasperreport distribution in the NoXmlDesignApp class, found in the “demo/samples/noxmldesign/src” folder.

Please note that the library’s designers strongly recommend against runtime modifications of reports unless the project absolutely necessitates it, as it has several disadvantages like the disability to use a graphical designer such as iReports, tighter coupling of code, and so on. If you want to stay away from several lines of java code, and are open to try newer libraries, especially if you are creating your reports directly in code without jrxmls, please check the DynamicJasper library, which abstracts away the complexities of the JasperReports api by making several default assumptions, which can be overridden as required.

That being said, there are several reasons why one might want to modify the report structure at runtime. A classic case would be when the report structure needs to adapt based on user input, such as limiting the number of columns in a crosstab report. Several examples of modifying tabular reports are already available. This post aims at illustrating how the same can be achieved on graphical elements such as charts.

The example below utilizes a simple report containing one graphical element, a line chart, and shows how to modify its title, subtitle, height, width, chart customizer class and styles at run time.

First off is the jrxml file for the report:

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
	name="GenericLineChart">
	<property name="author" value="Jeshurun Daniel" />
	<subDataset name="sampleDataset">
		<field name="category-titles" class="java.lang.String" />
		<field name="series-titles" class="java.lang.String" />
		<field name="data" class="java.lang.Double" />
	</subDataset>

	<parameter name="datasource"
		class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource" />
	<summary>
		<band height="750">
			<lineChart>
				<chart evaluationTime="Report"
					customizerClass="ca.jeshurun.blog.jasperreports.customizers.LineCustomizer">
					<reportElement x="0" y="0" width="555" height="350" />
					<chartTitle>
						<font size="16" isBold="true" />
						<titleExpression><![CDATA["Title"]]></titleExpression>
					</chartTitle>
					<chartSubtitle>
						<font size="14" isBold="true" />
						<subtitleExpression><![CDATA["Sub Title"]]></subtitleExpression>
					</chartSubtitle>
					<chartLegend />
				</chart>
				<categoryDataset>
					<dataset>
						<datasetRun subDataset="sampleDataset">
							<dataSourceExpression><![CDATA[$P{datasource}]]></dataSourceExpression>
						</datasetRun>
					</dataset>
					<categorySeries>
						<seriesExpression><![CDATA[$F{series-titles}]]></seriesExpression>
						<categoryExpression><![CDATA[$F{category-titles}]]></categoryExpression>
						<valueExpression><![CDATA[$F{data}]]></valueExpression>
					</categorySeries>
				</categoryDataset>
				<linePlot isShowLines="true" isShowShapes="true">
					<plot />
					<categoryAxisLabelExpression><![CDATA["Y - axis label"]]></categoryAxisLabelExpression>
					<valueAxisLabelExpression><![CDATA["X - axis label"]]></valueAxisLabelExpression>
				</linePlot>
			</lineChart>
		</band>
	</summary>
</jasperReport>

The jrxml is pretty standard with one line chart embedded in the summary band. The chart has two embedded styles and a sub-dataset.

Next lets see how to modify this report dynamically.

The first step would be to load the  jrxml into memroy, as a JasperDesign object. Please change the file location appropriately to reflect the location where you saved the above jrxml file in your computer.

JasperDesign design = JRXmlLoader.load("/home/jeshurun/Workspace/apps/jrxmls/GenericLine.jrxml");

Now that we have a Java representation of the report in memory, we can proceed with modifying it.
Lets start by changing the page size and margins.

design.setPageHeight(250);
design.setColumnWidth(390);
design.setLeftMargin(0);
design.setRightMargin(0);
design.setTopMargin(0);
design.setBottomMargin(0);
design.setPageWidth(400);

Now lets add some report level styles, which will be used by the title and subtitle elements of the chart

// create a style element and add it to the report
JRDesignStyle chartStyle = new JRDesignStyle();
chartStyle.setName("Title");
chartStyle.setDefault(false);
chartStyle.setFontName("DejaVu Sans");
chartStyle.setFontSize(10);
chartStyle.setPdfFontName("Helvetica");
chartStyle.setPdfEncoding("Cp1252");
chartStyle.setPdfEmbedded(false);
chartStyle.setBold(true);
design.addStyle(chartStyle);

Next, lets change the title and subtitle of the report. To do this, we first extract the summary band from the design object. We then loop through all its contained elements, and look for an element of type JRDesignChart. Once we have the chart element, we can change its customizer class, title and subtitle and alter its size.

// Extract the summary band
JRDesignBand summary = (JRDesignBand) design.getSummary();
JRElement[] elements = summary.getElements();
for (JRElement element : elements) { // loop through all its elements
	if (element instanceof JRDesignChart) {
		JRDesignChart chart = (JRDesignChart) element; // get the line chart
		// we first remove the existing chart element from the
		// summary band
		summary.removeElement(chart);

		// change the customizer class
		chart.setCustomizerClass("ca.jeshurun.blog.reports.customizers.DynamicLineCustomizer");
		// get the chart's existing title and subtitle expression and modify its text
		JRDesignExpression titleExpression1 = (JRDesignExpression) chart
					.getTitleExpression();
		titleExpression1.setText(""Dynamic title"");
		chart.setTitleExpression(titleExpression1);
		JRDesignFont titleFont = new JRDesignFont();
		titleFont.setBold(true);
		titleFont.setFontSize(12);
		chart.setTitleFont(titleFont);

		titleExpression1 = (JRDesignExpression) chart.getSubtitleExpression();
		titleExpression1.setText(""Dynamic sub title"");
		chart.setSubtitleExpression(titleExpression1);
		titleFont = new JRDesignFont();
		titleFont.setBold(true);
		titleFont.setFontSize(11);
		chart.setSubtitleFont(titleFont);

		// change the chart's dimensions
		chart.setHeight(200);
		chart.setWidth(350);

		// add the chart style
		chart.setStyle(chartStyle);

		//add the chart back to the summary band
		summary.addElement(chart);

		//change the summary band's size
		summary.setHeight(240);

		//add the summary band back to the chart
		design.setSummary(summary);
	}
}

The chart data is passed to the report as instances of a concrete implementation of the JRDataSource interface. JasperReports provides several default implementations of this interface. One such implementation is the JRMapCollectionDataSource which passes data to the report as a collection of maps. Each collection represents one series in the chart, with each map holding a single set of values for the category (y-axis) labels, series (x-axis) labels and the data itself. One such mock data source is created below.

String[] categoryTitles = {"Category 1", "Category 2","Category 3","Category 4","Category 5"};
Double[] dataSeries1 = {2.09,1.79,1.73,2.23,2.09};
Double[] dataSeries2 = {2.48,2.16,2.21,2.01,2.66};

Map dataMap;
ArrayList<Map<String, Object>> lineChartData = new ArrayList<Map<String, Object>>();

for(int i=0; i<5; i++){
	dataMap = new HashMap();
	dataMap.put("series-titles", "Year 1");
	dataMap.put("category-titles", categoryTitles[i]);
	dataMap.put("data", dataSeries1[i]);
	lineChartData.add(dataMap);
}

for(int i=0; i<5; i++){
	dataMap = new HashMap();
	dataMap.put("series-titles", "Year 2");
	dataMap.put("category-titles", categoryTitles[i]);
	dataMap.put("data", dataSeries2[i]);
	lineChartData.add(dataMap);
}

datasource = new JRMapCollectionDataSource(lineChartData);
paramsMap.put("datasource", datasource);

Customizer classes are JasperReports’ way of allowing the programmer to tweak less commonly used options available in the underlying charting system. The charting system used in this example is jFreeChart, the default, although it is now possible to use other charting systems with JasperReports as well. Customizer classes must implement the JRChartCustomizer interface, which mandates a single method with the signature

public void customize(JFreeChart jfreeChart, JRChart jrChart);

This method is called automatically by the library when the report is run and is passed two objects. One is JasperReports’ representation of the chart and the other is JFreeChart’s representation of the same chart. Several additional options are available under the JFreeChart’s object representation. For this example, lets rotate the category axis lables by an angle of 90 degrees, so they appear less cluttered in the chart.

package ca.jeshurun.blog.reports.customizers;

import java.io.Serializable;

import net.sf.jasperreports.engine.JRChart;
import net.sf.jasperreports.engine.JRChartCustomizer;

import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.plot.CategoryPlot;

/**
 * @author Jeshurun Daniel
 *
 */
public class DynamicLineCustomizer implements JRChartCustomizer, Serializable {

	private static final long serialVersionUID = -8493880774698206000L;

	@Override
	public void customize(JFreeChart jfreeChart, JRChart jrChart) {

		CategoryPlot plot = jfreeChart.getCategoryPlot();
		CategoryAxis domainAxis = plot.getDomainAxis();
		domainAxis.setCategoryLabelPositions(CategoryLabelPositions.createUpRotationLabelPositions(Math.PI / 2.0));

	}

}

And thats it! All that is left to do is to compile the JasperDesign object into a JasperReport object, and then fill it with data. The JasperPrint object obtained after filling can be viewed in the built-in report viewer provided by the JasperReports api.

// compile the report
JasperReport report = JasperCompileManager.compileReport(design);
// fill it with our data
JasperPrint print = JasperFillManager.fillReport(report, paramsMap,new JREmptyDataSource());
//view the report with the built-in viewer
JasperViewer.viewReport(print);

It is worthy of note here that since data is passed into the report as report parameters, it is necessary to pass in an empty datasource in the form of a JREmptyDataSource instance when the report is filled. When ignored, JasperReports displays a message indicating the report contains no data and stops further processing.

Throwing all this code in a main method and running the program results in the following chart when viewed with the built-in swing viewer:

Dynamic Report Example

The complete source code is available below. The code has been tested to work with JasperReports version 3.7.5 and up.
DynamicReport.java
DynamicLineCustomizer.java
GenericLine.jrxml

This completes my “How To”, but as you can see, the possibilities of runtime customization of a report are endless. With a little more work, the entire report can be created directly in code, a very viable option for savvy Java programmers determined to stay away from XML.

Leave a Reply

Your email address will not be published. Required fields are marked *

twelve − 3 =