Data Table
render_tableRender a sortable, interactive data table with customizable themes for styled visuals.
Instructions
Render a sortable, interactive data table. Click column headers to sort. Supports themes for styled visuals.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | Table title | |
| columns | Yes | Column names in display order | |
| rows | Yes | Array of row objects. Keys must match column names. | |
| options | No | ||
| theme | No | Theme preset: boardroom, corporate, sales-floor, golden-treasury, clinical, startup, ops-control, tokyo-midnight, zen-garden, consultant, black-tron, black-elegance, black-matrix, forest-amber, forest-earth, sky-light, sky-ocean, sky-twilight, gray-hf, gray-copilot | |
| palette | No | Override palette only (mix-and-match) | |
| typography | No | Override typography: professional, luxury, cyberpunk, editorial, mono, bold, system, techno | |
| effects | No | Override effects: none, subtle, shimmer, neon, energetic |
Implementation Reference
- src/charts/table.ts:170-345 (handler)Main handler function for the render_table tool. Takes a container HTMLElement and TableData payload, builds an HTML table with striped rows, column sorting via click, row-click selection messaging, and CSV export button. Injects styles, applies themes, and renders title/subtitle metadata.
export function renderTable(container: HTMLElement, payload: TableData): void { const { title, columns, rows, options = {} } = payload; const sortable = options.sortable !== false; // default true const striped = options.striped === true; // Apply theme if specified const theme = resolveTheme(payload.theme, { palette: payload.palette, typography: payload.typography, effects: payload.effects, }); if (theme) applyTheme(container, theme); injectStyles(container); const rowCount = rows.length; const colCount = columns.length; const subtitle = `${rowCount} row${rowCount !== 1 ? "s" : ""} \u00b7 ${colCount} column${colCount !== 1 ? "s" : ""}`; const numericCols = detectNumericColumns(columns, rows); // Build header cells const headerCells = columns .map((col, i) => { const align = numericCols.has(col) ? "right" : "left"; const sortClass = sortable ? " tbl-sortable" : ""; const icon = sortable ? `<span class="tbl-sort-icon" data-icon="${i}">▴▾</span>` : ""; return `<th class="tbl-th${sortClass}" data-col="${i}" style="text-align:${align}" title="${escapeHtml(col)}">${escapeHtml(col)}${icon}</th>`; }) .join(""); // Build body rows let bodyHtml = ""; if (rows.length === 0) { bodyHtml = `<tr><td class="tbl-empty" colspan="${columns.length}">No data</td></tr>`; } else { for (let ri = 0; ri < rows.length; ri++) { const row = rows[ri]; const cells = columns .map((col) => { const val = row[col]; const cellClass = numericCols.has(col) ? "tbl-num" : "tbl-str"; const display = escapeHtml(formatCell(val)); return `<td class="${cellClass}">${display}</td>`; }) .join(""); bodyHtml += `<tr data-row-idx="${ri}">${cells}</tr>`; } } const stripedClass = striped ? " tbl--striped" : ""; container.innerHTML = ` <div class="chart-view"> <div class="card chart-card"> <div class="chart-card__header"> <div> <div class="chart-card__title${theme?.effects.shimmerTitle ? " shimmer-text" : ""}">${escapeHtml(title)}</div> <div class="chart-card__subtitle tbl-meta">${subtitle}</div> </div> </div> <div class="chart-card__body" style="display:flex;flex-direction:column;"> <div class="tbl-wrap"> <table class="tbl${stripedClass}"> <thead> <tr>${headerCells}</tr> </thead> <tbody id="tbl-body"> ${bodyHtml} </tbody> </table> </div> </div> </div> </div> `; // Re-inject the style tag (innerHTML wipe removed it) injectStyles(container); // CSV export button const card = container.querySelector<HTMLElement>(".chart-card"); if (card) addCsvExportButton(card, columns, rows, title); // Row click events for bidirectional messaging const tbody = container.querySelector<HTMLElement>("#tbl-body")!; tbody.addEventListener("click", (e) => { const tr = (e.target as HTMLElement).closest<HTMLElement>("tr[data-row-idx]"); if (!tr) return; const idx = parseInt(tr.dataset.rowIdx ?? "0", 10); const row = rows[idx]; if (!row) return; const summary = columns.map((col) => `${col}: ${row[col] ?? ""}`).join(", "); sendClickMessage(`Row ${idx + 1} in "${title}": ${summary}`); }); if (!sortable || rows.length === 0) return; // Sorting state let sortCol: number | null = null; let sortAsc = true; // Working copy of rows - indices into original rows array let sortedIndices: number[] = rows.map((_, i) => i); const headers = container.querySelectorAll<HTMLElement>(".tbl-th.tbl-sortable"); function rebuildBody(): void { const colName = sortCol !== null ? columns[sortCol] : null; const isNum = colName !== null && numericCols.has(colName); const sorted = [...sortedIndices].sort((a, b) => { if (colName === null) return 0; const av = rows[a][colName]; const bv = rows[b][colName]; let cmp = 0; if (isNum) { const an = av === undefined || av === "" ? -Infinity : Number(av); const bn = bv === undefined || bv === "" ? -Infinity : Number(bv); cmp = an - bn; } else { const as = av === undefined ? "" : String(av); const bs = bv === undefined ? "" : String(bv); cmp = as.localeCompare(bs, undefined, { numeric: true, sensitivity: "base" }); } return sortAsc ? cmp : -cmp; }); sortedIndices = sorted; let html = ""; for (const idx of sortedIndices) { const row = rows[idx]; const cells = columns .map((col) => { const val = row[col]; const cellClass = numericCols.has(col) ? "tbl-num" : "tbl-str"; const display = escapeHtml(formatCell(val)); return `<td class="${cellClass}">${display}</td>`; }) .join(""); html += `<tr data-row-idx="${idx}">${cells}</tr>`; } tbody.innerHTML = html; } headers.forEach((th) => { th.addEventListener("click", () => { const colIdx = parseInt(th.dataset.col ?? "0", 10); if (sortCol === colIdx) { sortAsc = !sortAsc; } else { sortCol = colIdx; sortAsc = true; } // Update header visuals headers.forEach((h) => { h.classList.remove("tbl-sorted"); const icon = h.querySelector<HTMLElement>(".tbl-sort-icon"); if (icon) icon.innerHTML = "▴▾"; }); th.classList.add("tbl-sorted"); const activeIcon = th.querySelector<HTMLElement>(".tbl-sort-icon"); if (activeIcon) { activeIcon.innerHTML = sortAsc ? "▴" : "▾"; } rebuildBody(); }); }); } - src/charts/table.ts:4-16 (schema)TableData interface defining the input schema for render_table: title (string), columns (string[]), rows (array of Record<string, string|number>), optional sortable/striped options, and theme/palette/typography/effects styling fields.
export interface TableData { title: string; columns: string[]; rows: Array<Record<string, string | number>>; options?: { sortable?: boolean; striped?: boolean; }; theme?: string; palette?: string; typography?: string; effects?: string; } - src/charts/table.ts:347-347 (registration)Registration call: registerChart('table', 'render_table', renderTable) which maps the internal type 'table' to the tool name 'render_table' and the render function.
registerChart("table", "render_table", renderTable); - src/charts/auto.ts:302-319 (helper)toTableData helper function in auto.ts that converts arbitrary data into the TableData format expected by renderTable. Handles arrays of objects, plain objects (converts to two-column key-value table), and primitive arrays.
function toTableData(title: string, data: any): Parameters<typeof renderTable>[1] { if (Array.isArray(data) && data.length > 0 && typeof data[0] === "object" && data[0] !== null) { const columns = Object.keys(data[0]); return { title, columns, rows: data }; } // Object - convert to two-column table if (!Array.isArray(data) && typeof data === "object" && data !== null) { const flat = tryFlatten(data) as Record<string, any>; return { title, columns: ["Key", "Value"], rows: Object.entries(flat).map(([k, v]) => ({ Key: k, Value: String(v) })), }; } return { title, columns: ["Value"], rows: (data as any[]).map((v) => ({ Value: String(v) })) }; } - src/charts/shared.ts:567-593 (helper)addCsvExportButton helper used by renderTable to add a CSV download button to the table card header.
export function addCsvExportButton( container: HTMLElement, columns: string[], rows: Array<Record<string, string | number>>, filename: string ): void { const header = container.querySelector(".chart-card__header"); if (!header) return; const btn = document.createElement("button"); btn.className = "export-btn"; btn.title = "Download as CSV"; btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`; btn.addEventListener("click", () => { const csvRows = [columns.join(",")]; for (const row of rows) { csvRows.push(columns.map((col) => { const val = String(row[col] ?? ""); return val.includes(",") || val.includes('"') || val.includes("\n") ? `"${val.replace(/"/g, '""')}"` : val; }).join(",")); } saveViaServer(`${filename}.csv`, csvRows.join("\n"), "utf-8"); }); getOrCreateActions(header).appendChild(btn); }