typed-xlsx
Charts and Pivots

Charts

Native Excel charts built from the tables you already declare, with column names and value types checked by TypeScript.

Charts plot the tables your workbook already contains. A chart is a list of marks (bars, lines, areas, scatter points or pie slices), and each mark reads one table through refs, the same way a formula reads columns.

createWorkbook
()
.
sheet
("2026")
.
table
("sales", {
schema
:
sales
,
rows
:
salesRows
})
.
table
("targets", {
schema
:
targets
,
rows
:
targetRows
})
.
chart
("revenue", ({
refs
}) => ({
title
: "Revenue vs target",
marks
: [
refs
.
table
("sales").
bar
({
x
: "month",
y
: "revenue" }),
refs
.
table
("targets").
line
({
x
: "month",
y
: "target",
name
: "Target" }),
], }));

The result is a native Excel chart. It points at the table's cells, so it follows edits to the data and readers can restyle it in Excel.

How charts reach tables

.chart() sits in the same chain as .sheet() and .table():

  • refs.table(id) reads a table declared earlier on the current sheet.
  • refs.sheet(name).table(id) reads a table declared earlier on another sheet.
createWorkbook
()
.
sheet
("2026")
.
table
("sales", {
schema
:
sales
,
rows
:
rows2026
})
.
sheet
("2025")
.
table
("sales", {
schema
:
sales
,
rows
:
rows2025
})
.
sheet
("Dashboard")
.
chart
("yearOverYear", ({
refs
}) => ({
title
: "Revenue 2026 vs 2025",
marks
: [
refs
.
sheet
("2026").
table
("sales").
bar
({
x
: "month",
y
: "revenue",
name
: "2026" }),
refs
.
sheet
("2025").
table
("sales").
bar
({
x
: "month",
y
: "revenue",
name
: "2025" }),
], }));

Table ids only need to be unique within a sheet, so both sheets above can have a sales table. Sheet names must be unique in the chain.

refs only sees what the chain declared before the chart. Calling workbook.sheet(name) in separate statements still works for tables, but charts that combine tables need one unbroken chain.

What TypeScript checks

Every name is checked against the table it comes from, after that table's select is applied. Value columns must hold numbers: the schema remembers what each column writes into its cells, including the result of a transform and of a formula.

createWorkbook
()
.
sheet
("Sales")
.
table
("sales", {
schema
:
sales
,
rows
: [] })
.
chart
("revenue", ({
refs
}) => ({
// "label" holds text after its transform, so it cannot be plotted
marks
: [
refs
.
table
("sales").
bar
({
x
: "month", y: "label" })],
Type '"label"' is not assignable to type 'ChartValuesInput<{ month: string; revenue: number; label: `${number} \u20AC`; }, never> | undefined'.
}));

The compiler also rejects:

MistakeMessage
A column excluded by the table's selectthe id is not offered
A table declared later, or on another sheet without refs.sheet()the id is not offered
A duplicate sheet name or table id✗ sheet "2026" already exists, ✗ table "sales" already exists on this sheet
A pie mixed with other marks✗ a pie or doughnut chart takes a single mark
Scatter or horizontal-bar marks mixed with other marks✗ scatter marks cannot share a chart with other marks
Marks whose x columns hold different kinds of values✗ marks disagree on the x axis: dates

How marks line up

Marks from different tables line up row by row on their own x column. In the first example, the target line's months come from the targets table, and they must match the sales months in the same order.

TypeScript checks that both x columns hold the same kind of value. When the workbook is written, the library compares the values themselves and stops with a clear error instead of drawing a misleading chart:

Chart "revenue": "2026/targets" row 2 is "Mar" but "2026/sales" row 2 is "Feb".
Marks line up row by row on their x column.

What gets plotted

  • Only body rows: titles, headers, summary rows and native totals rows stay out of the chart.
  • Series are named after their column header, linked to the header cell. Pass name to override it.
  • Formula columns are plotted too. Excel computes their values when the file opens.
  • A table with no rows gives an empty chart instead of an error.
  • Tables that expand rows into sub-rows (array accessors) cannot be charted.

Placement and size

Without at, a chart goes to the right of the sheet's tables, and further charts stack below it. Sheets without tables start at B2.

createWorkbook
()
.
sheet
("Sales")
.
table
("sales", {
schema
:
sales
,
rows
: [] })
.
chart
("revenue", ({
refs
}) => ({
at
: "H2",
size
: {
width
: 720,
height
: 360 },
marks
: [
refs
.
table
("sales").
line
({
x
: "month",
y
: "revenue" })],
}));

size is in pixels, so the chart keeps its size whatever the column widths.

Streaming

With createWorkbookStream(), each streamed table creates marks directly, and sheet.chart() takes the chart as an object. Charts are written when the workbook is finalized, so they can read tables of any sheet.

const 
workbook
=
createWorkbookStream
();
const
monthly
= await
workbook
.
sheet
("Monthly").
table
("monthly", {
schema
});
for await (const
rows
of
batches
) {
await
monthly
.
commit
({
rows
});
}
workbook
.
sheet
("Dashboard").
chart
("trend", {
title
: "Revenue and cost",
marks
: [
monthly
.
line
({
x
: "month",
y
: ["revenue", "cost"] })],
}); await
workbook
.
writeToFile
("./monthly.xlsx");

Streamed rows are gone by the time charts are written, so streamed charts carry no cached values (Excel computes them on open), and marks from different tables are only checked to have the same number of rows.

Copyright © 2026 Cyprien Thao. Released under the MIT License.