chartsheet

How to add a pivot table in ExcelJS

There is no addPivotTable in any version you can install. Here is why, and how to get a real one into the file anyway.

Short answer. ExcelJS has no pivot table API in any released version. The implementation was merged into its master branch on 31 October 2023 — twelve days after the last release — and has never been published, so npm install exceljs cannot produce one. Write the workbook as usual, then pass the buffer through addPivotTable.

npm install chartsheet

A pivot table from a sheet of data

const ExcelJS = require('exceljs')
const { addPivotTable } = require('chartsheet')
const fs = require('fs')

async function main () {
  const wb = new ExcelJS.Workbook()
  const data = wb.addWorksheet('Data')
  data.addRow(['Region', 'Product', 'Sales'])
  data.addRow(['East', 'Alpha', 100])
  data.addRow(['East', 'Beta', 150])
  data.addRow(['West', 'Alpha', 200])
  data.addRow(['West', 'Beta', 120])
  data.getRow(1).font = { bold: true }

  // the pivot needs a sheet of its own to land on
  wb.addWorksheet('Report')

  let buffer = await wb.xlsx.writeBuffer()

  buffer = await addPivotTable(buffer, {
    sourceSheet: 'Data',
    sourceRef: 'A1:C5',        // include the header row
    targetSheet: 'Report',
    anchor: 'A3',
    rows: ['Region'],
    columns: ['Product'],
    values: [{ field: 'Sales', fn: 'sum' }],
  })

  fs.writeFileSync('report.xlsx', buffer)
}

main()

Open report.xlsx and the Report sheet holds a working pivot table: sums by region down the side, products across the top, grand totals on both. Click it and the field list appears, same as one built by hand.

An Excel pivot table with regions down the side, products across the top, sums in each cell and grand totals on both axes. The Region and Product field buttons are visible.
A pivot table written by addPivotTable, open in Excel. Note the field buttons on Region and Product — those only appear on a real pivot table. Excel computed every one of those totals itself from the cache.

Options

OptionMeaning
sourceSheetSheet holding the source table. Defaults to the first sheet
sourceRefThe range, including the header row — 'A1:C500'
targetSheetSheet the table is written to. Must already exist
anchorTop-left cell. Default 'A3'
rows, columns, filtersField names, taken from the header row
values['Sales'], or [{ field, fn, name }]
nameTable name. Default 'PivotTable1'

fn is one of sum, count, average, max, min, product, countNums, stdDev, stdDevp, var, varp.

Several tables at once

const { addPivotTables } = require('chartsheet')

buffer = await addPivotTables(buffer, [
  { sourceSheet: 'Data', sourceRef: 'A1:D500', targetSheet: 'Report', anchor: 'A3',
    rows: ['Region'], columns: ['Product'], values: [{ field: 'Sales', fn: 'sum' }] },
  { sourceSheet: 'Data', sourceRef: 'A1:D500', targetSheet: 'Report', anchor: 'A20',
    name: 'PivotTable2',
    rows: ['Quarter'], values: [{ field: 'Sales', fn: 'average' }] },
])

Keeping a pivot table you already have

The opposite problem, and the older one. Load a workbook that already has a pivot table, edit a cell, write it back, and the pivot is gone — reported in 2017 and still open. ExcelJS models no pivot part, so it keeps only what it recognises and writes a file that never had one. Nothing is corrupt, which is why there is no error.

const { preservePivotTables } = require('chartsheet')

const original = fs.readFileSync('report.xlsx')     // has a pivot table

const wb = new ExcelJS.Workbook()
await wb.xlsx.load(original)
wb.getWorksheet('Data').getCell('C2').value = 999
const rewritten = await wb.xlsx.writeBuffer()       // pivot is gone here

const output = await preservePivotTables(original, rewritten)

You do not need to know how the pivot was laid out — the parts come off the original as they are. The restored cache is marked refreshOnLoad, so Excel recomputes the totals from the sheet as it opens, rather than showing the figures captured before your edit.

Charts go the same way on a read-write cycle, and most real templates have both:

const { preserveAll } = require('chartsheet')
const output = await preserveAll(original, rewritten)

If you would rather write the parts yourself

A pivot table is four things in the package, and each one is a silent failure if you get it wrong — Excel says only “we found a problem with some content”.

All four new parts also need their own <Override> entries in [Content_Types].xml. The generic <Default Extension="xml"> looks like it should cover them and does not.

One thing worth knowing before you start: you do not have to compute the aggregation. Setting refreshOnLoad="1" on the cache definition makes Excel rebuild rows, columns and totals from the cached records when the file opens. Reimplementing Excel's own aggregation in JavaScript is a great deal of work to arrive at the same numbers.

Checking the file

const { validate } = require('chartsheet')

const result = await validate(fs.readFileSync('report.xlsx'))
console.log(result.valid, result.errors)

This reports the specific defect instead of Excel's one unhelpful sentence — an unbound cacheId, a missing cache relationship, a recordCount that disagrees with the records. It works on any .xlsx, not only files this library produced.

Using SheetJS instead?

It works the same way, and SheetJS has the same gap. Adding a pivot table with SheetJS.

Questions

Why is there no addPivotTable in ExcelJS?

Because the code that adds it was never released. ExcelJS 4.4.0 was published on 19 October 2023; the pivot table commit landed on 31 October 2023. npm gives you the release, and the release has no pivot support.

Is it a real pivot table?

Yes — a native table with a pivot cache. Change the fields, expand and collapse, refresh it. Not a static block of cells formatted to look like one.

Does it work with SheetJS?

Yes. The parts are written into a finished .xlsx buffer, so anything producing valid xlsx output works.

My pivot table disappears when ExcelJS saves the file. Why?

Because ExcelJS never wrote it. It models no pivot part, so a load-edit-save cycle drops the pivot tables, their caches and the workbook's <pivotCaches> entry, with no error. preservePivotTables lifts them off the original and puts them back.

Can I have a chart and a pivot table in the same workbook?

Yes. Pass the buffer through both — charts and pivot tables are separate parts and do not collide.