Crosstabs

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-09. A grouped report prints one row per record. A manager of the Bookshelf Billing service, however, wants to see revenue by category and by month at the same time: categories down the left side, months across the top and the amount in every cell, with totals on both axes. That is a pivot table, and in JasperReports it is the crosstab element. The fill engine reads the records of a dataset, finds for each record the row and the column it belongs to (the buckets), accumulates one or more measures in the cell where the two meet and in the total cells around it, and then prints the resulting grid. The number of rows and columns is not known when the template is designed: it follows the distinct values found in the data.

This page explains the parts of a crosstab, builds the Bookshelf Billing revenue grid, and then covers buckets and their ordering, measures and percentages, totals, alternate row colours, dataset runs and parameters, and page breaks. The examples were compiled and run with Java 21 and JasperReports 7.0.8, and the PDFs were checked by extracting their text and by looking at the rendered pages. All templates use the v7 JRXML format (<element kind="...">, no namespace). Bands and elements are covered in JRXML Report Design, datasets in Subreports, Datasets, Tables & Lists and styles in Styles, Templates & Markup.

Anatomy of a crosstab

The figure labels the pieces of a crosstab that has two nested row groups (category, then title), one column group (month) and one measure (revenue).

Crosstab anatomy: header cell, row and column groups, detail, subtotal and grand total cells
Part What it is

dataset

Where the records come from: the main dataset of the report by default, or a sub dataset through a datasetRun.

rowGroup, columnGroup

An axis. Any number of groups can be declared and they nest in declaration order: the first row group encloses the second, and so on. A row group has a width (the width of its header column) and a column group has a height.

bucket

Inside a group: the expression that gives the group value of each record (here $F{category}), an optional comparatorExpression and an optional orderByExpression.

header, totalHeader

The cell contents printed for each bucket value and for the group total.

measure

A value accumulated in the cells, declared like a report variable: expression, class, calculation, optional percentage.

cell

The contents of a grid cell. A cell without total attributes is a detail cell; with rowTotalGroup and/or columnTotalGroup it is a total cell.

headerCell, titleCell, whenNoDataCell

The top left corner; a title above the column headers; the contents printed instead of the crosstab when the dataset has no records.

parameter

A value passed from the enclosing report into the crosstab, readable as $P{name} inside it.

The crosstab calculation engine works by iterating the records of the dataset, so its cost grows with the number of records and its memory with the number of distinct bucket values. The configuration property net.sf.jasperreports.crosstab.bucket.measure.limit limits the number of buckets to avoid out of memory errors; its default is 0, which the configuration reference documents as no limit, and it was not exercised for this page. When the database can do the aggregation, a GROUP BY query that returns one record per cell is cheaper than shipping every record to the crosstab. The crosstab is nevertheless an in-memory pivot over a dataset, not an OLAP engine: the Mondrian/XMLA data sources that older books present next to crosstabs are retired (finding O24, see Data Sources & Query Executers).

Maven dependencies and data

The crosstab element is part of the core jasperreports artifact. The four dependencies used throughout this section (explained in Getting Started with Spring Boot) are enough for every example here.

<dependencies>
  <dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports</artifactId>
    <version>7.0.8</version>
  </dependency>
  <dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-pdf</artifactId>
    <version>7.0.8</version>
  </dependency>
  <dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-fonts</artifactId>
    <version>7.0.8</version>
  </dependency>
  <dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-jdt</artifactId>
    <version>7.0.8</version>
  </dependency>
</dependencies>

The records are the lines of invoices flattened to one record per book sold. They are plain Java beans with getters, because the bean data source reads properties through getters: a Java record fails with an Error retrieving field value from bean message with 7.0.8, so the class below is an ordinary class (the workarounds are in Generating Reports from Databases & Services).

package bookshelf;

import java.math.BigDecimal;

public class SaleLine {

    private final String category;
    private final String month;
    private final String title;
    private final BigDecimal amount;

