@jeyabbalas/data-table

中文文档
0.10.1发布于 2026/11/01MITTypeScriptES Module零依赖构建来源已验证

A client-side JavaScript library for interactive, explorable data tables using DuckDB WASM

原始使用文档
文档目录23 节
  1. Install
  2. Quick start
  3. Sizing the container
  4. What goes wrong without one
  5. Resizing after mount
  6. Documentation
  7. Framework integration
  8. React
  9. Vue 3
  10. Feature toggles
  11. Skipping CodeMirror
  12. Events
  13. Theming
  14. CSS isolation
  15. Error handling
  16. Multiple tables, CSP, and offline
  17. Accessibility
  18. Custom visualizations
  19. Internationalization
  20. Browser support
  21. Advanced: modular API
  22. Development
  23. License

@jeyabbalas/data-table

A client-side TypeScript library for interactive, explorable data tables. Built on DuckDB WASM — all analytics run entirely in the browser, so no data ever leaves the user's machine.

Try the live demo: open a CSV, JSON or Parquet file, or an Excel workbook, which the demo converts, and explore it. See also the examples and the keyboard shortcuts.

  • Per-column visualizations (histograms, value counts, date/time histograms) with brush/click crossfilter; nested columns get a non-null / null summary bar, which does not filter
  • Manual filter UI per column + raw-SQL WHERE filters
  • Pin / hide / reorder / resize columns; virtual scrolling
  • Derived columns (SQL expressions or JS-provided value vectors), with same-name dependency-aware replacement
  • Stable synthetic __rowid__ + exact read-only value reads (actions.getColumnValues, actions.getCellValue) for app-side row alignment and chart feeds
  • Nested columns (lists, arrays, structs, maps, unions, VARIANT): DuckDB's text in cells, bounded to 32 items; sorting by value; standard JSON in CSV and on the clipboard. For them and for JSON columns: exact filters that compare the text (the filter panel's, or valueType: 'text' in code), a value inspector that shows a whole value as a keyboard tree (F2 or double-click) and copies it as JSON, and an extract button that turns any field into a column of its own (actions.addNestedFieldColumn in code)
  • Programmatic row / column / cell annotations — severity tiers, a popover, JSON round-trip, session-persisted, tested at a million; F8 goes from one to the next, and a filter shows only the annotated rows
  • Programmatic column-header tooltips — XSS-safe structured popover (title / description / items + enum chips)
  • Custom column-stats panels — replace the built-in two-line stats display in a column header with your own DOM and DuckDB queries via BaseStatsPanel + per-instance StatsPanelRegistry
  • Public CodeMirror SQL editor primitives — embed a schema- and DuckDB-aware SQL editor anywhere in your own UI shell via createSqlExtensions / buildCompletionContext (filter-preset composers, derived-column wizards, query-template editors); live or literal schema, optional library theme
  • Filter presets (import/export JSON)
  • Undo/redo and IndexedDB session persistence
  • CSV / JSON / Parquet export
  • Automatic light/dark mode via CSS custom properties

Install

npm install @jeyabbalas/data-table \
  @duckdb/duckdb-wasm \
  @codemirror/autocomplete @codemirror/commands @codemirror/lang-sql \
  @codemirror/language @codemirror/state @codemirror/view @lezer/highlight

