JavaScript Data GridColumn summary

Calculate sum, min, max, count, average or custom aggregates of individual columns' data, using Handsontable's aggregate functions.

Overview

The ColumnSummary plugin lets you quickly calculate and display a column summary.

To customize your column summaries, you can:

Column summary example

This example calculates and displays five different column summaries:

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

const container = document.querySelector('#example1');

new Handsontable(container, {
  licenseKey: 'non-commercial-and-evaluation',
  data: [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15], [null]],
  colHeaders: ['sum', 'min', 'max', 'count', 'average'],
  rowHeaders: true,
  // enable and configure the `ColumnSummary` plugin
  columnSummary: [
    {
      sourceColumn: 0,
      type: 'sum',
      destinationRow: 3,
      destinationColumn: 0,
      // force this column summary to treat non-numeric values as numeric values
      forceNumeric: true,
    },
    {
      sourceColumn: 1,
      type: 'min',
      destinationRow: 3,
      destinationColumn: 1,
    },
    {
      sourceColumn: 2,
      type: 'max',
      destinationRow: 3,
      destinationColumn: 2,
    },
    {
      sourceColumn: 3,
      type: 'count',
      destinationRow: 3,
      destinationColumn: 3,
    },
    {
      sourceColumn: 4,
      type: 'average',
      destinationRow: 3,
      destinationColumn: 4,
    },
  ],
  autoWrapRow: true,
  autoWrapCol: true,
  height: 'auto',
});

Built-in summary functions

To decide how a column summary is calculated, you can use one of the following summary functions:

Function Description
sum Returns the sum of all values in a column.
min Returns the lowest value in a column.
max Returns the highest value in a column.
count Returns the number of all non-empty cells in a column.
average Returns the sum of all values in a column,
divided by the number of non-empty cells in that column.
custom Lets you implement a custom summary function.

Column summary options

You can customize each of your column summaries with configuration options.

For the full list of available options, see the API reference.

Set up a column summary

To set up a column summary, follow the steps below.

Step 1: Enable the ColumnSummary plugin

To enable the ColumnSummary plugin, set the columnSummary configuration option to an array of objects. Each object represents a single column summary.

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

const hot = new Handsontable(document.querySelector('#example'), {
  licenseKey: 'non-commercial-and-evaluation',
  data: [
    [1, 2, 3, 4, 5],
    [6, 7, 8, 9, 10],
    [11, 12, 13, 14, 15]
  ],
  colHeaders: true,
  rowHeaders: true,
  // set the `columnSummary` configuration option to an array of objects
  columnSummary: [
    {},
    {}
  ],
});

You can also set the columnSummary option to a function.

Step 2: Select cells that you want to summarize

By default, a column summary takes all cells of the column in which it displays its result (see the destinationColumn option in step 4).

To summarize any other column, use the sourceColumn option:

columnSummary: [
  {
    // set this column summary to summarize the first column
    // (i.e. a column with physical index `0`)
    sourceColumn: 0,
  },
  {
    // set this column summary to summarize the second column
    // (i.e. a column with physical index `1`)
    sourceColumn: 1,
  }
]

You can also summarize individual ranges of rows (rather than a whole column). To do this, set the ranges option to an array of arrays, where each array represents a single row range.

columnSummary: [
  {
    sourceColumn: 0,
    // set this column summary to only summarize rows with physical indexes 0-2, 4, and 6-8
    ranges: [
      [0, 2], [4], [6, 8]
    ],
  },
  {
    sourceColumn: 0,
    // set this column summary to only summarize rows with physical indexes 0-5
    ranges: [
      [0, 5]
    ],
  }
]

Step 3: Calculate your summary

Now, decide how you want to calculate your column summary.

You can:

columnSummary: [
  {
    sourceColumn: 0,
    // set this column summary to return the sum all values in the summarized column
    type: 'sum',
  },
  {
    sourceColumn: 1,
    // set this column summary to return the lowest value in the summarized column
    type: 'min',
  }
]

Step 4: Provide the destination cell's coordinates

To display your column summary result in a cell, provide the destination cell's coordinates.

Set the destinationRow and destinationColumn options to the physical coordinates of your required cell.