    public SaleLine(String category, String month, String title, BigDecimal amount) {
        this.category = category;
        this.month = month;
        this.title = title;
        this.amount = amount;
    }

    public String getCategory() {
        return category;
    }

    public String getMonth() {
        return month;
    }

    public String getTitle() {
        return title;
    }

    public BigDecimal getAmount() {
        return amount;
    }
}
package bookshelf;

import java.math.BigDecimal;
import java.util.List;

public final class SalesSample {

    private SalesSample() {
    }

    public static List<SaleLine> lines() {
        return List.of(
                new SaleLine("Fiction", "2026-07", "Dune", new BigDecimal("114.00")),
                new SaleLine("Fiction", "2026-08", "Neuromancer", new BigDecimal("61.25")),
                new SaleLine("Fiction", "2026-08", "Dune", new BigDecimal("85.50")),
                new SaleLine("Fiction", "2026-09", "Dune", new BigDecimal("142.50")),
                new SaleLine("Software", "2026-07", "The Pragmatic Programmer", new BigDecimal("199.50")),
                new SaleLine("Software", "2026-08", "Clean Architecture", new BigDecimal("136.00")),
                new SaleLine("Software", "2026-09", "Clean Architecture", new BigDecimal("204.00")),
                new SaleLine("Software", "2026-09", "The Pragmatic Programmer", new BigDecimal("119.70")),
                new SaleLine("Science", "2026-07", "Cosmos", new BigDecimal("96.00")),
                new SaleLine("Science", "2026-09", "Cosmos", new BigDecimal("120.00")),
                new SaleLine("Poetry", "2026-08", "Leaves of Grass", new BigDecimal("54.00")),
                new SaleLine("Poetry", "2026-09", "Leaves of Grass", new BigDecimal("36.00")));
    }
}

Poetry has no sales in 2026-07 on purpose: it shows what a crosstab prints for a pair of buckets without records.

A first crosstab