@duckdb/duckdb-wasm is a required peer dependency. The @codemirror/* and @lezer/* packages are optional peers — install them only if you use the default SQL expression editor (for derived columns and raw-SQL filters). If you supply your own editorFactory or disable those features, you can omit them.

To serve DuckDB-WASM's files yourself, for a strict Content Security Policy or an offline deployment, install @duckdb/duckdb-wasm at exactly 1.33.1-dev57.0 (npm install --save-exact): see CSP and offline deployments.

Quick start

Give the container a bounded height — the table virtualizes against it, and without one virtualization is silently defeated. See Sizing the container.

<div id="my-table" style="height: 600px"></div>
import { createDataTable } from '@jeyabbalas/data-table';
import '@jeyabbalas/data-table/styles';

const table = await createDataTable({
  container: document.getElementById('my-table')!,
  source: myCsvFileOrUrl, // File | string URL | ArrayBuffer | Blob
});

table.on('filterChange', ({ filters, filteredRowCount }) => {
  console.log(`${filters.length} filters, ${filteredRowCount} rows match`);
});

// When unmounting (e.g., route change in an SPA):
await table.destroy();

For file pickers, URL inputs, or "swap dataset" flows, mount once and load on user action — no destroy()/recreate dance needed:

const table = await createDataTable({
  container: document.getElementById('my-table')!,
});

document.getElementById('file-picker')!.addEventListener('change', async (e) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (file) await table.loadData(file);
});

loadData(source) accepts a File, an absolute URL (https://…, http://…, file:, data:, blob:), a protocol-relative URL (//host/…), a root-relative path (/data.csv), a dot-prefixed relative path (./data.csv, ../data.csv), an ArrayBuffer, a Blob, or inline CSV/JSON content (multi-line text, or a string starting with [ / {). Relative URLs resolve against window.location.href, so a <base href> on the page does not apply to them. Ambiguous strings (a single-line sample.csv with no leading slash, for example) throw LoadError with code SOURCE_AMBIGUOUS rather than silently parsing the literal text as CSV content.

Probe required browser APIs before mounting:

import { checkBrowserSupport } from '@jeyabbalas/data-table';

const support = checkBrowserSupport();
if (!support.supported) {
  renderUnsupportedScreen(support.missing); // ['Worker', 'WebAssembly', …]
  return;
}
// safe to call createDataTable(...) here

The library is ESM-only since v0.4.0. Use import syntax (every modern bundler — Vite, webpack 5, Rollup, esbuild, Bun — resolves the ESM build by default). It is browser-only and not safe to evaluate during SSR — see Framework integration below for client-side mounting patterns.

Sizing the container

The mount container must have a bounded height. This is a requirement, not a style preference — it is what makes the table fast.

The table is virtualized: it renders only the rows that fit in view, about ⌈containerHeight / rowHeight⌉ + 10 of them. It learns how many that is by measuring the container. The container's height is therefore the knob that caps how much work every scroll, filter, and sort has to do — a 600 px container renders ~29 rows whether the dataset has 1 thousand rows or 10 million.

Give it a height in one of two ways:

<!-- 1. An explicit height — simplest, and always correct -->
<div id="my-table" style="height: 600px"></div>
/* 2. A flex or grid child that fills the space left over */
html,
body {
  height: 100%;
  margin: 0;
}
body {
  display: flex;
  flex-direction: column;
}
#my-table {
  flex: 1;
  min-height: 0; /* required — see below */
}

min-height: 0 is not optional in the flex case. Flex items default to min-height: auto, which refuses to shrink below the content's intrinsic height — and this table's content is every row in the dataset. Omit it and you get the unbounded-growth failure below, in a container that looks like it was sized correctly. The same applies to grid children.

height: 100% on the container alone does nothing unless every ancestor up to the viewport also has a resolved height. That is the usual reason a container that "has a height" still behaves as if it doesn't.

What goes wrong without one

Nothing errors. The table renders, and on a small dataset it looks fine — which is exactly why this is easy to ship and painful later.

With no bounded height, the library's root element (height: 100%) resolves against an auto-height parent and becomes content-sized. The scroll area then grows to its full capped height — min(rowCount × rowHeight, 15,000,000 px) — so the "visible region" the scroller measures is that entire range. Virtualization is silently defeated: the table builds DOM for every row in the range and issues block fetches to fill all of them.

At the default 32 px row height, the cap corresponds to ~468,750 rows, so that is where the damage saturates instead of scaling with the dataset — no comfort: hundreds of thousands of DOM rows and a churn of block fetches to fill them. The tab stalls, memory climbs, and scrolling is unusable. With a bounded container the same dataset renders ~29 rows and stays interactive.

