Skip to content

jeyabbalas/data-table

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

349 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@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.

  • Per-column visualizations (histograms, value counts, date/time histograms) with brush/click crossfilter
  • 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__ + read-only column export (actions.getColumnValues) for app-side row alignment and chart feeds
  • Programmatic row / column / cell annotations — severity tiers, intersection popover, JSON round-trip, session-persisted
  • 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.

Quick start

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 — the same way <img src> and fetch behave. 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.

Documentation

Full documentation lives under docs/. A quick index:

Start here

  • Quick start (above) · Runnable examples
  • 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 — 34 error codes and 19 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:

Option Default Notes
persistence true Auto-save filters/sort/columns to IndexedDB
presets true Show the "Presets" button for saving filter sets
undoRedo true Ctrl/Cmd+Z and Ctrl+Y keyboard shortcuts
expressionFilter true Show the "Expression" (raw SQL) filter button
derivedColumns true Show the "+" add-column button and per-header f(x) edit icon
visualizations true Auto-attach column header histograms / value counts
exportDialog true table.openExportDialog() opens a CSV/JSON/Parquet modal

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 34 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: self-host the worker and WASM bundles and pass bridgeOptions.workerFactory + bridgeOptions.duckdbBundles.

See AGENTS.md §3(h) for a shared-bridge snippet, and docs/troubleshooting.md §4 for the CSP recipe.

Accessibility

The grid targets WCAG 2.1 AA: role="grid" with aria-rowcount / aria-colcount, roving-tabindex arrow-key navigation, 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 {
  /* fetchData, render, … */
}

const registry = new VisualizationRegistry();
registry.register({
  name: 'my-viz',
  isApplicable: (t) => t === 'float',
  constructor: MyViz as any,
  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

API Used for
Worker DuckDB runs in a dedicated worker
WebAssembly DuckDB is compiled to Wasm
IndexedDB Session persistence (skipped when persistence: false)
ResizeObserver Column resize, responsive visualizations
BigInt DuckDB integer columns cross the worker boundary as BigInt
structuredClone Bridge 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