The first template reads the main data source of the report and puts the crosstab in the summary band. In v7 JRXML the title and summary bands are children of the report element directly, without a wrapping band element, and the crosstab is an <element kind="crosstab">. The template is saved as src/main/resources/monthly-sales-crosstab.jrxml.

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport name="monthly-sales-crosstab" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40"
              whenNoDataType="AllSectionsNoDetail">
  <style name="Zebra" mode="Opaque">
    <conditionalStyle backcolor="#F0F0F0">
      <conditionExpression><![CDATA[$V{IS_EVEN_ROW}]]></conditionExpression>
    </conditionalStyle>
  </style>
  <field name="category" class="java.lang.String"/>
  <field name="month" class="java.lang.String"/>
  <field name="amount" class="java.math.BigDecimal"/>
  <title height="40">
    <element kind="staticText" x="0" y="0" width="515" height="24" fontSize="16.0" bold="true">
      <text><![CDATA[Bookshelf Billing: revenue by category and month]]></text>
    </element>
  </title>
  <summary height="200">
    <element kind="crosstab" x="0" y="0" width="515" height="200">
      <dataset/>
      <headerCell>
        <element kind="staticText" x="0" y="0" width="90" height="20" bold="true" hTextAlign="Center" vTextAlign="Middle">
          <text><![CDATA[Category]]></text>
        </element>
      </headerCell>
      <rowGroup name="CategoryRow" totalPosition="End" width="90">
        <bucket order="Descending" class="java.lang.String">
          <expression><![CDATA[$F{category}]]></expression>
          <orderByExpression><![CDATA[$V{Revenue}]]></orderByExpression>
        </bucket>
        <header mode="Opaque" backcolor="#E1F0FF">
          <element kind="textField" x="0" y="0" width="90" height="26" vTextAlign="Middle" bold="true">
            <expression><![CDATA[$V{CategoryRow}]]></expression>
          </element>
        </header>
        <totalHeader mode="Opaque" backcolor="#FFF3D6">
          <element kind="staticText" x="0" y="0" width="90" height="26" vTextAlign="Middle" bold="true">
            <text><![CDATA[Total]]></text>
          </element>
        </totalHeader>
      </rowGroup>
      <columnGroup name="MonthColumn" totalPosition="End" height="20">
        <bucket class="java.lang.String">
          <expression><![CDATA[$F{month}]]></expression>
        </bucket>
        <header mode="Opaque" backcolor="#E1F0FF">
          <element kind="textField" x="0" y="0" width="85" height="20" hTextAlign="Center" vTextAlign="Middle" bold="true">
            <expression><![CDATA[$V{MonthColumn}]]></expression>
          </element>
        </header>
        <totalHeader mode="Opaque" backcolor="#FFF3D6">
          <element kind="staticText" x="0" y="0" width="85" height="20" hTextAlign="Center" vTextAlign="Middle" bold="true">
            <text><![CDATA[Total]]></text>
          </element>
        </totalHeader>
      </columnGroup>
      <measure name="Revenue" calculation="Sum" class="java.math.BigDecimal">
        <expression><![CDATA[$F{amount}]]></expression>
      </measure>
      <measure name="Share" calculation="Sum" class="java.math.BigDecimal" percentageType="GrandTotal">
        <expression><![CDATA[$F{amount}]]></expression>
      </measure>
      <whenNoDataCell>
        <element kind="staticText" x="0" y="0" width="300" height="22">
          <text><![CDATA[No sales for the selected period]]></text>
        </element>
      </whenNoDataCell>
      <cell width="85" height="26">
        <contents style="Zebra">
          <element kind="textField" x="0" y="0" width="85" height="14" hTextAlign="Right" blankWhenNull="true" pattern="#,##0.00">
            <expression><![CDATA[$V{Revenue}]]></expression>
          </element>
          <element kind="textField" x="0" y="14" width="85" height="10" hTextAlign="Right" fontSize="7.0" blankWhenNull="true" pattern="0.0&apos; %&apos;">
            <expression><![CDATA[$V{Share}]]></expression>
          </element>
        </contents>
      </cell>
      <cell width="85" height="26" rowTotalGroup="CategoryRow">
        <contents mode="Opaque" backcolor="#FFF3D6">
          <element kind="textField" x="0" y="0" width="85" height="14" hTextAlign="Right" bold="true" blankWhenNull="true" pattern="#,##0.00">
            <expression><![CDATA[$V{Revenue}]]></expression>
          </element>
          <element kind="textField" x="0" y="14" width="85" height="10" hTextAlign="Right" fontSize="7.0" blankWhenNull="true" pattern="0.0&apos; %&apos;">
            <expression><![CDATA[$V{Share}]]></expression>
          </element>
        </contents>
      </cell>
      <cell width="85" height="26" columnTotalGroup="MonthColumn">
        <contents mode="Opaque" backcolor="#FFF3D6">
          <element kind="textField" x="0" y="0" width="85" height="14" hTextAlign="Right" bold="true" blankWhenNull="true" pattern="#,##0.00">
            <expression><![CDATA[$V{Revenue}]]></expression>
          </element>
          <element kind="textField" x="0" y="14" width="85" height="10" hTextAlign="Right" fontSize="7.0" blankWhenNull="true" pattern="0.0&apos; %&apos;">
            <expression><![CDATA[$V{Share}]]></expression>
          </element>
        </contents>
      </cell>
      <cell width="85" height="26" rowTotalGroup="CategoryRow" columnTotalGroup="MonthColumn">
        <contents mode="Opaque" backcolor="#FFE2A8">
          <element kind="textField" x="0" y="0" width="85" height="14" hTextAlign="Right" bold="true" blankWhenNull="true" pattern="#,##0.00">
            <expression><![CDATA[$V{Revenue}]]></expression>
          </element>
          <element kind="textField" x="0" y="14" width="85" height="10" hTextAlign="Right" fontSize="7.0" blankWhenNull="true" pattern="0.0&apos; %&apos;">
            <expression><![CDATA[$V{Share}]]></expression>
          </element>
        </contents>
      </cell>
    </element>
  </summary>