The degenerate case is the opposite one: a container that is zero-tall at mount renders no rows at all, and the library logs a console warning saying so. A container that is merely unbounded gets no warning, because it has a perfectly good non-zero height — it is just the wrong one.

Resizing after mount

Give the container its height before mounting, and prefer a height that the browser resolves through CSS (flex: 1, 100%, vh) over one you assign imperatively after the fact. A height that changes after mount is followed: the table works out which rows are in view whenever its body resizes, so a window resize, a panel collapsing above it, or a table mounted in a hidden tab and shown later fills with rows without a scroll.

Every runnable example uses the flex pattern, and docs/integrations/cdn.md has a complete copy-pasteable HTML starter.

Documentation

Full documentation lives under docs/. A quick index:

Start here

  • Quick start (above) · Runnable examples
  • Sizing the container — the bounded height the table virtualizes against; read this before reporting a performance problem
  • AGENTS.md — agent-facing guide: capability matrix, clarifying-question checklist, canonical snippets, pitfalls

Reference

  • API reference — every option, event, action, error, filter shape, derived-column type
  • Troubleshooting — 46 error codes and 26 common-issue FAQs with fix snippets

Guides

Concepts

Integrations

Source

Framework integration

The library is browser-only — it uses Web Workers, window, document, and IndexedDB directly, and is not safe to evaluate during SSR. Mount the table inside your framework's client-side lifecycle hook.

React

import { useEffect, useRef } from 'react';
import { createDataTable, type DataTable } from '@jeyabbalas/data-table';
import '@jeyabbalas/data-table/styles';

export function Table({ source }: { source: File | string }) {
  const hostRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (!hostRef.current) return;
    let cancelled = false;
    let instance: DataTable | undefined;
    void createDataTable({ container: hostRef.current, source }).then((t) => {
      if (cancelled) {
        void t.destroy();
        return;
      }
      instance = t;
    });
    return () => {
      cancelled = true;
      if (instance && !instance.isDestroyed()) void instance.destroy();
    };
  }, [source]);
  return <div ref={hostRef} style={{ height: 600 }} />;
}

The cancelled flag handles the case where the effect re-runs before createDataTable resolves. isDestroyed() guards against double destroys when React's Strict Mode double-invokes effects in dev.

Vue 3

<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import { createDataTable, type DataTable } from '@jeyabbalas/data-table';
import '@jeyabbalas/data-table/styles';

const host = ref<HTMLElement | null>(null);
let table: DataTable | undefined;

onMounted(async () => {
  if (host.value) table = await createDataTable({ container: host.value, source: props.source });
});
onBeforeUnmount(async () => {
  if (table && !table.isDestroyed()) await table.destroy();
});
</script>

<template>
  <div ref="host" style="height: 600px" />
</template>

After destroy(), the public methods (loadData, on, off, openExportDialog, clearSession, setColorScheme) throw DestroyedError. Check table.isDestroyed() in long-lived closures before calling them.

Feature toggles

All features are on by default; pass false or a config object to customize:

OptionDefaultNotes
persistencetrueAuto-save filters/sort/columns to IndexedDB
presetstrueShow the "Presets" button for saving filter sets
undoRedotrueCtrl/Cmd+Z, Ctrl/Cmd+Shift+Z and Ctrl+Y keyboard shortcuts
expressionFiltertrueShow the "Expression" (raw SQL) filter button
derivedColumnstrueShow the "+", f(x) and nested/JSON extract buttons, and the value inspector's "Add as column"
visualizationstrueAuto-attach column header charts (histograms, value counts, …)
exportDialogtruetable.openExportDialog() opens a CSV/JSON/Parquet modal
annotationNavigationtrueF8 / Shift+F8 and Alt+↓ / Alt+↑ go to the next / previous annotation

For the full options surface (mounting, worker, UI, customization), see docs/api-reference.md#createdatatableoptions.

Skipping CodeMirror

