vowl-wasm
An ontology visualisation engine in Rust, compiled to WebAssembly.
It turns an OWL ontology into something a browser can draw: parse the ontology, build a typed graph from it, settle that graph with a force-directed layout, and hand the result to JavaScript — either as structured data, or as a pointer into WASM linear memory that a rendering worker reads without copying.
This crate renders; it does not own any ontology format. With the ngg1
feature, vowl_wasm::ngg1 and NggExplorer read NGG1 binary graph tiers through
a re-export of the ontomark-ngg1 crate from
ontomark. ontomark is the single owner of the
NGG1 specification, constants and codec; this crate holds no format constants of
its own. The JavaScript API is unchanged.
Attribution
This is a clean-room Rust reimplementation of the VOWL visual notation
defined by VisualDataWeb/WebVOWL
(MIT, © 2014–2019 Vincent Link, Steffen Lohmann, Eduard Marbach, Stefan Negru,
Vitalis Wiens). WebVOWL is the origin of the visual notation and of the ontology
JSON shape this crate reads; none of its JavaScript, fixtures, or assets are
vendored here. Both licence notices are carried in LICENSE.
This crate is not affiliated with or endorsed by the WebVOWL project or TIB.
Install
[]
= "0.1"
From JavaScript, via the published bundle:
Rust usage
use WebVowl;
let mut vowl = new;
vowl.load_ontology.expect;
vowl.init_simulation.expect;
vowl.run_simulation.expect;
assert_eq!;
JavaScript usage
import init from '@dreamlab-ai/vowl-wasm';
await ;
const vowl = ;
vowl.;
vowl.;
vowl.;
console.log;
Reading positions back
getNodePositions() returns a Float32Array of interleaved x, y in node
index order. That index order is stable for the lifetime of a loaded graph —
ticking, pinning, filtering and folding never reorder or remove nodes — so map
index to id once and then read only floats per frame:
const ids = vowl.; // once, after loadOntology
To reuse one buffer instead of allocating per frame, use
writePositionsInto(buf), which fills a caller-owned Float32Array and returns
the number of floats written:
const xy = ;
vowl.;
;
Labels, types and edges still come from getGraphData(), which stays behind the
debug-serde feature.
For a rendering worker, NggExplorer avoids per-frame serialisation entirely:
import init from '@dreamlab-ai/vowl-wasm';
const wasm = await ;
const explorer = ;
explorer.;
// A zero-copy view over the live position buffer.
const positions = ;
while
The view is invalidated by the next loadCsr and by any WASM memory growth —
re-read positionsPtr() after either.
Public API
This is the frozen 0.1.0 surface. Names and signatures will not change without a major version bump; later 0.1.x releases only add to it.
WebVowl — always available
| JavaScript | Purpose |
|---|---|
new WebVowl() |
Construct an empty instance |
loadOntology(json) |
Parse the ontology and build the graph |
initSimulation() |
Seed the force simulation |
runSimulation(n) |
Advance n steps |
tick() |
Advance one step |
isFinished() |
Has the layout settled? |
getAlpha() |
Current annealing temperature |
setCenter(x, y) |
Move the layout centre |
setLinkDistance(d) |
Spring rest length |
setChargeStrength(s) |
Repulsion strength |
getNodeCount() / getEdgeCount() |
Graph size |
getNodeIds() |
Node ids, in index order |
getNodePositions() |
Live x, y per node as a Float32Array |
writePositionsInto(buf) |
Copy those positions into a caller-owned buffer |
getStatistics() |
Degree, connected components, OWL2 metrics |
getMetadata() |
Ontology header: IRI, version, title, description |
Filtering, folding and pinning are also always available: filterByDegree,
filterHierarchy, toggleDatatypes, toggleSetOperators, resetVisibility,
getNodeDegree, pinNode, unpinNode, isPinned, resetPins,
getPinnedCount.
Feature-gated
| JavaScript | Feature |
|---|---|
new NggExplorer() — loadCsr, tick, isFinished, getAlpha, nodeCount, edgeCount, positionsPtr, positionsLen, writePositionsInto, setParam, reheat |
ngg1 |
checkNodeClick(origin, direction) |
interaction |
getGraphData() |
debug-serde |
Features
Every optional feature is off by default. The general-purpose engine — OWL/JSON parsing, the graph model, force layout (Barnes-Hut quadtree and the CSR simulation), pinning and statistics — is always compiled.
| Feature | Default | What it adds |
|---|---|---|
ngg1 |
off | The NGG1 reader (vowl_wasm::ngg1) and the NggExplorer zero-copy binding |
interaction |
off | Ray-sphere hit testing for click and selection |
debug-serde |
off | Legacy full-graph serde export, for diagnostics |
parallel |
off | Data-parallel ontology loading via rayon. Native only |
simd |
off | WASM SIMD128 force accumulation; needs the simd128 target feature |
Each feature compiles standalone, and the combinations are checked in CI.
The published npm bundle is built with --features ngg1,interaction.
The markdown-ontology feature, with its Markdown parser and OWL 2 validator,
was removed. Parsing Markdown vaults is ontomark-parse's job.
parseMarkdownOntology and validateOWL2 were not in the 0.1.2 published bundle
(0.1.0 and 0.1.1 shipped them); they are gone from the crate as well.
Version 0.2.0 also removes the Rust-only ontology::Domain and
ontology::OntologyBlock types. Class metadata accepts caller-supplied domain
strings, including domains outside the estate, or no domain. The renderer holds
no domain registry or namespace table.
Reading and writing NGG1 in JavaScript
With ngg1 enabled, decodeNgg1(bytes) returns an owned NggGraph.
ids() and degrees() return Uint32Array; positions() returns packed
Float32Array coordinates; domains() and categories() return Uint16Array;
flags() and edgeType() return Uint8Array. labels() and iris() return
string arrays. rowPtr() and colIdx() return CSR Uint32Array snapshots.
All arrays are independent copies and remain usable after graph.free().
import init from '@dreamlab-ai/vowl-wasm';
await ;
const graph = ; // Uint8Array containing a tier
const fields = ;
graph.;
const encoded = ; // standalone Uint8Array
const = ;
For synthetic graphs, supply seed positions and metadata in the same local
node order. Every node field and string array must match ids.length, positions
must have twice that length, and CSR targets must be valid local indices.
Malformed input throws an NGG1: ... string. The owned decoder rejects invalid
UTF-8; unknown taxonomy ids, flag bits and edge codes are preserved. The existing
NggExplorer remains the zero-copy physics interface for animation.
Bundle size. Measured with the published-bundle build,
wasm-pack build --release --target web --features ngg1,interaction
(wasm-pack 0.15, wasm-bindgen 0.2.128, wasm-opt off as in Cargo.toml), on
2026-10-11:
| Tree | pkg/vowl_wasm_bg.wasm |
gzip -9 |
|---|---|---|
| 0.1.2 source (hand-written NGG1 reader) | 232,764 bytes | 91,418 bytes |
| 0.2.0 (shared codec, owned decode/encode bindings) | 264,158 bytes | 101,069 bytes |
The 0.1.2 release notes record 232,052 bytes (90,675 gzipped); the difference is toolchain drift, not a source change.
The 0.2.0 browser decoder, encoder and constants add 31,394 bytes (+13.49%) raw
over the 0.1.2 source baseline. No reasoner
(whelk-el), RDF, YAML or regex crate may enter the bundle's dependency graph:
tests/ontomark_ngg1_migration.rs walks the normal dependencies of
--features ngg1,interaction (dev-dependencies such as criterion do not ship).
With VOWL_BUNDLE_GATE=1 after a wasm-pack build (CI does this), it also
requires pkg/vowl_wasm_bg.wasm to be a real WebAssembly module below 280,000
bytes, with the recorded 0.2.0 JavaScript API in pkg/vowl_wasm.d.ts.
parallel, simd and debug-serde are likewise off in the published bundle;
build from source to enable them.
Building
# The published bundle
pkg/ is a build artefact and is not committed. CI uploads it as a workflow
artifact, and tagged releases attach it as vowl-wasm-<version>-pkg.tgz and
publish that same tarball to npm as @dreamlab-ai/vowl-wasm through npm
trusted publishing (OIDC from release.yml; no token is stored anywhere and
provenance attestations are generated automatically). To republish an existing
tag, run the Release workflow with the tag as its input; it skips versions the
registry already has.
MSRV: 1.85.
Migrating from webvowl-wasm / @dreamlab-ai/webvowl-wasm
This crate replaces three divergent in-tree copies of webvowl-wasm. Migrating:
-
Package name.
webvowl-wasm→@dreamlab-ai/vowl-wasm. Replace anyfile:path dependency with a real semver pin. -
Generated file names change, because the crate's lib name changed from
webvowl_wasmtovowl_wasm:Before After webvowl_wasm.jsvowl_wasm.jswebvowl_wasm.d.tsvowl_wasm.d.tswebvowl_wasm_bg.wasmvowl_wasm_bg.wasmAnything that references those files by path — a bundler
optimizeDepsentry, a hand-writteninit(url)call, a CDN URL, a CSP hash — needs updating. Importing the package by name is unaffected. -
Version resets to 0.1.0. Neither of the old copies (1.0.0 and 0.3.4) described this API, so the numbering starts over. There is no implied compatibility with either.
-
Feature gates.
NggExplorerandcheckNodeClickwere unconditional before and are now behindngg1andinteractionThe published bundle enables both; a consumer building from source must ask for them.
-
Layout defaults changed.
LayoutConfig::default()now carries general-purpose force constants rather than values tuned for one 1,155-node ontology. The old numbers are available asLayoutConfig::large_graph(). This affectsWebVowlonly —NggExplorerandCsrSimulationhave their own parameters and are unchanged.
Licence
MIT. See LICENSE, which carries both this project's notice and
the upstream WebVOWL notice.