</jasperReport>

The Java side is an ordinary fill: compile, fill with the collection as data source, export. The runner also holds the second example of this page, so it is shown once here (saved as bookshelf/CrosstabReports.java).

package bookshelf;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;

public class CrosstabReports {

    private static JasperReport compile(String resource) throws Exception {
        try (InputStream in = CrosstabReports.class.getResourceAsStream(resource)) {
            return JasperCompileManager.compileReport(in);
        }
    }

    /** The crosstab reads the main data source of the report. */
    public static byte[] monthlySales() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        parameters.put(JRParameter.REPORT_LOCALE, Locale.ENGLISH);
        JasperPrint print = JasperFillManager.fillReport(
                compile("/monthly-sales-crosstab.jrxml"),
                parameters,
                new JRBeanCollectionDataSource(SalesSample.lines()));
        return JasperExportManager.exportReportToPdf(print);
    }

    /** The crosstab reads a sub dataset fed by the SALES parameter. */
    public static byte[] salesByTitle() throws Exception {
        Map<String, Object> parameters = new HashMap<>();
        parameters.put(JRParameter.REPORT_LOCALE, Locale.ENGLISH);
        parameters.put("SALES", new JRBeanCollectionDataSource(SalesSample.lines()));
        JasperPrint print = JasperFillManager.fillReport(
                compile("/sales-by-title-crosstab.jrxml"), parameters, new JREmptyDataSource(1));
        return JasperExportManager.exportReportToPdf(print);
    }

    public static void main(String[] args) throws Exception {
        Files.write(Path.of("monthly-sales-crosstab.pdf"), monthlySales());
        Files.write(Path.of("sales-by-title-crosstab.pdf"), salesByTitle());
    }
}

The structure of the template follows the JasperReports crosstabs sample, translated to v7 JRXML.

The result was checked by extracting the text of the PDF and looking at the rendered page: the categories come out as Software (659.20), Fiction (403.25), Science (216.00) and Poetry (90.00), the grand total is 1,368.45 and the cells show the amount with the share of the grand total below it. The points worth knowing about the template follow.

  • whenNoDataType="AllSectionsNoDetail" on the report is needed because the crosstab is in the summary band: without data the report would otherwise print nothing at all. With an empty collection the output of this template is the title and the whenNoDataCell text, which was checked.

  • The <dataset/> element is empty, so the crosstab uses the main dataset. The dataPreSorted attribute of dataset tells the engine that the records already arrive sorted by the row and column groups. Here they do not, so it is left at its default and the engine sorts while it aggregates.

  • Every detail cell needs a width and height, and the header of a row group has to match the height of the cells next to it. A text field has to fit its element: a field 11 units high with the default 10 point font printed nothing in 7.0.8, and the amount only appeared when the cell was made 26 units high with a 14 unit field above a 10 unit one.

  • A sum over zero records is zero, so the empty Poetry/2026-07 pair prints 0.00 rather than a blank. To print a blank, test the value in the expression, for example $V{Revenue}.signum() == 0 ? null : $V{Revenue} together with blankWhenNull.

  • The default locale of the JVM decides the decimal separator of a pattern such as #,##0.00; the Java runner sets REPORT_LOCALE explicitly so the same PDF comes out on every machine.

  • A crosstab cell may contain any report element except subreports, charts and other crosstabs, according to the sample reference.

Buckets, comparators and ordering

