Skip to content

Table Layout Engine

ABCrimson edited this page Mar 3, 2026 · 1 revision

Table Layout Engine

Generate styled XLSX tables from declarative options -- no manual cell coordinate math.

The Table Layout Engine composes existing primitives (Worksheet.cell, StyleBuilder, addMergeCell, setColumnWidth, frozenPane) into a high-level API that handles header styling, zebra striping, auto-width, merge cells, per-cell overrides, and multi-table composition.


Quick Start

import { initWasm, Workbook, drawTable } from 'modern-xlsx';

await initWasm();

const wb = new Workbook();
const ws = wb.addSheet('Report');

const result = drawTable(wb, ws, {
  headers: ['Name', 'Department', 'Salary'],
  rows: [
    ['Alice', 'Engineering', 95000],
    ['Bob', 'Marketing', 72000],
    ['Carol', 'Engineering', 105000],
  ],
  columnWidths: [20, 18, 12],
});

console.log(result.range); // "A1:C4"

await wb.toFile('report.xlsx');

API

drawTable(wb, ws, opts): TableResult

Draw a styled table on a worksheet.

Parameter Type Description
wb Workbook Workbook instance (needed for style registration)
ws Worksheet Target worksheet
opts DrawTableOptions Table configuration

Returns a TableResult with layout metadata for composing multiple tables.

drawTableFromData(wb, ws, data, opts?): TableResult

Create a table from a JSON array, auto-extracting headers from object keys.

Parameter Type Description
wb Workbook Workbook instance
ws Worksheet Target worksheet
data Record<string, unknown>[] Array of row objects
opts DrawTableFromDataOptions Optional configuration
const data = [
  { name: 'Alice', age: 30, city: 'NYC' },
  { name: 'Bob', age: 25, city: 'LA' },
];

drawTableFromData(wb, ws, data, {
  headerMap: { name: 'Full Name', age: 'Age', city: 'City' },
  autoWidth: true,
});

Types

TableResult

Returned by both drawTable and drawTableFromData with layout metadata.

interface TableResult {
  range: string;        // "A1:D10" -- full table range (A1-style)
  rowCount: number;     // Total rows (header + data)
  colCount: number;     // Number of columns
  firstDataRow: number; // 0-based row index of first data row
  lastDataRow: number;  // 0-based row index of last data row
}

DrawTableOptions

Full configuration for drawTable.

interface DrawTableOptions {
  // --- Layout (required) ---
  headers: string[];
  rows: (string | number | boolean | null)[][];

  // --- Layout (optional) ---
  origin?: string;           // A1-style origin cell (default: "A1")
  columns?: TableColumn[];   // Per-column config
  columnWidths?: number[];   // Fixed widths shorthand
  autoWidth?: boolean;       // Auto-calculate widths (default: false)
  freezeHeader?: boolean;    // Freeze the header row (default: false)
  autoFilter?: boolean;      // Add filter dropdowns (default: false)
  wrapText?: boolean;        // Enable text wrapping (default: false)

  // --- Header styling ---
  headerFont?: Partial<FontData>;                // Default: bold white
  headerColor?: string;                          // Hex, no # (default: '4472C4')
  headerAlign?: 'left' | 'center' | 'right';    // Default: 'center'

  // --- Body styling ---
  bodyFont?: Partial<FontData>;
  bodyAlign?: 'left' | 'center' | 'right';
  verticalAlign?: 'top' | 'center' | 'bottom';
  borderStyle?: BorderStyle | null;              // Default: 'thin', null = none
  borderColor?: string;                          // Hex (default: '000000')
  alternateRowColor?: string | null;             // Hex for zebra stripes

  // --- Advanced ---
  merges?: { row: number; col: number; rowSpan?: number; colSpan?: number }[];
  cellStyles?: Record<string, CellStyle>;
}

DrawTableFromDataOptions

Extends DrawTableOptions (minus headers and rows) with JSON-specific options.

interface DrawTableFromDataOptions extends Omit<DrawTableOptions, 'headers' | 'rows'> {
  headers?: string[];                     // Explicit column order (default: Object.keys of first item)
  headerMap?: Record<string, string>;     // Map object keys to display headers
}

TableColumn

Per-column configuration for width, alignment, and number format.

interface TableColumn {
  header?: string;                         // Header label override
  width?: number;                          // Fixed width (character units)
  align?: 'left' | 'center' | 'right';    // Horizontal alignment for data cells
  numberFormat?: string;                   // e.g. '#,##0.00', '$#,##0.00'
}

CellStyle