columnSummary: [
  {
    sourceColumn: 0,
    type: 'sum',
    // set this column summary to display its result in cell (4, 0)
    destinationRow: 4,
    destinationColumn: 0
  },
  {
    sourceColumn: 1,
    type: 'min',
    // set this column summary to display its result in cell (4, 1)
    destinationRow: 4,
    destinationColumn: 1
  }
]

TIP

Don't change the className metadata of the summary row.

If you need to style the summary row, use the class name assigned automatically by the ColumnSummary plugin: columnSummaryResult.

Step 5: Make room for the destination cell

The ColumnSummary plugin doesn't automatically add new rows to display its summary results.

So, if you always want to display your column summary result below your existing rows, you need to:

  1. Add an empty row to the bottom of your grid (to avoid overwriting your existing rows).
  2. Reverse row coordinates for your column summary (to always display your summary result at the bottom).

TIP

To reverse row coordinates for your column summary, set the reversedRowCoords option to true, and adjust the destinationRow coordinate.

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

const container = document.querySelector('#example2');

new Handsontable(container, {
  licenseKey: 'non-commercial-and-evaluation',
  data: [
    [1, 2, 3, 4, 5],
    [6, 7, 8, 9, 10],
    [11, 12, 13, 14, 15],
    // add an empty row
    [null],
  ],
  colHeaders: true,
  rowHeaders: true,
  columnSummary: [
    {
      sourceColumn: 0,
      type: 'sum',
      // for this column summary, count row coordinates backward
      reversedRowCoords: true,
      // now, to always display this column summary in the bottom row,
      // set `destinationRow` to `0` (i.e. the last possible row)
      destinationRow: 0,
      destinationColumn: 0,
    },
    {
      sourceColumn: 1,
      type: 'min',
      // for this column summary, count row coordinates backward
      reversedRowCoords: true,
      // now, to always display this column summary in the bottom row,
      // set `destinationRow` to `0` (i.e. the last possible row)
      destinationRow: 0,
      destinationColumn: 1,
    },
  ],
  autoWrapRow: true,
  autoWrapCol: true,
  height: 'auto',
});

Set up column summaries, using a function

Instead of setting up the column summary options manually, you can provide the whole column summary configuration as a function that returns a required array of objects.

The example below sets up five different column summaries. To do this, it:

  • Defines a function named generateData which generates an array of arrays with dummy numeric data, and which lets you add an empty row at the bottom of the grid (to make room for displaying column summaries)
  • Sets Handsontable's columnSummary configuration option to a function that:
    • Iterates over visible columns
    • For each visible column, adds a column summary with a configuration
    • To display the column summaries in the empty row added by generateData, sets the reversedRowCoords option to true, and the destinationRow option to 0

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

// generate an array of arrays with dummy numeric data
const generateData = (rows = 3, columns = 7, additionalRows = true) => {
  let counter = 0;
  const array2d = [...new Array(rows)].map((_) =>
    [...new Array(columns)].map((_) => counter++)
  );

  // add an empty row at the bottom, to display column summaries
  if (additionalRows) {
    array2d.push([]);
  }

  return array2d;
};

const container = document.querySelector('#example7');

new Handsontable(container, {
  licenseKey: 'non-commercial-and-evaluation',
  // initialize a Handsontable instance with the generated data
  data: generateData(5, 5, true),
  height: 'auto',
  rowHeaders: true,
  colHeaders: ['sum', 'min', 'max', 'count', 'average'],
  // set the `columnSummary` configuration option to a function
  columnSummary() {
    const configArray = [];
    const summaryTypes = ['sum', 'min', 'max', 'count', 'average'];

    for (let i = 0; i < this.hot.countCols(); i++) {
      // iterate over visible columns
      // for each visible column, add a column summary with a configuration
      configArray.push({
        sourceColumn: i,
        type: summaryTypes[i],
        // count row coordinates backward
        reversedRowCoords: true,
        // display the column summary in the bottom row (because of the reversed row coordinates)
        destinationRow: 0,
        destinationColumn: i,
        forceNumeric: true,
      });
    }

    return configArray;
  },
  autoWrapRow: true,
  autoWrapCol: true,
});

Using a function to provide a column summary configuration lets you set up all sorts of more complex column summaries. For example, you can sum subtotals for nested groups:

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

