<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>FlexTable: record-grouping-total-rows</title>
<link href="https://developer.temenos.com/uux/base.css" rel="stylesheet" />
<script src="https://developer.temenos.com/uux/unified-ux-web.min.js"></script>
<style>
uwc-flex-table {
--uwc-flex-table-record-group-anno-background-color: whitesmoke;
}
</style>
<script>
/*
* Our "raw" data records (as might be received from an API call, for instance)
*/
const dataRecords = [
{
category: "X",
item: "X-001",
a: 2.8,
b: 3.6,
c: 2
},
{
category: "X",
item: "X-002",
a: 2.4,
b: 3.1,
c: 4
},
{
category: "X",
item: "X-003",
a: 1.9,
b: 3.12,
c: 3
},
{
category: "Y",
item: "Y-001",
a: 2.8,
b: 3.6,
c: 2
},
{
category: "Y",
item: "Y-002",
a: 2.4,
b: 3.1,
c: 4
},
{
category: "Y",
item: "Y-003",
a: 1.9,
b: 3.12,
c: 3
},
{
category: "Y",
item: "Y-004",
a: 1.8,
b: 3.13,
c: 4
},
{
category: "Z",
item: "Z-001",
a: 2.8,
b: 3.6,
c: 2
},
{
category: "Z",
item: "Z-002",
a: 2.4,
b: 3.1,
c: 4
}
];
/*
* Constants for field-names *added* to each element of dataRecords[] by preprocessDataRecords() below
*/
const ExtendedRecordFieldNames = Object.freeze({
/*
* The sortFieldName for the columnSpec with fieldName: "category".
* Referenced as the *primary* "base sort key" in our sortSpec.
*/
SORTABLE_CATEGORY: "sortableCategory",
/*
* This is set false for data records, and true for grouping summary records.
*
* Referenced as the *secondary* "base sort key" in our sortSpec (to ensure that table rows for category grouping summary rows
* ALWAYS come AFTER the data records for that category *regardless* of any additional column-sorts may have been applied by
* the user (true > false))
*/
IS_GROUPING_SUMMARY_ROW: "isGroupingSummaryRow"
});
const columnSpecs = [
{
label: "Item",
fieldName: "item"
},
{
label: "Category",
fieldName: "category",
sortFieldName: ExtendedRecordFieldNames.SORTABLE_CATEGORY
},
{
label: "A",
fieldName: "a",
horizontalAlignment: "end",
renderFieldValueAsHtml: true,
},
{
label: "B",
fieldName: "b",
horizontalAlignment: "end",
renderFieldValueAsHtml: true
},
{
label: "C",
fieldName: "c",
horizontalAlignment: "end",
renderFieldValueAsHtml: true
}
];
/*
* Preprocesses dataRecords[], ensuring this is sorted in ascending order of category, augmenting the original records with additional fields
* (per ExtendedRecordFieldNames above), tracking totals values for summarizable fields for the current category / remembering insert
* position + details for each insertable (per category) group summary row on change of category, and finally (on completion of the above)
* inserting the per-category group summary records at the appropriate indices.
*/
function preprocessDataRecords() {
const
iFinalDataRecord = dataRecords.length - 1,
/*
* A map of group summary record objects keyed by the index at which each needs to be inserted (relative
* to dataRecords[] "as is" - i.e. *prior* to injection of any group summary records)
*/
groupSummaryRecordByInsertIndex = new Map(),
/*
* Used below to track total values for the grouping category that we're currently processing.
*/
fieldTotalsForCurrentCategory = {
a: 0,
b: 0,
c: 0
},
/*
* Local helper function to increment the fields in fieldTotalsForCurrentCategory with the corresponding
* values from a given dataRecord[] object.
*/
incrementFieldTotalsForCurrentCategory = (a, b, c) => {
fieldTotalsForCurrentCategory.a += a;
fieldTotalsForCurrentCategory.b += b;
fieldTotalsForCurrentCategory.c += c;
},
/*
* Local helper function to zeroize the fields in fieldTotalsForCurrentCategory.
*/
zeroizeFieldTotalsForCurrentCategory = () => {
fieldTotalsForCurrentCategory.a =
fieldTotalsForCurrentCategory.b =
fieldTotalsForCurrentCategory.c = 0;
},
/*
* Local helper function to generate the html for a supplied (grouping summary field) label and value
*/
genGroupSummaryValueHtml = (label, value) => `<span style="color: darkblue"; font-weight: 500">${label}: </span>${value.toFixed(2)}`
let
currentCategory = undefined,
recordCountForCurrentCategory = 0;
// Pre-sort our dataRecords into ascending order of category (our record grouping field)
dataRecords.sort((a, b) => a.category.localeCompare(b.category));
/*
* Iterate our (sorted) dataRecords, on each iteration:
*
* - preprocessing the original data record:
* - convert numeric values to fixed decimal format (2 decimal places)
* - extend the original record with additional fields (named / described in ExtendedRecordFieldNames above)
*
* - tracking totals values for category group summarizable fields / adding entries to our groupSummaryRecordByInsertIndex map
* (one for each grouping summary "pseudo" record that subsequently we'll subsequently need to inject into dataRecords[])
* as appropriate
*/
for(let iDataRecord = 0; iDataRecord <= iFinalDataRecord; ++iDataRecord) {
const
dataRecord = dataRecords[iDataRecord],
isFinalDataRecord = (iDataRecord === iFinalDataRecord),
{ category, a, b, c } = dataRecord,
isCategoryChange = (!!currentCategory && (category !== currentCategory));
/*
* Augment the original data record with additional fields / convert numeric values to fixed decimal (2 dp)...
*/
Object.assign(
dataRecord,
{
[ExtendedRecordFieldNames.SORTABLE_CATEGORY]: category,
[ExtendedRecordFieldNames.IS_GROUPING_SUMMARY_ROW]: false,
a: a.toFixed(2),
b: b.toFixed(2),
c: c.toFixed(2),
}
);
if (isCategoryChange || isFinalDataRecord) {
let groupSummaryRowInsertIndex = iDataRecord;
if (isFinalDataRecord) {
if (! isCategoryChange) {
/*
* Either (a) the category of our final record matches that of the previous dataRecord, OR
* b) there was *no* previous record (i.e. dataRecord is the *sole* element of dataRecords[])
*/
++recordCountForCurrentCategory;
}
else {
/*
* The category of our final record differs from that of the previous record.
*
* 1. Add an entry to groupSummaryRecordByInsertIndex representing the insert position / group summary record for
* the category of the *previous* record...
*/
groupSummaryRecordByInsertIndex.set(
groupSummaryRowInsertIndex,
{
sortableCategory: currentCategory || category,
[ExtendedRecordFieldsNames.IS_GROUPING_SUMMARY_ROW]: true,
a: genGroupSummaryValueHtml("Total", fieldTotalsForCurrentCategory.a),
b: genGroupSummaryValueHtml("Total", fieldTotalsForCurrentCategory.b),
c: genGroupSummaryValueHtml("Mean", fieldTotalsForCurrentCategory.c / recordCountForCurrentCategory)
}
);
/*
* 2. Set things up such that subsequent code will add a (further) groupSummaryRecordByInsertIndex entry for
* representing the insert position / group summary record for category of our *final* dataRecord (which is
* the *sole* record with that category)
*/
currentCategory = category;
recordCountForCurrentCategory = 1;
++groupSummaryRowInsertIndex;
zeroizeFieldTotalsForCurrentCategory();
} // else [! isCategoryChange]
incrementFieldTotalsForCurrentCategory(a, b, c);
} // if (isFinalDataRecord)
groupSummaryRecordByInsertIndex.set(
groupSummaryRowInsertIndex,
{
sortableCategory: currentCategory || category,
[ExtendedRecordFieldNames.IS_GROUPING_SUMMARY_ROW]: true,
a: genGroupSummaryValueHtml("Total", fieldTotalsForCurrentCategory.a),
b: genGroupSummaryValueHtml("Total", fieldTotalsForCurrentCategory.b),
c: genGroupSummaryValueHtml("Mean", fieldTotalsForCurrentCategory.c / recordCountForCurrentCategory)
}
);
if (isFinalDataRecord)
break; // for each dataRecords[] element
recordCountForCurrentCategory = 0;
zeroizeFieldTotalsForCurrentCategory();
} // if (isCategoryChange || isFinalDataRecord)
incrementFieldTotalsForCurrentCategory(a, b, c);
currentCategory = category;
++recordCountForCurrentCategory;
} // for each dataRecords[] element
const summaryRecordMapEntriesArray = Array.from(groupSummaryRecordByInsertIndex.entries());
/*
* Finally, inject our group-summary records (processing summaryRecordMapEntriesArray in *reverse* order
* so that, on each iteration, the insertIndex that stored (above) as the key of the current entry *remains*
* correct regardless of any insertions performed on preceding iterations).
*/
for(let iEntry = summaryRecordMapEntriesArray.length - 1; iEntry >= 0; --iEntry) {
const [ insertIndex, groupSummaryRecord ] = summaryRecordMapEntriesArray[iEntry];
dataRecords.splice(insertIndex, 0, groupSummaryRecord);
}
return dataRecords;
} // preprocessDataRecords()
addEventListener("DOMContentLoaded", () => {
const uwcFlexTableElem = document.querySelector("uwc-flex-table");
uwcFlexTableElem.tableSpec = {
primaryKeyFieldName: "item",
columnSpecs: columnSpecs,
dataRecords: preprocessDataRecords(),
recordGroupAnnotationRowOptions: {
annoFieldNameByPrimarySortFieldName: {
[ExtendedRecordFieldNames.SORTABLE_CATEGORY]: ExtendedRecordFieldNames.SORTABLE_CATEGORY
},
expandableCollapsible: true
}
};
uwcFlexTableElem.sortSpec = {
// This is our primary "base sort" key
[`${ExtendedRecordFieldNames.SORTABLE_CATEGORY}!`]: "ascending",
// This secondary "base sort" key ensures that rows for category group summary records always appear AFTER those for data records in that category
[`${ExtendedRecordFieldNames.IS_GROUPING_SUMMARY_ROW}!`]: "ascending"
};
uwcFlexTableElem.recordSelectionMode = "single";
//uwcFlexTableElem.showGlobalRecordFilterToolbar = true;
});
</script>
</head>
<body>
<uwc-flex-table _debug></uwc-flex-table>
</body>
</body>