Per-cell style override applied on top of the base table style.

interface CellStyle {
  font?: Partial<FontData>;
  fill?: { pattern?: string; fgColor?: string };
  border?: Partial<{
    left: { style: BorderStyle; color?: string | null };
    right: { style: BorderStyle; color?: string | null };
    top: { style: BorderStyle; color?: string | null };
    bottom: { style: BorderStyle; color?: string | null };
  }>;
  alignment?: Partial<AlignmentData>;
  numberFormat?: string;
}

Options Reference

Layout Options

Option Type Default Description
headers string[] required Header labels
rows (string|number|boolean|null)[][] required Data rows
origin string "A1" A1-style origin cell
columns TableColumn[] -- Per-column config (width, align, numberFormat)
columnWidths number[] -- Fixed column widths (shorthand for columns[].width)
autoWidth boolean false Auto-calculate widths from content
freezeHeader boolean false Freeze the header row
autoFilter boolean false Add filter dropdown arrows to headers
wrapText boolean false Enable text wrapping in body cells

Styling Options

Option Type Default Description
headerFont Partial<FontData> bold white Header font
headerColor string '4472C4' Header background color (hex)
bodyFont Partial<FontData> -- Body font override
borderStyle BorderStyle|null 'thin' Border style for all cells (null to disable)
borderColor string '000000' Border color (hex)
alternateRowColor string|null -- Zebra stripe color for odd rows (hex)
headerAlign 'left'|'center'|'right' 'center' Header horizontal alignment
bodyAlign 'left'|'center'|'right' -- Body horizontal alignment
verticalAlign 'top'|'center'|'bottom' -- Vertical alignment for all cells

Recipes

Invoice Table

A financial table with currency formatting, frozen header, and zebra stripes.

drawTable(wb, ws, {
  headers: ['Item', 'Qty', 'Unit Price', 'Total'],
  rows: [
    ['Widget', 10, 25.5, 255],
    ['Gadget', 5, 42.0, 210],
    ['Doohickey', 2, 99.99, 199.98],
  ],
  columnWidths: [20, 8, 12, 12],
  headerColor: '2F5496',
  alternateRowColor: 'D6E4F0',
  freezeHeader: true,
  columns: [
    { align: 'left' },
    { align: 'center' },
    { align: 'right', numberFormat: '$#,##0.00' },
    { align: 'right', numberFormat: '$#,##0.00' },
  ],
});

Zebra Striping

Add alternating row colors for readability.

drawTable(wb, ws, {
  headers: ['ID', 'Name', 'Status'],
  rows: data,
  alternateRowColor: 'F2F2F2',
});

Per-Cell Style Overrides

Override individual cell styles using "row,col" keys (0-based, relative to the data area).

drawTable(wb, ws, {
  headers: ['Name', 'Score', 'Grade'],
  rows: [
    ['Alice', 95, 'A'],
    ['Bob', 42, 'F'],
  ],
  cellStyles: {
    '1,1': { font: { color: 'FF0000', bold: true } },
    '1,2': { fill: { pattern: 'solid', fgColor: 'FFCCCC' } },
  },
});

The cellStyles map uses "dataRow,col" keys where dataRow 0 is the first data row (row after headers). Overrides are merged on top of the base style for that cell (including zebra stripe and column styles).

Merge Cells

Merge cells in the data area using 0-based row/col indices relative to the first data row.

drawTable(wb, ws, {
  headers: ['Category', 'Product', 'Price'],
  rows: [
    ['Electronics', 'Phone', 999],
    ['', 'Laptop', 1299],
    ['Clothing', 'Shirt', 49],
  ],
  merges: [
    { row: 0, col: 0, rowSpan: 2 }, // Merge "Electronics" across 2 rows
  ],
});
Property Type Default Description
row number required 0-based data row index
col number required 0-based column index
rowSpan number 1 Number of rows to span
colSpan number 1 Number of columns to span

Nested Tables (Vertical Composition)

Use the returned TableResult metadata to position sequential tables.

const result1 = drawTable(wb, ws, {
  headers: ['Q1 Summary'],
  rows: [['Revenue: $1M'], ['Profit: $200K']],
});

// Place second table below the first, with a gap row
const nextRow = result1.lastDataRow + 2;
drawTable(wb, ws, {
  headers: ['Q2 Summary'],
  rows: [['Revenue: $1.2M'], ['Profit: $250K']],
  origin: `A${nextRow + 1}`,
});

Side-by-Side Tables (Horizontal Composition)

Place multiple tables on the same row using column offsets.