When both expressionFilter: false and derivedColumns: false are set, the library no longer reaches the CodeMirror-bound modals at runtime. Consumers in this configuration can omit the @codemirror/* and @lezer/highlight peer dependencies (already marked optional in peerDependenciesMeta) — modern bundlers chunk-split the modals into separate runtime modules that the unreachable code paths never load. The programmatic API (actions.addFilter({ type: 'raw-sql' }), actions.addDerivedColumn, FilterPresetManager) keeps working in this mode. When the flags are on, the first click of an SQL or derived-column button now fetches a small modal chunk on demand instead of loading it eagerly on table mount.

Events

Subscribe with table.on(event, handler) — returns an unsubscribe function. The event bus covers ready, loadStart / loadProgress / loadComplete / loadError, filterChange, sortChange, selectionChange, columnChange, derivedChange (with a kind: 'added' | 'removed' | 'replaced' | 'updated' discriminator and the affected columnName), undoChange, destroy, plus error and warning for recoverable failure modes. Annotation mutations flow through a separate table.annotations.on('change', …) channel — see the annotations guide. For payload types see docs/api-reference.md#event-catalog.

Theming

All colors, spacing, typography, and z-indices are driven by CSS custom properties (74 --dt-* tokens — covering the full palette, sizing scale, z-index stacking ladder, annotation severity tints, and column-header tooltip layering). Override the ones you care about on :root, on a per-instance element, or at runtime.

:root {
  --dt-primary: #10b981;
  --dt-radius: 4px;
  --dt-z-modal: 1500; /* raise above your app's modal layer */
  --dt-panel-width: 420px; /* widen filter / preset / derived-edit panels */
}

Light/dark mode follows prefers-color-scheme by default. Pass colorScheme: 'light' | 'dark' | 'auto' to force a theme, or call table.setColorScheme(...) at runtime; body-portalled modals stay in sync.

See docs/guides/theming.md for the complete variable reference with light/dark defaults side-by-side, the dark-mode scoping model, stacking-ladder deep-dive, and per-instance override patterns.

CSS isolation

All selectors carry the dt- prefix. Column-drag cursor, CodeMirror autocomplete, and modal stacking are all scoped so they don't collide with host styles. For stricter isolation (two copies of the library on one page, strict brand walls), pass classPrefix: 'myapp-dt' and every selector, modal, and tooltip re-renders with that prefix.

Shadow DOM is intentionally not used — modals portal into light DOM so they can inherit --dt-* variables from :root. Wrap the library in a shadow root yourself if you need that, and forward the theme variables + portalTarget accordingly.

Error handling

Every error extends DataTableError (which extends Error). Subscribe to the error event to route by subsystem, or to warning for non-fatal degradations like STYLESHEET_MISSING / PERSISTENCE_UNAVAILABLE:

table.on('error', ({ error, source }) => {
  if (error.code === 'PARSE_FAILED') toast('Could not read that file.');
  else if (source === 'persistence') console.warn(error);
  else reportToSentry(error);
});

For the full list of error codes with triggers and fixes, see docs/troubleshooting.md.

Multiple tables, CSP, and offline

  • Multiple tables: share a WorkerBridge (and optionally a SessionStore / FilterPresetManager) to avoid spinning up two DuckDB instances. Give each table a distinct tableName so session snapshots don't clobber each other.
  • CSP / air-gapped: serve DuckDB-WASM's files from your origin, copied from @duckdb/duckdb-wasm pinned to exactly 1.33.1-dev57.0, and pass them as bridgeOptions.duckdbBundles (relative URLs resolve against the page). Mirror DuckDB's icu, parquet and json extensions and set custom_extension_repository, and send one policy, with 'wasm-unsafe-eval' and worker-src 'self' blob:, with every response.

See AGENTS.md §3(h) for a shared-bridge snippet, CSP and offline deployments for the self-hosting recipe, and docs/troubleshooting.md §4 when DuckDB does not start.

Accessibility

