Skip to content

Architecture ​

vizcrush is a hybrid Rust + TypeScript monorepo. Algorithms live in Rust crates that compile to WebAssembly with wasm-bindgen; thin TypeScript packages wrap the WASM modules with async APIs and a JavaScript fallback. The only WebGPU compute path is bin2d's opt-in one — measured slower than WASM (ADR 0004); ADR 0002/0003 explain why GPU/SIMD are not default paths, and VIZCRUSH_SPEC.md's header note covers what the original vision document got wrong about this.

Pipeline overview ​

High-level layout ​

vizcrush/
├── crates/                       Rust workspace — algorithms compiled to WASM
│   ├── vizcrush-core/            Shared bounds-finding (used by bin/bin3d/spatial/spatial3d)
│   ├── vizcrush-downsample/      LTTB, MinMaxLTTB, M4, LTOB
│   ├── vizcrush-aggregate/       Welford, percentile (exact), streaming, sketches (t-digest: unwired)
│   ├── vizcrush-transform/       Sort, normalize, filter
│   ├── vizcrush-bin/             1D, 2D, hex binning
│   ├── vizcrush-bin3d/           3D voxel binning
│   ├── vizcrush-spatial/         Quadtree, hash grid (k-d tree: unwired)
│   ├── vizcrush-spatial3d/       Octree, frustum culling
│   └── vizcrush-ai/              Anomaly, autoOpt, shape (planned — packages/ai ships pure JS instead)
│
├── packages/                     TypeScript workspace — bindings + servers
│   ├── core/                     init(), backend selection, the WASM/JS dispatch kernel
│   ├── downsample/               @vizcrush/downsample
│   ├── aggregate/                @vizcrush/aggregate
│   ├── transform/                @vizcrush/transform
│   ├── bin/                      @vizcrush/bin (+ WGSL shaders)
│   ├── bin3d/                    @vizcrush/bin3d
│   ├── spatial/                  @vizcrush/spatial
│   ├── spatial3d/                @vizcrush/spatial3d
│   ├── ai/                       @vizcrush/ai (pure JS today)
│   ├── react/                    @vizcrush/react — hooks
│   └── mcp-server/               @vizcrush/mcp-server — MCP server
│
├── benchmarks/                   Performance regression suite
├── examples/                     43 runnable example apps
├── scripts/                      Build/publish/test bash scripts
└── .github/workflows/            CI: build, lint, test, bench, deploy docs

The two layers are deliberately decoupled:

  • The Rust workspace is a normal Cargo workspace. You can cargo test or cargo bench it without touching JavaScript at all.
  • The TypeScript workspace is a pnpm + Turbo monorepo. Each package has its own package.json, tsconfig.json, and src/.

Connecting them is wasm-bindgen — Rust functions tagged with #[wasm_bindgen] get JS bindings auto-generated, and the TypeScript packages just import the resulting .js + .d.ts from packages/<name>/wasm/.

Build pipeline ​

                  ┌────────────────────────────────────────────────┐
                  │                Rust workspace                  │
                  │  cargo build --target wasm32-unknown-unknown   │
                  │           --release  (with +simd128)           │
                  └─────────────────┬──────────────────────────────┘
                                    │  *.wasm artifacts
                                    ▼
                  ┌────────────────────────────────────────────────┐
                  │             wasm-bindgen --target web          │
                  │   (writes .js loader + .d.ts to packages/*/wasm/) │
                  └─────────────────┬──────────────────────────────┘
                                    │  ESM modules per package
                                    ▼
                  ┌────────────────────────────────────────────────┐
                  │                 pnpm turbo build               │
                  │  (per-package tsc → packages/*/dist/)          │
                  └────────────────────────────────────────────────┘

This is run by pnpm build from the monorepo root, which is just:

bash
pnpm build:wasm     # bash scripts/build-wasm.sh
pnpm turbo build    # tsc per package