drawTable(wb, ws, {
  headers: ['Team A'],
  rows: [['Alice'], ['Bob']],
  origin: 'A1',
});

drawTable(wb, ws, {
  headers: ['Team B'],
  rows: [['Carol'], ['Dave']],
  origin: 'D1', // Start 3 columns over
});

Auto-Width with CJK Support

Auto-width calculation counts CJK characters (codepoint > 0x2E7F) as double-width.

drawTable(wb, ws, {
  headers: ['Name', 'Description'],
  rows: [
    ['Widget', 'Standard component'],
    ['部品', '日本語の説明'],
  ],
  autoWidth: true,
});

JSON Array to Table

Use drawTableFromData to skip manual header/row extraction.

const employees = [
  { name: 'Alice', department: 'Engineering', salary: 95000 },
  { name: 'Bob', department: 'Marketing', salary: 72000 },
  { name: 'Carol', department: 'Design', salary: 88000 },
];

drawTableFromData(wb, ws, employees, {
  headerMap: {
    name: 'Employee Name',
    department: 'Department',
    salary: 'Annual Salary',
  },
  columns: [
    { align: 'left' },
    { align: 'center' },
    { align: 'right', numberFormat: '$#,##0' },
  ],
  autoWidth: true,
  alternateRowColor: 'F5F5F5',
  freezeHeader: true,
  autoFilter: true,
});

Full-Featured Report

Combine multiple features into a production-ready report.

import { initWasm, Workbook, drawTable, drawTableFromData } from 'modern-xlsx';

await initWasm();

const wb = new Workbook();
const ws = wb.addSheet('Monthly Report');

// Title table
const titleResult = drawTable(wb, ws, {
  headers: ['March 2026 Sales Report'],
  rows: [],
  headerColor: '1F4E79',
  headerFont: { bold: true, size: 16, color: 'FFFFFF' },
  columnWidths: [60],
  borderStyle: null,
});

// Data table below title
const dataRow = titleResult.lastDataRow + 2;
drawTable(wb, ws, {
  headers: ['Region', 'Q1', 'Q2', 'Q3', 'Q4', 'Total'],
  rows: [
    ['North', 120000, 135000, 142000, 155000, 552000],
    ['South', 95000, 102000, 98000, 115000, 410000],
    ['East', 88000, 91000, 105000, 112000, 396000],
    ['West', 145000, 152000, 161000, 170000, 628000],
  ],
  origin: `A${dataRow + 1}`,
  headerColor: '2F5496',
  alternateRowColor: 'D6E4F0',
  freezeHeader: false,
  autoFilter: true,
  columns: [
    { align: 'left', width: 14 },
    { align: 'right', numberFormat: '$#,##0', width: 12 },
    { align: 'right', numberFormat: '$#,##0', width: 12 },
    { align: 'right', numberFormat: '$#,##0', width: 12 },
    { align: 'right', numberFormat: '$#,##0', width: 12 },
    { align: 'right', numberFormat: '$#,##0', width: 14 },
  ],
  cellStyles: {
    '3,5': { font: { bold: true, color: '1F4E79' } }, // Highlight West total
  },
});

await wb.toFile('monthly-report.xlsx');

How It Works

  1. Style palette -- drawTable pre-builds a fixed set of style indices (header, body-even, body-odd, per-column variants) using StyleBuilder. This avoids creating duplicate styles for every cell.

  2. Header row -- Headers are written at the origin row with the header style.

  3. Data rows -- Each cell gets the appropriate style based on: per-column config > zebra stripe > base body style. Per-cell overrides (cellStyles) are merged on top.

  4. Column widths -- Either from columnWidths, columns[].width, or auto-calculated from content (with CJK double-width support).

  5. Post-processing -- Merge cells, frozen pane, and auto-filter are applied after all cells are written.

  6. Result -- Returns TableResult with the range and row indices for composing additional tables.


Tips

  • Style efficiency -- The engine builds styles once per table region, not per cell. Even a 10,000-row table creates only a handful of unique styles.
  • origin positioning -- Use TableResult.lastDataRow to chain tables vertically. Use column letters to place tables side by side.
  • columnWidths vs columns -- Use columnWidths for quick fixed widths. Use columns when you also need alignment or number format per column.
  • autoWidth -- Scans all header and data values to compute widths. Suitable for small/medium tables. For very large tables, prefer explicit columnWidths.
  • cellStyles keys -- Keys are "row,col" strings where row 0 is the first data row (not the header). Column 0 is the first column of the table.

Clone this wiki locally