Skip to main content

Report Layout Custom Visualization Pattern

Overview

A widget used as a custom visualization inside a Protrak Report Layout. The platform pre-aggregates the report data and passes both the summarized rows (data) and the raw underlying rows (totalData) to the widget. The widget renders a custom table or chart from data, and integrates with the platform's built-in drill-down mechanism to let users click any numeric cell and see the raw details.

This is distinct from the Dashboard: totalData + Pipe Parsing pattern. In the Report Layout pattern:

  • data = platform-aggregated summary rows (ready to display)
  • totalData = raw underlying records (for drill-down)
  • config.group.attribute.attributeName = the attribute used as the row key
  • onClick(...) = triggers the drill-down panel
  • ShowDrillDownData(...) = renders the drill-down panel

When to Use

  • You need a completely custom table layout for a report (e.g., conditional row colors, calculated columns, trend indicators)
  • The default platform report visualization does not support your required structure
  • You need clickable numeric cells that show underlying details on click

Applicable Layout Types

TargetNotes
ReportLayoutPrimary target — platform pre-aggregates data

pageContext Shape for Report Layout

{
// Pre-aggregated report summary rows
data: Array<{
groupName?: string, // Pipe-delimited (if used as row key source)
[attributeName]: any, // Aggregated field values (sums, counts, etc.)
id?: string,
}>,

// Raw underlying records — used by ShowDrillDownData when user drills down
totalData: Array<object>,

// Report configuration from admin
config: {
group: {
attribute: {
attributeName: string, // The attribute used as the grouping/row key
},
},
fields: Array<{ // (Optional) custom field config for drill-down
attributeName: string,
label: string,
type?: string, // 'link' for clickable name column
isClickable?: boolean,
isRedirect?: boolean,
attributeType?: string,
}>,
},

// Drill-down state — managed by platform, widget just reads/sets it
showDrilldown: boolean,
setShowDrilldown: (value: boolean) => void,

// Called by widget when user clicks a numeric cell to trigger drill-down
onClick: (params: {
filterValue: any, // Value of the group/row key for the clicked row
columnValue: any, // Numeric value in the clicked cell
columnName: string, // Column key name
}) => void,

// Passed straight through to ShowDrillDownData
filters: object,
typeName: string, // (some widgets use instanceType instead)
instanceType?: string,
sortState: object,
onSortApplied: function,
typePluralName: string,
states: object,
}

Pattern Structure

  1. Destructure data, config, totalData, showDrilldown, setShowDrilldown, onClick, plus the pass-through drill-down props from pageContext
  2. Get aggregator = config.group.attribute.attributeName — the key of each summary row
  3. Render ShowDrillDownData(...) at the top of the JSX (invisible when showDrilldown is false)
  4. Render a custom table from data rows
  5. For each numeric cell, render a Button with appearance={ButtonEnums.Appearance.Link} and onClick calling onClick({ filterValue: row[aggregator], columnValue: value, columnName: key })

Code Example

