Development
How to build Charturbo from source, produce the library package, the API docs, the examples site and the documentation site, and run the GPU visual regression tests.
1. Compile the Rust WASM package
wasm-pack build crates/charturbo-core --target web --out-dir ../../web/src/wasm-pkg2. Build the plain NPM library (dist-lib/)
Packages only the SDK bundle and the .d.ts type definitions into web/dist-lib/, with the example showcase and benchmark UI fully excluded. Output:
charturbo.js(ES) +charturbo_core_bg.wasm(a separate file, loaded withnew URL('./charturbo_core_bg.wasm', import.meta.url)→ handled as an asset by Vite · webpack 5 (verified) and by Rollup / Parcel asset plugins)inline.js(charturbo/inline, WASM embedded as base64 — for environments without asset handling such as esbuild),charturbo.umd.cjs(UMD, WASM embedded:<script>tag · CommonJSrequire)- Framework entries
react.js·vue.js(import the core chunk;react/vueare external) - Types:
index.d.ts(+index.d.cts/inline.d.ctsfor CJS),react/·vue/·sdk/declarations.scripts/finalize-dts.mjsadds the.jsextension to relative paths so types also resolve undermoduleResolution: node16 / nodenext. - To move the WASM file, call
setWasmSource(url | Response | bytes | WebAssembly.Module)before the firstcreate().
cd web
npm run build:lib3. Publish to NPM
cd web
npm login
npm publish --access publicRun npm pack --dry-run before publishing to confirm that only dist-lib/*, README.md, CHANGELOG.md and LICENSE go into the package. prepack runs build:lib again. See Releasing for the version policy and the full checklist, and web/CHANGELOG.md for the change history.
API docs · examples site
cd web
npm run docs # TypeDoc → docs/public/api/ (src/index.ts, react, vue entries)
npm run examples:dev # examples site dev server (web/examples/, one page per chart family + source)
npm run site:dev # the whole site on http://localhost:5310/: examples + these docs under /docs/ (add `-- --host` for a phone)
npm run examples:build # static build → web/examples-dist/Documentation site
This documentation site is a VitePress project in docs/. English lives in docs/, Korean in docs/ko/ and Simplified Chinese in docs/zh/, all with the same paths.
cd docs
npm install
npm run dev # VitePress dev server, docs only (http://localhost:5173/docs/)
npm run build # static build → docs/.vitepress/dist/
npm run preview # preview the build output
npm run check # locale parity check: every page must exist in en / ko / zhnpm run check(docs/scripts/check-docs.mjs) collects the sidebar fromdocs/.vitepress/config.mtsand the pages underguide/,charts/andcontributing/, and fails with exit 1 if a page is missing in any of the three locales or is not in the sidebar.- To add a page, put its slug and per-locale titles in the sidebar table of
config.mts, then create the file in all three locales. - For the API reference (English),
cd web && npm run docswrites the TypeDoc output todocs/public/api/, which the site serves as-is under/api/.docs/public/api/is ignored by git.
4. GPU visual regression tests (npm run test:visual)
Renders fixture charts on the GPU in a real headless Chrome and compares the screenshots with baseline images. This is a separate script from npm test (vitest).
cd web
npm run test:visual # compare (exit 1 on failure)
npm run test:visual -- --update # rewrite the baselines (after an intended rendering change)
npm run test:visual -- --backend=webgpu --filter=pie # narrow by backend / part of the fixture name
npm run test:visual -- --canvas-only # machines with different fonts: skip the full shots that contain DOM text
npm run test:visual -- --repeat=3 # repeat 3 times in the same process (flake check)
npm run test:visual -- --param=color:#ff00ff # fixture parameter (break a fixture on purpose to see a failure; cannot be combined with --update)- Layout:
web/e2e/index.html+main.ts+fixtures.ts(the fixture page, served by Vite),run-visual.mjs(runner),lib/image.mjs(dependency-free PNG decode/encode withnode:zlib+ pixel diff). The runner starts its own Vite server on a free port, launches Chrome (--headless=new --enable-unsafe-webgpu,--use-angle=metalon macOS) with a unique--user-data-dir·--remote-debugging-port, and opens a fresh page per fixture over the DevTools Protocol. - Backends: a
webgpupass and a forcedwebgl2pass (the page hidesnavigator.gpubefore the engine starts so wgpu falls back to the GL backend — the same path as a browser without WebGPU). The actual backend is checked through the canvas context; a mismatch is an error. - Fixtures (27): 1M-point line (M4 LOD), line dark/light theme, stacked area, grouped bars, scatter, candlestick, histogram (200k samples), heatmap, box · violin, pie · donut, combo, radar · gauge · funnel · waterfall, treemap · sunburst · sankey, vertical-axis zoom (
line-yzoom) · box zoom (box-zoom), a grid of 24 sparklines on a sharedCharturboDevice, hover (hoverAtX→ crosshair + marker), navigator (zoom window), brush rectangle (selectChartRangexy). - Two shots:
canvas= the canvas area only, cropped, with the DOM overlay (title · ticks · legend · color bar · pie labels) hidden viavisibility: hidden(font-independent, the main check),full= the whole card including labels (stable only on the same machine with the same fonts — use--canvas-onlyon another OS / CI). - Baselines:
web/e2e/baselines/<webgpu|webgl2>/<fixture>.<canvas|full>.png(108 files). Results:web/e2e/out/report.html(baseline / actual / diff side by side),report.json,*.actual.png,*.diff.png(e2e/out/is ignored by git). - Comparison: a pixel differs when the maximum RGBA channel difference exceeds the threshold (canvas 24, full 32 / 255); the test fails when more than 0.2% of pixels differ or the size changes. Diff image: red = above threshold, yellow = difference within threshold, gray = identical.
- Determinism: seeded PRNG (mulberry32), fixed canvas size (480×300, sparklines 140×32), DPR 1 (
deviceScaleFactor: 1,--force-device-scale-factor=1),--force-color-profile=srgb,autoResize: false, localeen-US, fixed UTC dates on time axes, explicit padding (tick label width does not move the plot), emulatedprefers-reduced-motion: reduce/prefers-color-scheme: light. The SDK has no animation here, so each capture is onerender()+ a 3-frame wait. Three consecutive runs on the same machine produced pixel-identical shots (diff 0). - Baselines are tied to the GPU · driver · Chrome version: if they fail on another machine, check the differences in the report and regenerate them on that machine with
--update. - If Chrome is missing (set it with
CHROME_PATH/--chrome=<path>; a path that does not exist is also SKIPPED) or no GPU backend is usable, the runner printsSKIPPEDand exits 0, or exits 1 with--strict. An unusable backend is skipped on its own and the others still run (with--strict, any missing backend fails). Harness errors (a--filterthat matches no fixture, a lost Chrome connection, etc.) exit 2.