A bucket turns a record into a group value. Its class is the type of the value, the expression computes it and, by default, the values are ordered by their natural ordering, ascending. Two optional expressions change that.

  • comparatorExpression must evaluate to a java.util.Comparator for the bucket class. It is the way to order values whose natural ordering is not the one wanted. The second example below uses java.lang.String.CASE_INSENSITIVE_ORDER for the category, and a reverse order, for example of the months, would be java.util.Comparator.reverseOrder() (not run for this page).

  • orderByExpression orders the buckets by another value instead of by the bucket value itself. The first example orders categories by $V{Revenue}, the revenue of the whole row, together with order="Descending" on the bucket, which is what puts Software before Poetry. The bucket order follows a measure, which gives a ranking-style grid.

JRXML 7.0.8 writes the order as Ascending or Descending on the bucket element, as observed by writing a design with the Java API and reading it back.

Measures, totals and percentages

A measure behaves like a report variable. It is declared with a class, an expression evaluated for every record and a calculation (Sum, Count, Average and the other calculations of report variables, except System). incrementerFactoryClass plugs in a custom way to combine values, for example for a type that is not numeric. A crosstab can have several measures and each is available in the cells as $V{name}.

Two kinds of percentage are available.

  • percentageType="GrandTotal" on a measure makes $V{Share} the value of the cell as a percentage of the grand total of the same measure. With a BigDecimal measure and 7.0.8 the result is on a scale of 0 to 100 (Software in 2026-07 is 14.6, the grand total cell is 100), so a pattern with a % sign, which multiplies by 100, is wrong. The first example uses the pattern 0.0' %' (the quotes make the percent sign literal) and prints 14.6 %. percentageCalculatorClass replaces the calculation with a custom class.

  • A percentage of a row or a column total is written with the built-in total variables below, in an expression.

The totals of a group exist only if the group says so: totalPosition is Start, End or None (the default). A group with a total adds one row or column of totals, and the contents of that row or column come from the totalHeader of the group and from total cells. Which total cells are needed follows from the groups: each combination of "this row group is totalled" or not with "this column group is totalled" or not needs its own cell element, identified by rowTotalGroup and columnTotalGroup.

cell attributes Where it prints In the first example

none

Detail cell: a bucket pair

Software in 2026-07

rowTotalGroup="CategoryRow"

The total row of that row group, under the detail columns

Total row at the bottom

columnTotalGroup="MonthColumn"

The total column of that column group, to the right of the detail rows

Total column on the right

both

Where the two totals cross

Grand total, 1,368.45

An important detail is which total is which when row groups are nested. The total of the outermost row group is the grand total, because it sums every bucket of that group. The total of an inner group is a subtotal of each bucket of the outer one. The second example below shows it: TitleRow is the inner group, so its totalPosition="End" produces a subtotal per category, and CategoryRow produces the grand total at the bottom. It therefore needs six cells: detail, the two subtotal cells (for TitleRow without and with the column total), the two grand total cells (for CategoryRow without and with the column total) and the column total.

The engine also publishes built-in variables for combining values from different cells.

Variable Value

$V{Revenue}

The measure in the current cell (detail or total).

$V{Revenue_MonthColumn_ALL}

The total of the measure over all the entries of the column group in the same row, that is the row total.

$V{Revenue_CategoryRow_ALL}

The total over all the entries of the row group in the same column, that is the column total.

$V{Revenue_CategoryRow_MonthColumn_ALL}

The combined total over both groups.

$V{CategoryRow}, $V{MonthColumn}

The bucket value of the current row or column group, used in the headers.

$V{ROW_COUNT}, $V{COLUMN_COUNT}

The position of the current row or column.

IS_EVEN_ROW, IS_EVEN_COLUMN

Booleans, available since 6.20.6, for alternate row and column styling.

Using them, the share of each cell in its own row is one expression in the detail cell. It was run against the data of the second example below and printed 100.0 for each cell of a row that had a single record and 0.0 for the others.

