Charts
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" })],
}));
The compiler also rejects:
| Mistake | Message |
|---|---|
A column excluded by the table's select | the 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
nameto 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.