const container = document.querySelector('#example8');

new Handsontable(container, {
  licenseKey: 'non-commercial-and-evaluation',
  data: [
    {
      value: null,
      __children: [{ value: 5 }, { value: 6 }, { value: 7 }],
    },
    {
      __children: [{ value: 15 }, { value: 16 }, { value: 17 }],
    },
  ],
  columns: [{ data: 'value' }],
  nestedRows: true,
  rowHeaders: true,
  colHeaders: ['sum', 'min', 'max', 'count', 'average'],
  columnSummary() {
    const endpoints = [];
    const nestedRowsPlugin = this.hot.getPlugin('nestedRows');
    const getRowIndex = nestedRowsPlugin.dataManager.getRowIndex.bind(
      nestedRowsPlugin.dataManager
    );

    const resultColumn = 0;
    let nestedRowsCache = null;

    if (nestedRowsPlugin.isEnabled()) {
      nestedRowsCache = nestedRowsPlugin.dataManager.cache;
    } else {
      return [];
    }

    if (!nestedRowsCache) {
      return [];
    }

    for (let i = 0; i < nestedRowsCache.levels[0].length; i++) {
      if (
        !nestedRowsCache.levels[0][i].__children ||
        nestedRowsCache.levels[0][i].__children.length === 0
      ) {
        continue;
      }

      const tempEndpoint = {
        destinationColumn: resultColumn,
        destinationRow: getRowIndex(nestedRowsCache.levels[0][i]),
        type: 'sum',
        forceNumeric: true,
        ranges: [],
      };

      tempEndpoint.ranges.push([
        getRowIndex(nestedRowsCache.levels[0][i].__children[0]),
        getRowIndex(
          nestedRowsCache.levels[0][i].__children[
            nestedRowsCache.levels[0][i].__children.length - 1
          ]
        ),
      ]);
      endpoints.push(tempEndpoint);
    }

    return endpoints;
  },
  autoWrapRow: true,
  autoWrapCol: true,
  height: 'auto',
});

Implement a custom summary function

Apart from using the built-in summary functions, you can also implement your own custom function that performs any summary calculation you want.

To implement a custom summary function:

  1. Set up your column summary.
  2. In your column summary object, set the type option to 'custom':

columnSummary: [{
  sourceColumn: 1,
  // set the `type` option to `'custom'`
  type: 'custom',
  destinationRow: 0,
  destinationColumn: 5,
  reversedRowCoords: true
}]

  1. In your column summary object, add your custom summary function:

columnSummary: [{
    type: 'custom',
    destinationRow: 0,
    destinationColumn: 5,
    reversedRowCoords: true,
    // add your custom summary function
    customFunction: function(endpoint) {
      // implement your function here
    }
}]

This example implements a function that counts the number of even values in a column:

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

// generate an array of arrays with dummy numeric data
const generateData = (rows = 3, columns = 7, additionalRows = true) => {
  let counter = 0;
  const array2d = [...new Array(rows)].map((_) =>
    [...new Array(columns)].map((_) => counter++)
  );

  if (additionalRows) {
    array2d.push([]);
    array2d.push([]);
  }

  return array2d;
};

const container = document.querySelector('#example9');

new Handsontable(container, {
  licenseKey: 'non-commercial-and-evaluation',
  // initialize a Handsontable instance with the generated numeric data
  data: generateData(5, 7),
  height: 'auto',
  colHeaders: true,
  rowHeaders: true,
  // enable the `ColumnSummary` plugin
  columnSummary: [
    // configure a column summary
    {
      // set the `type` option to `'custom'`
      type: 'custom',
      destinationRow: 0,
      destinationColumn: 5,
      reversedRowCoords: true,
      // add your custom summary function
      customFunction(endpoint) {
        // implement a function that counts the number of even values in the column
        const hotInstance = this.hot;
        let evenCount = 0;
        // a helper function
        const checkRange = (rowRange) => {
          let i = rowRange[1] || rowRange[0];
          let counter = 0;

          do {
            if (
              parseInt(
                hotInstance.getDataAtCell(i, endpoint.sourceColumn),
                10
              ) %
                2 ===
              0
            ) {
              counter++;
            }

            i--;
          } while (i >= rowRange[0]);

          return counter;
        };

        // go through all declared ranges
        for (const r in endpoint.ranges) {
          if (endpoint.ranges.hasOwnProperty(r)) {
            evenCount += checkRange(endpoint.ranges[r]);
          }
        }

        return evenCount;
      },
      forceNumeric: true,
    },
  ],
  autoWrapRow: true,
  autoWrapCol: true,
});