<expression><![CDATA[$V{Revenue_MonthColumn_ALL}.signum() == 0
    ? java.math.BigDecimal.ZERO
    : $V{Revenue}.multiply(java.math.BigDecimal.valueOf(100))
        .divide($V{Revenue_MonthColumn_ALL}, 1, java.math.RoundingMode.HALF_UP)]]></expression>

Alternate row colours

The first template shades every second row with a conditional style and $V{IS_EVEN_ROW}; IS_EVEN_COLUMN does the same for columns. In v7 JRXML the conditional style holds the attributes of the override directly, and the style is applied to the cell with <contents style="Zebra">.

<style name="Zebra" mode="Opaque">
  <conditionalStyle backcolor="#F0F0F0">
    <conditionExpression><![CDATA[$V{IS_EVEN_ROW}]]></conditionExpression>
  </conditionalStyle>
</style>

In the rendered first example the second and fourth rows are shaded. The condition $V{ROW_COUNT} % 2 == 0 of the sample reference gives the same result and works in versions older than 6.20.6. Total cells and headers have their own fixed mode and backcolor in the example, so the zebra does not touch them. The general mechanism of styles is in Styles, Templates & Markup.

Sub datasets, dataset runs and parameters

A crosstab placed in a detail band or beside other content usually has data of its own, different from the main dataset. It then declares a subDataset at the top of the template and references it with a datasetRun, whose dataSourceExpression (or connectionExpression) says where the records come from. The second template does that, with the records supplied by a SALES parameter, so it is filled with a single dummy record just to print the detail band once. It also shows a crosstab parameter (a value that is local to the crosstab and readable as $P{Currency}), the comparator, two nested row groups with subtotals, and keepTogether. It is saved as src/main/resources/sales-by-title-crosstab.jrxml.

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport name="sales-by-title-crosstab" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <style name="Zebra" mode="Opaque">
    <conditionalStyle backcolor="#F0F0F0">
      <conditionExpression><![CDATA[$V{IS_EVEN_ROW}]]></conditionExpression>
    </conditionalStyle>
  </style>
  <dataset name="Sales">
    <field name="category" class="java.lang.String"/>
    <field name="month" class="java.lang.String"/>
    <field name="title" class="java.lang.String"/>
    <field name="amount" class="java.math.BigDecimal"/>
  </dataset>
  <parameter name="SALES" class="net.sf.jasperreports.engine.JRDataSource"/>
  <detail>
    <band height="180">
      <element kind="crosstab" x="0" y="0" width="515" height="180">
        <dataset>
          <datasetRun subDataset="Sales">
            <dataSourceExpression><![CDATA[$P{SALES}]]></dataSourceExpression>
          </datasetRun>
        </dataset>
        <parameter name="Currency" class="java.lang.String">
          <expression><![CDATA["EUR"]]></expression>
        </parameter>
        <headerCell>
          <element kind="textField" x="0" y="0" width="240" height="20" bold="true" vTextAlign="Middle">
            <expression><![CDATA["Revenue (" + $P{Currency} + ")"]]></expression>
          </element>
        </headerCell>
        <rowGroup name="CategoryRow" totalPosition="End" width="90" keepTogether="true">
          <bucket class="java.lang.String">
            <expression><![CDATA[$F{category}]]></expression>
            <comparatorExpression><![CDATA[java.lang.String.CASE_INSENSITIVE_ORDER]]></comparatorExpression>
          </bucket>
          <header mode="Opaque" backcolor="#E1F0FF">
            <element kind="textField" x="0" y="0" width="90" height="22" vTextAlign="Middle" bold="true">
              <expression><![CDATA[$V{CategoryRow}]]></expression>
            </element>
          </header>
          <totalHeader mode="Opaque" backcolor="#FFF3D6">
            <element kind="staticText" x="0" y="0" width="240" height="22" vTextAlign="Middle" bold="true">
              <text><![CDATA[Grand total]]></text>
            </element>
          </totalHeader>
        </rowGroup>
        <rowGroup name="TitleRow" totalPosition="End" width="150">
          <bucket class="java.lang.String">
            <expression><![CDATA[$F{title}]]></expression>
          </bucket>
          <header>
            <element kind="textField" x="0" y="0" width="150" height="22" vTextAlign="Middle">
              <expression><![CDATA[$V{TitleRow}]]></expression>
            </element>
          </header>
          <totalHeader mode="Opaque" backcolor="#FFF3D6">
            <element kind="textField" x="0" y="0" width="150" height="22" vTextAlign="Middle" bold="true">
              <expression><![CDATA["Subtotal " + $V{CategoryRow}]]></expression>
            </element>
          </totalHeader>
        </rowGroup>
        <columnGroup name="MonthColumn" totalPosition="End" height="20">
          <bucket class="java.lang.String">
            <expression><![CDATA[$F{month}]]></expression>
          </bucket>
          <header mode="Opaque" backcolor="#E1F0FF">
            <element kind="textField" x="0" y="0" width="65" height="20" hTextAlign="Center" vTextAlign="Middle" bold="true">
              <expression><![CDATA[$V{MonthColumn}]]></expression>
            </element>
          </header>
          <totalHeader mode="Opaque" backcolor="#FFF3D6">
            <element kind="staticText" x="0" y="0" width="65" height="20" hTextAlign="Center" vTextAlign="Middle" bold="true">
              <text><![CDATA[Total]]></text>
            </element>
          </totalHeader>
        </columnGroup>
        <measure name="Revenue" calculation="Sum" class="java.math.BigDecimal">
          <expression><![CDATA[$F{amount}]]></expression>
        </measure>
        <cell width="65" height="22">
          <contents style="Zebra">
            <element kind="textField" x="0" y="0" width="65" height="22" hTextAlign="Right" vTextAlign="Middle" pattern="#,##0.00">
              <expression><![CDATA[$V{Revenue}]]></expression>
            </element>
          </contents>
        </cell>
        <cell width="65" height="22" rowTotalGroup="TitleRow">
          <contents mode="Opaque" backcolor="#FFF3D6">
            <element kind="textField" x="0" y="0" width="65" height="22" hTextAlign="Right" vTextAlign="Middle" bold="true" pattern="#,##0.00">
              <expression><![CDATA[$V{Revenue}]]></expression>
            </element>
          </contents>
        </cell>
        <cell width="65" height="22" rowTotalGroup="TitleRow" columnTotalGroup="MonthColumn">
          <contents mode="Opaque" backcolor="#FFF3D6">
            <element kind="textField" x="0" y="0" width="65" height="22" hTextAlign="Right" vTextAlign="Middle" bold="true" pattern="#,##0.00">
              <expression><![CDATA[$V{Revenue}]]></expression>
            </element>
          </contents>
        </cell>
        <cell width="65" height="22" rowTotalGroup="CategoryRow">
          <contents mode="Opaque" backcolor="#FFF3D6">
            <element kind="textField" x="0" y="0" width="65" height="22" hTextAlign="Right" vTextAlign="Middle" bold="true" pattern="#,##0.00">
              <expression><![CDATA[$V{Revenue}]]></expression>
            </element>
          </contents>
        </cell>
        <cell width="65" height="22" columnTotalGroup="MonthColumn">
          <contents mode="Opaque" backcolor="#FFF3D6">
            <element kind="textField" x="0" y="0" width="65" height="22" hTextAlign="Right" vTextAlign="Middle" bold="true" pattern="#,##0.00">
              <expression><![CDATA[$V{Revenue}]]></expression>
            </element>
          </contents>
        </cell>
        <cell width="65" height="22" rowTotalGroup="CategoryRow" columnTotalGroup="MonthColumn">
          <contents mode="Opaque" backcolor="#FFE2A8">
            <element kind="textField" x="0" y="0" width="65" height="22" hTextAlign="Right" vTextAlign="Middle" bold="true" pattern="#,##0.00">
              <expression><![CDATA[$V{Revenue}]]></expression>
            </element>
          </contents>
        </cell>
      </element>
    </band>
  </detail>
