12
Views
2
Comments
[DataGrid Reactive Dynamic Columns] How to create Multiple Aggregate Statistics for a Datagrid Numbers Column?
datagrid-reactive-dynamic-columns
Reactive icon
Forge asset by Henrique Silva

Hi Everyone

I would like to have multiple aggregate statistics for a datagrid numbers column. The standard one allows for only one aggregate statistic. Use case is to show min, max, mean, median and standard deviation for the column values. This is easy to do in an Excel spreadsheet, but problematic in an Outsystems  DataGrid.

I have implemented a solution using a custom datagrid column, and it broke with the latest update of the OutSystems Datagrid. Creating a custom solution makes it more troublesome for maintenance with each upgrade. 

The basic requirements are that:

1. When you sort on a column, the summary statistics should not be sorted and should stay at the bottom of the column so any suggestion to just add the statistics as extra rows of the column data would fail this sort feature.

2. It should scroll left and right when you scroll the columns of the main table.  So any suggestion to create a separate grid table would not work because the two tables will not scroll together.

3. If you shift a data column in the data grid, the statistics should shift together with the data column. Another reason why a separate table for the statistics does not work.

I understand that another forge component AG Datagrid allows for multiple statistics and is more powerful than the Outsystems Datagrid but that is entirely in javascript and everything has to be rebuilt in javascript which defeats the whole point of the Outsystems user interface.

Thanks for any ideas that would help.

T

2026-01-28 16-57-48
Mihai Melencu
Champion

Hi @Tommy Tan ,

You can use the following JS in the OnInitialize event of the grid, this will add the footer values only under that column, while still keeping them attached to the Data Grid footer area, so they will not be sorted as normal rows and will stay aligned when scrolling horizontally or moving columns. 

Result:

  1. const gridObj = OutSystems.GridAPI.GridManager.GetGridById($parameters.GridWidgetId);
  2. const grid = gridObj.provider;

  3. const targetBinding = $parameters.ColumnBinding;

  4. const statistics = [
  5.     { label: "Min", key: "min" },
  6.     { label: "Max", key: "max" },
  7.     { label: "Mean", key: "mean" },
  8.     { label: "Median", key: "median" },
  9.     { label: "Std Dev", key: "stdDev" }
  10. ];

  11. function getValueByBinding(item, binding) {
  12.     return binding.split(".").reduce((obj, key) => {
  13.         return obj ? obj[key] : undefined;
  14.     }, item);
  15. }

  16. function parseNumber(value) {
  17.     if (value === null || value === undefined || value === "") {
  18.         return null;
  19.     }

  20.     if (typeof value === "number") {
  21.         return Number.isFinite(value) ? value : null;
  22.     }

  23.     const parsed = Number(
  24.         String(value)
  25.             .replace(/\s/g, "")
  26.             .replace(",", ".")
  27.     );

  28.     return Number.isFinite(parsed) ? parsed : null;
  29. }

  30. function getNumericValues() {
  31.     const items = grid.collectionView ? grid.collectionView.items : [];

  32.     return items
  33.         .map(item => parseNumber(getValueByBinding(item, targetBinding)))
  34.         .filter(value => value !== null);
  35. }

  36. function calculateMedian(values) {
  37.     if (!values.length) {
  38.         return null;
  39.     }

  40.     const sorted = [...values].sort((a, b) => a - b);
  41.     const middle = Math.floor(sorted.length / 2);

  42.     if (sorted.length % 2 === 1) {
  43.         return sorted[middle];
  44.     }

  45.     return (sorted[middle - 1] + sorted[middle]) / 2;
  46. }

  47. function calculateStdDev(values) {
  48.     if (values.length < 2) {
  49.         return null;
  50.     }

  51.     const mean = values.reduce((sum, value) => sum + value, 0) / values.length;

  52.     const variance = values.reduce((sum, value) => {
  53.         return sum + Math.pow(value - mean, 2);
  54.     }, 0) / (values.length - 1);

  55.     return Math.sqrt(variance);
  56. }

  57. function formatNumber(value) {
  58.     if (value === null || value === undefined || Number.isNaN(value)) {
  59.         return "";
  60.     }

  61.     return Number(value).toLocaleString(undefined, {
  62.         minimumFractionDigits: 0,
  63.         maximumFractionDigits: 2
  64.     });
  65. }

  66. function calculateStatistics() {
  67.     const values = getNumericValues();

  68.     const mean = values.length
  69.         ? values.reduce((sum, value) => sum + value, 0) / values.length
  70.         : null;

  71.     return {
  72.         min: values.length ? Math.min(...values) : null,
  73.         max: values.length ? Math.max(...values) : null,
  74.         mean: mean,
  75.         median: calculateMedian(values),
  76.         stdDev: calculateStdDev(values)
  77.     };
  78. }

  79. function ensureFooterRows() {
  80.     grid.beginUpdate();

  81.     while (grid.columnFooters.rows.length < statistics.length) {
  82.         grid.columnFooters.rows.push(new wijmo.grid.Row());
  83.     }

  84.     grid.endUpdate();
  85. }

  86. function renderFooterCell(panel, rowIndex, colIndex, cell) {
  87.     if (panel !== grid.columnFooters) {
  88.         return;
  89.     }

  90.     if (rowIndex >= statistics.length) {
  91.         return;
  92.     }

  93.     const column = grid.columns[colIndex];

  94.     if (!column || column.binding !== targetBinding) {
  95.         cell.textContent = "";
  96.         return;
  97.     }

  98.     const result = calculateStatistics();
  99.     const stat = statistics[rowIndex];

  100.     cell.textContent = stat.label + ": " + formatNumber(result[stat.key]);
  101.     cell.style.textAlign = "right";
  102.     cell.style.fontWeight = "600";
  103. }

  104. ensureFooterRows();

  105. grid.formatItem.addHandler(function (s, e) {
  106.     renderFooterCell(e.panel, e.row, e.col, e.cell);
  107. });

  108. function refreshFooter() {
  109.     ensureFooterRows();
  110.     grid.invalidate();
  111. }

  112. if (grid.collectionView) {
  113.     grid.collectionView.collectionChanged.addHandler(refreshFooter);
  114. }

  115. grid.cellEditEnded.addHandler(refreshFooter);
  116. grid.sortedColumn.addHandler(refreshFooter);
  117. grid.loadedRows.addHandler(refreshFooter);

  118. refreshFooter();
UserImage.jpg
Tommy Tan

Thanks Mihai. I will compare it to a js which I had implemented which worked until I upgraded to the new version of Datagrid which broke it. So I had to roll the upgrade back as I did not have the time to try to fix it. My fix required creating a custom numerical column block.

Community GuidelinesBe kind and respectful, give credit to the original source of content, and search for duplicates before posting.