function LeadIndicatorReport(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const {
data,
config,
showDrilldown,
setShowDrilldown,
totalData,
filters,
typeName,
sortState,
onSortApplied,
typePluralName,
states,
onClick,
} = pageContext;

const { Button, ButtonEnums, ShowDrillDownData } = protrakComponents;
const aggregator = config.group.attribute.attributeName;

const columns = [
{ key: 'leadIndicators', label: 'Lead Indicators' },
{ key: 'LastWeek', label: 'Last Week', type: 'number' },
{ key: 'Cumulative', label: 'Cumulative', type: 'number' },
{ key: 'trend', label: 'Trend' },
];

// Derive a calculated column before rendering
const rows = data.map((item) => ({
...item,
trend: item.LastWeek > 0 ? 'Uptrend' : 'Constant',
}));

const headerStyle = {
border: '1px solid #dddddd',
textAlign: 'center',
padding: '8px',
background: 'rgb(188, 116, 16)',
color: '#fff',
fontWeight: 'bold',
};
const cellStyle = { border: '1px solid #dddddd', padding: '8px' };
const trendUpStyle = {
...cellStyle,
backgroundColor: '#6cef6c',
fontWeight: 'bold',
};
const trendFlatStyle = {
...cellStyle,
backgroundColor: '#fcf951',
fontWeight: 'bold',
};

return (
<div>
{/* Drill-down panel — call as a function, not JSX */}
{ShowDrillDownData(
showDrilldown,
totalData,
config,
filters,
typeName,
setShowDrilldown,
sortState,
onSortApplied,
typePluralName,
states
)}

<table
style={{
border: '1px solid grey',
borderCollapse: 'collapse',
width: '100%',
fontFamily: 'inherit',
}}
>
<thead style={{ position: 'sticky', top: 0 }}>
<tr>
{columns.map((col) => (
<th key={col.key} style={headerStyle}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row[aggregator]}>
{columns.map((col) => {
const value = row[col.key];

if (col.type === 'number') {
return (
<td key={col.key} style={cellStyle}>
{/* Clickable numeric cell triggers drill-down */}
<Button
appearance={ButtonEnums.Appearance.Link}
text={value}
onClick={() =>
onClick({
filterValue: row[aggregator],
columnValue: value,
columnName: col.key,
})
}
/>
</td>
);
}

if (col.key === 'trend') {
return (
<td
key={col.key}
style={
value === 'Uptrend' ? trendUpStyle : trendFlatStyle
}
>
{value}
</td>
);
}

return (
<td key={col.key} style={cellStyle}>
{value}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}

ShowDrillDownData — Signature

Called as a function, not as a JSX element:

ShowDrillDownData(
showDrilldown, // boolean — show or hide the panel
totalData, // raw records to display
config, // report config (pass through unchanged)
filters, // active filters (pass through unchanged)
typeName, // type name string (some widgets use 'instanceType')
setShowDrilldown, // setter to close the panel
sortState, // sort state (pass through unchanged)
onSortApplied, // sort handler (pass through unchanged)
typePluralName, // plural display name (pass through unchanged)
states // lifecycle states (pass through unchanged)
);

Note: Some widgets use instanceType instead of typeName as the 6th argument. Use whichever is destructured from pageContext.

Custom Drill-Down Config

You can override the columns shown in the drill-down panel by mutating or spreading config with a custom fields array:

const drilldownConfig = {
...config,
fields: [
{
attributeName: 'Name',
label: 'Element Name',
type: 'link',
isClickable: true,
isRedirect: true,
attributeType: 'text',
},
{
attributeName: 'delayReason',
label: 'Delay Reason',
attributeType: 'text',
},
{
attributeName: 'Planning (Min.)',
label: 'Planning (Min.)',
attributeType: 'number',
},
],
};

// Then pass drilldownConfig instead of config to ShowDrillDownData
{
ShowDrillDownData(
showDrilldown,
totalData,
drilldownConfig,
filters,
typeName,
setShowDrilldown,
sortState,
onSortApplied,
typePluralName,
states
);
}

ExportCSVButton — Export Table Data

Many report widgets include a CSV export button. Use ExportCSVButton from protrakComponents:

const { ExportCSVButton } = protrakComponents;

// headers: array of { key: string, attributeType: string }
const csvHeaders = [
{ key: 'leadIndicators', attributeType: 'Text' },
{ key: 'LastWeek', attributeType: 'Numeric' },
{ key: 'Cumulative', attributeType: 'Numeric' },
];

<ExportCSVButton
data={rows} // Array of row objects
headers={csvHeaders} // Column config
filename="LeadIndicatorReport"
/>;

Report Layout vs Dashboard: totalData Pattern

AspectReport LayoutDashboard: totalData
data✅ Pre-aggregated summary rows❌ Not present
totalData✅ Raw records (for drill-down)✅ Pre-aggregated rows (parse groupName)
config✅ Report config object❌ Not present
onClick✅ Triggers drill-down❌ Not present
ShowDrillDownData✅ Required for drill-down❌ Not applicable
Drill-downBuilt-in via showDrilldownNot available
CSV exportVia ExportCSVButtonCan also use ExportCSVButton

Real Examples

  • LeadIndicatorReport.js — Construction360, MountMeru
  • CSADocumentReport.js — Construction360, MountMeru
  • InventoryMaterialConsumedVsPlannedCustomWidget.js — MountMeru
  • DelayReasonReportWidget.js — Buildcast, DevinciPrecastPOC
  • ProjectSummaryCustomReport.js, ProjectSummaryCustomReportProject.js — Buildcast, DevinciPrecastPOC