</jasperReport>

This template derives from the same crosstabs sample and from its Datasets section, see the references below.

The runner of the previous section fills it with new JREmptyDataSource(1) and the SALES parameter, and the result was checked in the same way: Fiction shows Dune (342.00) and Neuromancer (61.25) followed by Subtotal Fiction (403.25), the other categories follow in case-insensitive alphabetical order, and the bottom row is Grand total 1,368.45.

JasperPrint print = JasperFillManager.fillReport(
        compile("/sales-by-title-crosstab.jrxml"), parameters, new JREmptyDataSource(1));

According to the sample reference, parameters can also be passed as a whole Map through parametersMapExpression instead of being declared one by one, and charts use sub datasets in the same way as crosstabs. The complete treatment of datasetRun is in Subreports, Datasets, Tables & Lists.

Page breaks, keepTogether and layout attributes

When the crosstab does not fit on the page, it splits: a row break, a column break, or both. The attributes of the <element kind="crosstab"> control the behaviour; they were all written and loaded back with the 7.0.8 API.

Attribute Effect

repeatColumnHeaders

Print the column headers again after a row break. Default true.

repeatRowHeaders

Print the row headers again after a column break. Default true.

columnBreakOffset

Vertical space before the next piece when a column break puts it below the previous one on the same page. Default 10.

