npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jeyabbalas/data-table

v0.5.0

Published

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

Readme

@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