scripts/build-wasm.sh is the source of truth for the Rust → WASM step. It compiles each crate, runs wasm-bindgen, and (optionally) wasm-opt for size optimization. The output goes to packages/<name>/wasm/ which is gitignored — WASM artifacts are built locally or in CI, never committed.

Runtime backend selection ​

At runtime, init() from @vizcrush/core probes the environment and picks the best compute path:

                     ┌──────────────┐
                     │   init()     │
                     └──────┬───────┘
                            │
                ┌───────────▼───────────┐
                │  detectCapabilities() │
                └───────────┬───────────┘
                            │
              ┌─────────────▼─────────────┐
              │     selectBackend()       │
              │                           │
              │  WebAssembly? ────── yes ─┼──► wasm
              │     │ no                  │
              │     └─────────────────────┼──► js
              └───────────────────────────┘

Each algorithm package then dispatches to WASM when the module is loaded, and to the inline JS fallback otherwise. WGSL shaders live alongside the TypeScript source in packages/<name>/src/shaders/*.wgsl; bin2d's is wired to an opt-in WebGPU path (ADR 0004), the rest are unwired drafts. The JS fallback is implemented inline in TypeScript to keep the package self-contained.

See the Backends & Capabilities user guide for the selection logic and how to override per call.

Why typed arrays everywhere ​

Every public API takes and returns Float64Array (data) or Uint32Array (indices). This is intentional:

  • One bulk boundary copy — wasm-bindgen copies typed arrays into linear memory without per-element boxing
  • Cache-friendly inner loops — contiguous memory keeps the Rust inner loops tight
  • Drop-in friendly — most charting libraries (ChartGPU, Chart.js, D3, Three.js) already consume typed arrays

The cost is that you can't pass plain number[]. The benefit is predictable bulk transfer and cache-friendly inner loops. Public paired-data results use separate x and y typed arrays; index results use Uint32Array.

Why a separate Rust crate per package ​

Each TypeScript package has a 1:1 corresponding Rust crate. They could in principle share a single mega-crate, but separating them gives:

  • Smaller WASM bundles per package — @vizcrush/downsample's WASM doesn't include octree code
  • Independent versioning — packages can ship at different rates
  • Faster Cargo builds — touching the LTTB code doesn't invalidate the spatial cache

The cost is some duplication in Cargo.toml boilerplate, which is mostly handled by the workspace inheritance.

Where the AI features live ​

The @vizcrush/ai package is pure JavaScript today. It's intentionally separate from the Rust crates because:

  • The algorithms (MAD, CUSUM, linear regression, autocorrelation, cosine similarity) are short and don't benefit much from SIMD
  • Pure JS works in environments where WASM is unavailable
  • It can be loaded eagerly by an LLM agent that doesn't have a WASM runtime

A vizcrush-ai Rust crate exists in crates/ as a placeholder for future Rust/WASM implementations of the heavier features (e.g. shape vectors over very large datasets).

CI pipeline ​

.github/workflows/ci.yml classifies every PR by changed path. Code-affecting PRs run:

JobWhat it does
Rust Testscargo fmt --check, cargo clippy -D warnings, cargo test, build WASM with +simd128
Lint + Format + Typecheckpnpm lint (oxlint --deny-warnings), pnpm format:check (oxfmt), pnpm typecheck
TypeScript Build + Testpnpm turbo build, vitest run
Performance RegressionRun benchmarks, compare to benchmarks/benchmark-baseline.json (75% CI-noise threshold)
Security AuditAudit pnpm and Cargo dependencies

Documentation-only PRs run formatting, generated-inventory validation, and a VitePress build while the expensive jobs are skipped at job level. The change-scope and documentation checks are required merge gates, preventing classifier or docs-validation failures from being hidden by conditional skips. .github/workflows/docs-deploy.yml deploys the docs and examples gallery to GitHub Pages when their source changes on main.

See also ​

Released under the MIT License.