ignoreWidth

true lets the crosstab grow past its declared width without column breaks; false (default) stops at the width and continues with the remaining columns later. The property net.sf.jasperreports.crosstab.ignore.width sets the default for the report.

runDirection

LTR (default) or RTL fill direction.

horizontalPosition

Horizontal position of the crosstab inside its element (Left, Center or Right); not rendered for this page.

position on a row group

Where the header sits when row groups enclose each other: Top, Middle, Bottom or Stretch.

position on a column group

Left, Center, Right or Stretch.

mergeHeaderCells

Boolean child element of a row or column group, written and read by 7.0.8 (JRCrosstabGroup.getMergeHeaderCells() in the API); its effect was not tested for this page.

crosstabHeader in a column group, titleCell

The release notes describe them as cells for column group headers in the top left corner, and as a title cell that aligns content with dynamic width crosstabs. Both were written and loaded back, but not rendered.

The sample reference documents the row and column group attribute as headerPosition, which is how it was written in older versions; 7.0.8 writes and reads position.

keepTogether is a boolean on a row group, added in 7.0.7 according to the release notes ("new keepTogether flag for crosstab row groups"). The release notes say no more, so the behaviour was checked on this page with a variant of the second template in which the page was made small and the data had Fiction with 3 titles, Software with 6 and Science with 2: with keepTogether="false" the first page ended in the middle of the Software rows (at page heights 300, 340 and 380), and with keepTogether="true" the whole Software block moved to the second page and the first page ended after the Science subtotal. A block that is taller than a page still has to split.

<rowGroup name="CategoryRow" totalPosition="End" width="90" keepTogether="true">

Placing the crosstab in the summary band, as the first example does, is the simplest way to let it take as many pages as it needs. A crosstab in a detail band is repeated for every record of the report, and each repetition is a separate crosstab with its own pieces.

Findings and migration

  • JRXML written for versions before 7.0 uses the old XSD format and does not load in v7; crosstabs in such files have to be converted with Jaspersoft Studio 7 (finding O15). The v7 serialization is the kind="crosstab" element with child elements such as measure, cell and contents shown above.

  • The book material on JasperReports predates both the Jackson JRXML and the later crosstab additions (IS_EVEN_ROW in 6.20.6, keepTogether in 7.0.7); the books were consulted for scope only and nothing from them is reproduced.

  • The Mondrian/XMLA OLAP data sources are retired (finding O24); the crosstab aggregates the records of a dataset in memory.