Round a column summary result

You can round a column summary result to a specific number of digits after the decimal point.

To enable this feature, set the roundFloat option to your preferred number of digits between 0 and 100. See the following example:

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

const container = document.querySelector('#example12');

new Handsontable(container, {
  licenseKey: 'non-commercial-and-evaluation',
  data: [[0.5, 0.5], [0.5, 0.5], [1, 1], [], []],
  colHeaders: true,
  rowHeaders: true,
  columnSummary: [
    {
      type: 'average',
      destinationRow: 0,
      destinationColumn: 0,
      reversedRowCoords: true,
    },
    {
      type: 'average',
      destinationRow: 0,
      destinationColumn: 1,
      reversedRowCoords: true,
      // round this column summary result to two digits after the decimal point
      roundFloat: 2,
    },
  ],
  autoWrapRow: true,
  autoWrapCol: true,
  height: 'auto',
});

The roundFloat option accepts the following values:

Value Behavior
false (default) Don't round the column summary result.
true Round the result to 0 digits after the decimal point.
Integer 0-100 (n) Round the result to n digits after the decimal point.
Integer < 0 Round the result to 0 digits after the decimal point.
Integer > 100 Round the result to 100 digits after the decimal point.

If you enable roundFloat, the data type returned by Handsontable's data-retrieving methods (like getDataAtCell()) changes from number to string.

Handle non-numeric values

To summarize a column that contains non-numeric data, you can:

  • Either force your column summary to treat non-numeric values as numeric values
  • Or throw an error whenever a non-numeric value is passed to your column summary
  • Or make your column summary skip any non-numeric values

Force numeric values

You can force your column summary to treat non-numeric values as numeric values.

TIP

The forceNumeric option uses JavaScript's parseFloat() function.

This means that e.g., 3c is treated as 3, but c3 is still treated as c3.

To enable this feature, set the forceNumeric option to true (by default, forceNumeric is set to false). For example:

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

const container = document.querySelector('#example10');

new Handsontable(container, {
  licenseKey: 'non-commercial-and-evaluation',
  data: [[0, 1, 2], ['3c', '4b', 5], [], []],
  colHeaders: true,
  rowHeaders: true,
  columnSummary: [
    {
      type: 'sum',
      destinationRow: 0,
      destinationColumn: 0,
      reversedRowCoords: true,
      // force this column summary to treat non-numeric values as numeric values
      forceNumeric: true,
    },
    {
      type: 'sum',
      destinationRow: 0,
      destinationColumn: 1,
      reversedRowCoords: true,
      // force this column summary to treat non-numeric values as numeric values
      forceNumeric: true,
    },
  ],
  autoWrapRow: true,
  autoWrapCol: true,
  height: 'auto',
});

Throw data type errors

You can throw a data type error whenever a non-numeric value is passed to your column summary.

To throw data type errors, set the suppressDataTypeErrors option to false (by default, suppressDataTypeErrors is set to true). For example:

import Handsontable from 'handsontable';
import 'handsontable/dist/handsontable.full.min.css';

const container = document.querySelector('#example11');

new Handsontable(container, {
  licenseKey: 'non-commercial-and-evaluation',
  data: [[0, 1, 2], ['3c', '4b', 5], [], []],
  colHeaders: true,
  rowHeaders: true,
  columnSummary: [
    {
      type: 'sum',
      destinationRow: 0,
      destinationColumn: 0,
      reversedRowCoords: true,
      // enable throwing data type errors for this column summary
      suppressDataTypeErrors: false,
    },
    {
      type: 'sum',
      destinationRow: 0,
      destinationColumn: 1,
      reversedRowCoords: true,
      // enable throwing data type errors for this column summary
      suppressDataTypeErrors: false,
    },
  ],
  autoWrapRow: true,
  autoWrapCol: true,
  height: 'auto',
});

There is a newer version of Handsontable available. Switch to the latest version ⟶