The grid targets WCAG 2.1 AA: role="grid" with aria-rowcount / aria-colcount, a single cursor published via aria-activedescendant and driven by the arrow keys (plus F2 to reach a column's controls, Shift+F2 to resize and reorder one, and F8 to go to the next annotation), a tab order that doesn't grow with column count, polite aria-live announcements on filter/sort/row-count changes, focus trap + escape-to-close on every modal, and axe-core as a CI gate. Known out-of-scope: in-cell editing, mobile touch gestures, RTL.

Custom visualizations

Subclass BaseVisualization (from /advanced) and register on a per-instance VisualizationRegistry:

import { createDataTable, VisualizationRegistry } from '@jeyabbalas/data-table';
import { BaseVisualization } from '@jeyabbalas/data-table/advanced';

class MyViz extends BaseVisualization {
  /* a constructor that starts the first fetch, fetchData(), render() and six input handlers */
}

const registry = new VisualizationRegistry();
registry.register({
  name: 'my-viz',
  isApplicable: (t) => t === 'float',
  constructor: MyViz,
  priority: 10,
});
const table = await createDataTable({ container, source, visualizationRegistry: registry });

Runnable version in examples/08-custom-visualization.

Internationalization

Every user-facing string comes from a typed Strings object. Pass a DeepPartial<Strings> via messages; missing keys fall back to English defaults. Messages are resolved once at construction — recreate the table to switch locales.

await createDataTable({
  container,
  source,
  messages: {
    common: { close: 'Fermer', apply: 'Appliquer' },
    filters: { panelTitle: 'Filtres' },
  },
});

Runnable version in examples/07-i18n-french.

Browser support

APIUsed for
WorkerDuckDB runs in a dedicated worker
WebAssemblyDuckDB is compiled to Wasm
IndexedDBSession persistence (skipped when persistence: false)
ResizeObserverColumn resize, responsive visualizations
BigIntDuckDB integer columns cross the worker boundary as BigInt
structuredCloneBridge snapshots result sets

Roughly Chrome/Edge 98+, Firefox 94+, Safari 15.4+. Probe at runtime with checkBrowserSupport() or opt into fail-fast init via strictBrowserCheck: true. See docs/api-reference.md#browser-support-probe.

Advanced: modular API

The root entry (@jeyabbalas/data-table) exposes the facade plus a small set of stable hooks. Power users who want to orchestrate the stack directly — custom visualization lifecycles, headless use, driving the bridge themselves — can import the building blocks from /advanced:

import {
  TableContainer,
  FilterBar,
  ExportDialog,
  BaseVisualization /* … */,
} from '@jeyabbalas/data-table/advanced';

The /advanced surface trades stability for flexibility: it is not covered by the same semver guarantees as the root entry. See docs/api-reference.md#tier-2-exports for the full symbol list.

Development

See DEVELOPMENT.md for local setup, testing, the build pipeline, and the release workflow. See CONTRIBUTING.md for how to report bugs and submit changes.

License

MIT

版本与发布记录

@jeyabbalas/data-table 共发布 12 个版本,首次发布于 2026/04/28;过去 12 个月发布了 12 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.10.1latest2026/11/012 天前
0.10.02026/10/1123 天前
0.9.02026/10/0925 天前
0.9.0-next.0next2026/09/271 个月前
0.8.02026/09/271 个月前
0.7.02026/08/072 个月前

关于 @jeyabbalas/data-table 的常见问题

@jeyabbalas/data-table 是什么?

一个客户端JavaScript库,用于使用DuckDB WASM的交互式、可探索的数据表

如何安装 @jeyabbalas/data-table?

在项目目录运行 npm install @jeyabbalas/data-table,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@duckdb/duckdb-wasm(^1.33.1-dev57.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@jeyabbalas/data-table 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@jeyabbalas/data-table 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@jeyabbalas/data-table 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@jeyabbalas/data-table 可以商用吗?

@jeyabbalas/data-table 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@jeyabbalas/data-table 还在维护吗?

最新版本 0.10.1 发布于 2026/11/01(2 天前);至今共发布 12 个版本,其中过去 12 个月发布了 12 个;首次发布于 2026/04/28。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。