Displaying Summary and Matrix Reports with Lightning Components

Learn how to modify the Report App to display simple summary and matrix reports using Lightning Components.

Guest Post: Daniel Peter is a Lead Applications Engineer at Kenandy, Inc.  You can reach him on Twitter @danieljpeter or www.linkedin.com/in/danieljpeter.

Thus far, this series on building Lightning Components has walked you through building Lightning Components that work with tabular reports. To this point, the Apex code provided isn’t designed to handle summary and matrix reports. Let’s fix that.

Determining the Report Type in Apex

In order to process the ReportResult properly, we need to find out what type of report it is. Depending on if it is tabular, summary, or matrix we will want to represent the data in a different user defined data type as well.

Here’s some simple code to figure this out:

public static reportResponse getReportResponse(Id reportId) {
	//get the report result
	Reports.ReportResults results = Reports.ReportManager.runReport(reportId, true);

	//get the metadata
	Reports.ReportMetadata reportMetadata = results.getReportMetadata();

	//find out what type of report it is by looking at the groupings down and groupings across
	Integer groupingsDown = 0;
	Integer groupingsAcross = 0;

	List groupingDownList = reportMetadata.getGroupingsDown();
	List groupingAcrossList = reportMetadata.getGroupingsAcross();

	if (groupingDownList != null) {
		groupingsDown = groupingDownList.size();
	}

	if (groupingDownList != null) {
		groupingsAcross = groupingAcrossList.size();
	}		

	String reportType = 'tabular';
	if ( (groupingsDown > 0) && (groupingsAcross == 0) ) {
		reportType = 'summary';	
	}

	if ( (groupingsDown > 0) && (groupingsAcross > 0) ) {
		reportType = 'matrix';	
	}		

	reportResponse rr = new reportResponse();
	rr.reportType = reportType;	

	if (reportType == 'tabular') {
		rr.tabResp = getTabularReportResponse(results);
	} else if (reportType == 'summary') {
		rr.sumResp = getSummaryReportResponse(results);
	} else if (reportType == 'matrix') {
		rr.tabResp = getMatrixReportResponse(results);
	}

	return rr;
}

The code looks at the number of groupings down and groupings across to make a quick and dirty determination as to the report type and which method needs to be called to build the report and return it in the expected format. Since we may be returning different data types I put everything in one wrapper to rule them all:

public class reportResponse {
	public String reportType {get; set;}
	public tabularReportResponse tabResp {get; set;}
	public summaryReportResponse sumResp {get; set;}		
	public reportResponse(){}
}

This wrapper will either have one or the other response type populated, with an additional “reportType” attibute which will let us know which report type it is, and which object we should be using. As you will see, all 3 report types can be represented with 2 data structures: tabular and summary. Of course this will only support one level of grouping for tabular and matrix, but that will buy us quite a bit of functionality on a mobile device.

Handling Summary Reports

Since summary data is grouped, it needs to be stored in a different data structure. Here is what I came up with:

public class summaryReportResponse {
	public List reportFields {get; set;}
	public List groupList {get; set;}
	public summaryReportResponse(){}
}	

public class summaryReportGroup {
	public String fieldName {get; set;}
	public String fieldValue {get; set;}
	public String fieldLabel {get; set;}
	public String groupKey {get; set;}
	public Boolean isHyperLink {get; set;}	
	public Integer fieldsInGroup {get; set;}					
	public List<List<fieldData>> fieldDataList {get; set;}
	public summaryReportGroup(){}
}

The summaryReportResponse type contains the same reportFields as the tabular report, but instead of a big two dimensional array of data, it has a list of summaryReportGroups. summaryReport contains some info about the group itself, then contains a similar, smaller two dimensional array as the tabular report, with just the data for that group.

Let’s look at how we build up the summary report in Apex:

public static summaryReportResponse getSummaryReportResponse(Reports.ReportResults results) {
	summaryReportResponse srr = new summaryReportResponse();
	List reportFields = new List();	

	//get the metadata
	Reports.ReportMetadata reportMetadata = results.getReportMetadata();

	//get a string array of the field names
	List fieldNames = reportMetadata.getDetailColumns();		

	//get the extended metadata
	Reports.ReportExtendedMetadata reportExtendedMetadata = results.getReportExtendedMetadata();

	//get the map of the column names to their name and label
	Map<String, Reports.DetailColumn> detailColumnMap = reportExtendedMetadata.getDetailColumnInfo();

	//get the map of the grouping column names to their name and label
	Map<String, Reports.GroupingColumn> groupingColumnMap = reportExtendedMetadata.getGroupingColumnInfo();		

	//get the grouping column info
	Reports.GroupingInfo groupingInfo = reportMetadata.getGroupingsDown()[0]; //only supports one grouping level
	Reports.GroupingColumn groupingColumnDetail = groupingColumnMap.get(groupingInfo.getName());				

	//loop over the detailColumnMap and get the name, label, and data type
	for (String fieldName: fieldNames) {
		Reports.DetailColumn detailColumn = detailColumnMap.get(fieldName);
		fieldDef fd = new fieldDef();
		fd.fieldName = detailColumn.getName(); 
		fd.fieldLabel = detailColumn.getLabel();
		fd.dataType = detailColumn.getDataType().name();
		reportFields.add(fd);
	}
	srr.reportFields = reportFields;

	//get the summary grouping down dimension grouping values.  only going 1 level deep
	List groupList = new List();
	for (Reports.GroupingValue groupingValue: results.getGroupingsDown().getGroupings()) {
		summaryReportGroup srg = new summaryReportGroup();
		srg.fieldName = groupingColumnDetail.getLabel();			
		srg.fieldValue = (String)groupingValue.getValue();
		srg.fieldLabel = groupingValue.getLabel();
		srg.groupKey = groupingValue.getKey();
		srg.isHyperLink = isHyperlink(srg.fieldValue);

		//use our group key to get the group rows from the fact map
		Reports.ReportFactWithDetails factDetails = (Reports.ReportFactWithDetails)results.getFactMap().get(srg.groupKey+'!T');	
		List reportDetailRowList = factDetails.getRows();			

		List<List<fieldData>> fieldDataList = new List<List<fieldData>>();

		//loop over the rows
		for (Reports.ReportDetailRow reportDetailRow: reportDetailRowList) {
			Integer cellCounter = 0;
			List fieldDataRow = new List();
			//loop over the cells in the row
			for (Reports.ReportDataCell reportDataCell: reportDetailRow.getDataCells()) {
				fieldData fd = new fieldData();
				fd.fieldValue = (String)reportDataCell.getValue();
				fd.fieldLabel = (String)reportDataCell.getLabel();
				fd.dataType = reportFields[cellCounter].dataType;
				fd.isHyperLink = isHyperlink(fd.fieldValue);
				cellCounter++;
				fieldDataRow.add(fd);
			}

			//add the row to the list
			fieldDataList.add(fieldDataRow);
		}			
		srg.fieldsInGroup = srr.reportFields.size();			
		srg.fieldDataList = fieldDataList;
		groupList.add(srg);
	}
	srr.groupList = groupList;

	return srr;
}

You can see we loop over the groupings down, and use those grouping keys to get the fact map by a key we construct. Then we loop over the rows and cells in that group, in the same way we would do for a tabular report.

Creating a Lightning Component to Render a Summary Report

How do we use this data to render a summary report? With another component, of course! To get started, create a new component and name it reportGroupComponent.cmp, then enter the following:

<aura:component >
	<aura:attribute name="group" type="Object"/>

    <tr>
        <td class="cell" colspan="{!v.group.fieldsInGroup}">
            <b>{!v.group.fieldName}: </b>
            <aura:renderIf isTrue="{!v.group.isHyperLink}">
                <LIGHTNINGREPORT:sobjectHyperlink sObjectId="{!v.group.fieldValue}" hyperlinkLabel="{!v.group.fieldLabel}"/>
                <aura:set attribute="else">{!v.group.fieldLabel}</aura:set>                        
            </aura:renderIf>            
        </td>
    </tr>

    <aura:iteration var="row" items="{!v.group.fieldDataList}">  
        <tr>
            <aura:iteration var="cell" items="{!row}">
                <td class="cell">
                    <aura:renderIf isTrue="{!cell.isHyperLink}">
                        <LIGHTNINGREPORT:sobjectHyperlink sObjectId="{!cell.fieldValue}" hyperlinkLabel="{!cell.fieldLabel}"/>
                        <aura:set attribute="else">{!cell.fieldLabel}</aura:set>                        
                    </aura:renderIf>                            
                </td>
            </aura:iteration>
        </tr>
    </aura:iteration>

</aura:component>

This component is called once for each group in the summary report, and gives a grouping row that spans the whole table, and then iterates over the rest of the rows in the group.

Next, change the main component to conditionally call the correct subcomponent depending on whether it is a tabular / matrix or summary report. Use the renderIf and set/else tags again to accomplish the conditional behavior:

<!-- this is how tabular and matrix reports are displayed -->
<!-- Iterate over the list of report rows and display them -->
<!-- special case for the header row -->
<table data-role="table" data-mode="columntoggle" id="report-table" class="custom-reponsive table-stroke">

    <aura:renderIf isTrue="{!v.reportResponse.reportType == 'summary' ? false : true}">
        <thead>
            <LIGHTNINGREPORT:reportRowComponent row="{!v.tabResp.reportFields}" isHeader="true"/>
        </thead>
        <tbody>
            <aura:iteration var="row" items="{!v.tabResp.fieldDataList}">
                <LIGHTNINGREPORT:reportRowComponent row="{!row}" isHeader="false"/>
            </aura:iteration>
        </tbody>
        <aura:set attribute="else">
            <!-- this is how summary reports are displayed -->
            <thead>
                <LIGHTNINGREPORT:reportRowComponent row="{!v.sumResp.reportFields}" isHeader="true"/>
            </thead>
            <tbody>
                <aura:iteration var="group" items="{!v.sumResp.groupList}">
                    <LIGHTNINGREPORT:reportGroupComponent group="{!group}"/>
                </aura:iteration>
            </tbody>

        </aura:set>
    </aura:renderIf>   

</table>

 

To view the result, navigate to a summary report and check it out.

 

Here is what the summary salesforce desktop report looks like.

And here is the Lightning / jQuery Mobile version with groupings.

 

The grouping levels even have hyperlinks, awesome!

Matrix Reports

After looking at a matrix report with one grouping in each dimension, I realized it could be shoehorned into the tabular data structure relatively easy, so I went with that approach. Here is code that can accomplish this:

public static tabularReportResponse getMatrixReportResponse(Reports.ReportResults results) {
	tabularReportResponse trr = new tabularReportResponse();
	List<fieldDef> reportFields = new List<fieldDef>();
	List<List<fieldData>> fieldDataList = new List<List<fieldData>>();		

	//get the metadata
	Reports.ReportMetadata reportMetadata = results.getReportMetadata();

	//get a string array of the field names
	List<String> fieldNames = reportMetadata.getDetailColumns();		

	//get the extended metadata
	Reports.ReportExtendedMetadata reportExtendedMetadata = results.getReportExtendedMetadata();

	//get the map of the grouping column names to their name and label
	Map<String, Reports.GroupingColumn> detailColumnMap = reportExtendedMetadata.getGroupingColumnInfo();

	//create the reportFields header row from the grouping fields

	//first add the grouping down field info
	Reports.GroupingInfo groupingInfoDown = reportMetadata.getGroupingsDown()[0]; //only supports one grouping level
	Reports.GroupingColumn groupingColumnDown = detailColumnMap.get(groupingInfoDown.getName());
	fieldDef fdGroupDown = new fieldDef();
	fdGroupDown.fieldName = groupingColumnDown.getName(); 
	fdGroupDown.fieldLabel = groupingColumnDown.getLabel();
	fdGroupDown.dataType = groupingColumnDown.getDataType().name();
	reportFields.add(fdGroupDown);

	//now add all the groupings across
	for (Reports.GroupingValue groupingValue: results.getGroupingsAcross().getGroupings()) {
		fieldDef fd = new fieldDef();
		fd.fieldName = (String)groupingValue.getValue(); 
		fd.fieldLabel = groupingValue.getLabel();
		fd.dataType = 'DOUBLE_DATA';
		reportFields.add(fd);			
	}				

	//get the matrix grouping down dimension grouping values.  only going 1 level deep
	List<summaryReportGroup> groupListDown = new List<summaryReportGroup>();
	for (Reports.GroupingValue groupingValue: results.getGroupingsDown().getGroupings()) {
		summaryReportGroup srg = new summaryReportGroup();
		srg.fieldValue = (String)groupingValue.getValue();
		srg.fieldLabel = groupingValue.getLabel();
		srg.groupKey = groupingValue.getKey();
		srg.isHyperLink = isHyperlink(srg.fieldValue);
		groupListDown.add(srg);
	}

	//get the matrix grouping across dimension grouping values.  only going 1 level deep
	List<summaryReportGroup> groupListAcross = new List<summaryReportGroup>();
	for (Reports.GroupingValue groupingValue: results.getGroupingsAcross().getGroupings()) {
		summaryReportGroup srg = new summaryReportGroup();
		srg.fieldValue = (String)groupingValue.getValue();
		srg.fieldLabel = groupingValue.getLabel();
		srg.groupKey = groupingValue.getKey();
		srg.isHyperLink = isHyperlink(srg.fieldValue);
		groupListAcross.add(srg);
	}		

	//now we need to do a nested loop of the groupings down and across to get all the data from the fact map
	for (summaryReportGroup down: groupListDown) {
		List<fieldData> fieldDataRow = new List<fieldData>();

		//first cell is the grouping down
		fieldData fd = new fieldData();
		fd.fieldValue = down.fieldValue;
		fd.fieldLabel = down.fieldLabel;
		fd.dataType = 'STRING_DATA';
		fd.isHyperLink = down.isHyperLink;
		fieldDataRow.add(fd);					

		for (summaryReportGroup across: groupListAcross) {
			//use our composite down!across key to get values from the fact map
			Reports.ReportFactWithDetails factDetails = (Reports.ReportFactWithDetails)results.getFactMap().get(down.groupKey+'!'+across.groupKey);	
			Reports.SummaryValue summaryValue = factDetails.getAggregates()[0]; //only support 1 set of aggregates

			fd = new fieldData();
			fd.fieldValue = (String)(summaryValue.getValue()+'');
			fd.fieldLabel = (String)(summaryValue.getLabel()+'');
			fd.dataType = 'DOUBLE_DATA';
			fieldDataRow.add(fd);						

		}
		//add the row to the list
		fieldDataList.add(fieldDataRow);
	}

	trr.reportFields = reportFields;
	trr.fieldDataList = fieldDataList;

	return trr;
}

To get at the core numeric data at the intersections of the matrix, we have to build up lists of our groupings down and groupings across. Then we do a nested loop, build a composite key, and get the numeric value at that location from the fact map. If you are interested in matrix report data, I would recommend doing a system.debug(JSON.serialize(ReportResult)) as I showed in my first article in the series to look more at the data. The structure is very similar to tabular and summary, but the fact map has more complex keys and more of them. For each group down we build one row to represent all the cells in the group across. One of the other tricks to fit the matrix into the tabular format is to create the report fields, which ultimately display as the header, from the grouping down and the grouping across fields. A bit of a hack, but it gets the job done.

Let’s see what the matrix report looks like.

Here is what the matrix salesforce desktop report looks like.

And here is the Lightning / jQuery Mobile version.

 

I took some liberties in the presentation of the data, as you can see, but it is still a useful matrix report on the go when you are already familiar with the report and just need a quick view while away from the office.

There is still some room for improvement. In the next post we’ll look at some styling enhancements we can make.

Lightning Component Resources

Leave your comments...

Displaying Summary and Matrix Reports with Lightning Components