BlitZoom Developer Guide

Embed interactive graph views or run the pipeline offline.

Quick Start

Examples cover declarative HTML, programmatic embedding, streaming with the incremental API, and side-by-side renderer/compute comparisons. The skill bundle is a Claude Code skill — drop it into your skills directory to teach Claude how to use the BlitZoom API.

import { createBlitZoomView } from './blitzoom.js';

const view = createBlitZoomView(
  document.getElementById('canvas'),
  edgesText,   // SNAP .edges file content (string)
  nodesText,  // SNAP .nodes file content or null
  { initialLevel: 3, heatmapMode: 'density', showLegend: true }
);

That parses the data, runs the full MinHash + projection + blend pipeline, and renders an interactive canvas with pan, zoom, click-to-select, and scroll-to-zoom-level. Here it is with the Epstein dataset:

Epstein network. Scroll to zoom. Drag to pan. Click to select.

Data Format (SNAP)

BlitZoom uses a simple tab-delimited text format based on SNAP. A dataset consists of an .edges file (required) and an optional .nodes file.

.edges file (required)

Tab-delimited, one edge per line. Lines starting with # are comments. Each line is either From<tab>To (undirected) or From<tab>To<tab>EdgeType (typed).

# Social network edges
# From	To	EdgeType
alice	bob	friend
alice	carol	colleague
bob	dave	friend
carol	dave	colleague
eve	alice	manager

.nodes file (optional)

Tab-delimited, one node per line. The first comment line defines column names: # NodeId<tab>Label<tab>Group followed by any number of extra columns. Extra columns become property groups used for MinHash similarity and layout positioning.

# NodeId	Label	Group	Role	Experience
alice	Alice	engineering	backend	8
bob	Bob	engineering	frontend	5
carol	Carol	design	ux	3
dave	Dave	design	visual	7
eve	Eve	management	director	12

Column behavior

Numeric columns are auto-detected when >=80% of values parse as numbers. They receive 3-level tokenization (coarse, medium, fine bins), so nodes with nearby numeric values cluster together while distant values separate.

Empty fields (missing or blank values) produce zero tokens, which results in a neutral [0,0] projection for that property group. This avoids false clustering: nodes with unknown values float to the center rather than being forced into a cluster.

If no .nodes file is provided, the graph is edge-only. Layout relies entirely on topology (set smoothAlpha to 1.0 for best results).

Embed a View

The code above is the full embed. All you need is a <canvas> element and the SNAP data as strings. The canvas auto-sizes via ResizeObserver.

// Fetch data, create view
const [edges, nodes] = await Promise.all([
  fetch('data/epstein.edges').then(r => r.text()),
  fetch('data/epstein.nodes').then(r => r.text()),
]);

const view = createBlitZoomView(canvas, edges, nodes, {
  initialLevel: 3,
  edgeMode: 'lines',
  heatmapMode: 'density',
  showLegend: true,
  showResetBtn: true,
  strengths: { group: 8, edgetype: 4 },
  labelProps: ['label'],
});

The returned view is a BlitZoomCanvas instance with a full API for programmatic control.

Prefer HTML? Use the <bz-graph> web component — no JavaScript needed for basic use:
<style>bz-graph:not(:defined) { visibility: hidden; }</style>
<bz-graph edges="data/karate.edges" nodes="data/karate.nodes"
          level="3" heatmap="density" legend>
</bz-graph>
The :not(:defined) rule prevents a flash of raw text before the component loads.

From JS Objects

If your graph is already in memory as JavaScript objects, skip the SNAP parsing entirely with createBlitZoomFromGraph. Pass nodes as plain objects with id, group, label, and any extra properties. Edges are {src, dst} pairs.

import { createBlitZoomFromGraph } from './blitzoom.js';

const nodes = [
  { id: 'alice',   group: 'engineer', label: 'Alice',   lang: 'Go',     exp: 8 },
  { id: 'bob',     group: 'engineer', label: 'Bob',     lang: 'Go',     exp: 5 },
  { id: 'carol',   group: 'designer', label: 'Carol',   lang: 'JS',     exp: 3 },
  { id: 'dave',    group: 'designer', label: 'Dave',    lang: 'JS',     exp: 7 },
  { id: 'eve',     group: 'manager',  label: 'Eve',     lang: 'Python', exp: 12 },
  { id: 'frank',   group: 'engineer', label: 'Frank',   lang: 'Rust',   exp: 2 },
  { id: 'grace',   group: 'designer', label: 'Grace',   lang: 'JS',     exp: 6 },
  { id: 'hank',    group: 'manager',  label: 'Hank',    lang: 'Python', exp: 9 },
];

const edges = [
  { src: 'alice', dst: 'bob' },   { src: 'alice', dst: 'carol' },
  { src: 'bob',   dst: 'frank' }, { src: 'carol', dst: 'dave' },
  { src: 'carol', dst: 'grace' }, { src: 'dave',  dst: 'grace' },
  { src: 'eve',   dst: 'alice' }, { src: 'eve',   dst: 'hank' },
  { src: 'hank',  dst: 'bob' },   { src: 'frank', dst: 'eve' },
];

const view = createBlitZoomFromGraph(canvas, nodes, edges, {
  initialLevel: 3,
  showLegend: true,
  showResetBtn: true,
  strengths: { group: 5, lang: 8 },
});

Only id is required. group defaults to "unknown" and label defaults to the node's id. Any other property becomes an extra property group. Numeric values (like exp) get multi-resolution tokenization automatically.

8 nodes from JS objects. No file parsing.

Incremental Updates

Add, remove, and update nodes at runtime. The incremental attribute on <bz-graph> is a bundled preset that picks quant='norm' (existing nodes never shift), disables periodic full rebuild, and suppresses auto-tune-on-load (which would shift positions). Use it for any graph that will receive addNodes/removeNodes/updateNodes calls.

<bz-graph id="g" edges="data/karate.edges" nodes="data/karate.nodes" incremental>
</bz-graph>

<script type="module">
  const g = document.getElementById('g');
  await g.ready;

  // Add nodes + edges
  await g.addNodes(
    [{id: 'new1', group: 'analyst'}, {id: 'new2', group: 'admin'}],
    [{src: 'new1', dst: 'new2'}]
  );

  // Update properties (only changed nodes are re-projected)
  await g.updateNodes([{id: 'new1', group: 'manager'}]);

  // Remove by ID (edges cleaned up automatically)
  await g.removeNodes(['new2']);
</script>

The await g.ready Promise resolves once the build completes — modeled on document.fonts.ready and navigator.serviceWorker.ready. Use it from any async function; it works regardless of when you read it (before or after the build finishes). The legacy addEventListener('ready', ...) shape still works.

All three methods animate by default (existing items lerp to new positions, new items fade in). Pass { animate: false } to snap instantly — required when streaming many batches in a loop. A periodic full rebuild triggers automatically after rebuildThreshold × originalN cumulative inserts to refresh stale numeric bins and topology tokens; the incremental preset disables this entirely (sets the threshold to Infinity) since norm projections are deterministic per-node and rebuilds are unnecessary.

Each preset default can still be overridden by passing the matching attribute alongside incremental. For example, incremental rebuild-threshold="0.5" opts back into periodic rebuilds at 50% growth while keeping the rest of the preset.

Empty graphs and schema bootstrap

You can declare <bz-graph> with no edges URL and no inline data — the canvas builds empty and waits for addNodes to populate it. Useful when the graph is generated entirely at runtime (streaming, computed datasets, etc.):

<bz-graph id="g" incremental legend strengths="category:5,block:8"></bz-graph>

<script type="module">
  const g = document.getElementById('g');
  await g.ready;
  await g.addNodes(allMyNodes, [], { animate: false });
</script>

The first addNodes call into an empty graph runs a schema bootstrap: it derives the property-group names (category, block, etc.) from the first batch's node fields, builds projection matrices for them, computes numeric bins, and applies any strengths you set via attributes. The schema is then locked in.

Limitation: property keys not present in the first batch are silently dropped on subsequent batches — same as the existing static-build behavior. Include at least one representative node with every field you'll need in the first batch. For datasets where every node has the same fields (the typical case), this is invisible.

Why norm mode? There's a fundamental tradeoff: you can't have data-independent scale, optimal grid utilization, and zero displacement on insertion. Gaussian adapts its scale to the data (best grids, but every node shifts when new data arrives). Norm uses a fixed scale derived from the projection matrices (stable positions, but 5-30% worse grid utilization on datasets with low property diversity). Use norm for incremental/streaming use cases; use gaussian for static datasets where you load once and explore.

Live: incremental API with norm quantization.
{"nodes":[],"edges":[]}

Events

Each mutation dispatches an event on the canvas:

const el = document.querySelector('bz-graph');
el.addEventListener('nodesadded', e => console.log(`+${e.detail.count}, total: ${e.detail.total}`));
el.addEventListener('nodesremoved', e => console.log(`-${e.detail.count}, total: ${e.detail.total}`));
el.addEventListener('nodesupdated', e => console.log(`~${e.detail.count}`));

Canvas API

The same methods are available directly on BlitZoomCanvas for lower-level control:

import { createBlitZoomFromGraph } from './blitzoom.js';

const view = createBlitZoomFromGraph(canvas,
  [{id: 'a', group: 'x'}], [], { incremental: true }
);

await view.addNodes([{id: 'b', group: 'y'}], [{src: 'a', dst: 'b'}]);
await view.updateNodes([{id: 'b', group: 'z'}]);
await view.removeNodes(['b']);

The incremental: true JS option is the same preset as the <bz-graph incremental> attribute. Pass it to either factory (createBlitZoomView, createBlitZoomFromGraph) when the view will receive runtime mutations. Individual settings (quantMode, rebuildThreshold, autoTune) can still be passed alongside it to override the preset per-field.

Options Reference

All options are optional. Sensible defaults are applied.

OptionTypeDefaultDescription
initialLevelnumber3Starting zoom level index (0=L1, 13=L14, 14=RAW)
edgeModestring'curves''curves', 'lines', or 'none'
heatmapModestring'off''off', 'splat', or 'density'
quantModestring'gaussian''gaussian' (density-preserving), 'rank' (uniform), or 'norm' (order-independent, stable for incremental updates). Low-level — most users want incremental instead.
incrementalbooleanfalseBundled preset for runtime mutation/streaming. Sets quantMode='norm', rebuildThreshold=Infinity, autoTune=false. Each can still be overridden by passing it explicitly alongside.
rebuildThresholdnumber0.10Trigger periodic full rebuild after this fraction of original-N inserts via addNodes. Set to Infinity to disable. incremental defaults this to Infinity.
sizeBystring'edges'Node size: 'edges' (degree) or 'members' (count)
sizeLogbooleanfalseLog scale for node size
smoothAlphanumber0Topology blend weight, 0 (property only) to 1 (topology only)
strengthsobject{group:3, rest:1}Override property group strengths
labelPropsarray[]Property names to show as node labels
showLegendbooleanfalseDraw color legend in bottom-right corner
showResetBtnbooleanfalseDraw reset button in top-right corner
clickDelaynumber0Milliseconds to delay single-click for double-click disambiguation
keyboardTargetEventTargetcanvasElement to bind keyboard listener to (e.g. window)
webglbooleanfalseUse WebGL2 instanced rendering for geometry (details)
useGPUbooleanfalseForce WebGPU compute for projection and blend (details)
autoGPUbooleantrueAutomatically enable WebGPU when dataset is large enough to benefit (N×G > 2000). Set to false to disable.
colorSchemenumber0Color scheme index: 0=vivid, 1=viridis, 2=plasma, 3=inferno, 4=thermal, 5=grayscale, 6=diverging, 7=greens, 8=reds
colorBystringnullOverride which property group controls node colors. null = auto (highest-strength group). Set to a group name to pin coloring.
lightModebooleanfalseLight theme for labels, legend, and grid. Press C to cycle color schemes, set view.lightMode = true at runtime.

Color scheme and theme can also be changed at runtime:

import { SCHEME_VIRIDIS, SCHEME_DIVERGING } from './blitzoom-colors.js';

// Light mode with viridis colors
const view = createBlitZoomView(canvas, edges, nodes, {
  colorScheme: SCHEME_VIRIDIS,
  lightMode: true,
});

// Change at runtime
view.colorScheme = SCHEME_DIVERGING; // switch to diverging
view.lightMode = false;              // back to dark
view.cycleColorScheme();             // next scheme
console.log(view.colorSchemeName);   // "thermal"

Available constants: SCHEME_VIVID (0, default), SCHEME_VIRIDIS, SCHEME_PLASMA, SCHEME_INFERNO, SCHEME_THERMAL, SCHEME_GRAYSCALE, SCHEME_DIVERGING, SCHEME_GREENS, SCHEME_REDS.

colorScheme: 6 (diverging) · Press C to cycle
lightMode: true · grayscale

Callbacks

React to user interaction without touching the DOM:

const view = createBlitZoomView(canvas, edges, nodes, {
  onSelect(hit) {
    // hit = { type: 'node'|'supernode', item: nodeObj|supernodeObj }
    console.log('Selected:', hit.item.id || hit.item.bid);
  },
  onDeselect() {
    // Called when selection is cleared (click on empty space, Escape)
    console.log('Deselected');
  },
  onHover(hit) {
    // Called on every hover change (null when leaving a node)
    sidebar.textContent = hit ? (hit.item.label || hit.item.cachedLabel) : '';
  },
  onLevelChange(newIdx, prevIdx) {
    // Called after any zoom-level change (auto or manual)
    levelIndicator.textContent = 'L' + newIdx;
  },
  onRender() {
    // Called after each frame render. Useful for syncing external UI.
  },
  onKeydown(e) {
    // Called before default key handling. Return true to suppress.
    if (e.key === 'x') { myCustomAction(); return true; }
    return false;
  },
});

Strength Tuning

Property strengths control which attributes dominate the layout. The blend is a weighted sum of fixed 2D anchors that were computed once at load time, so a strength change only re-runs the sum and the quantization step — neither the MinHash signatures nor the per-group projections are touched.

kind:8 (by file type)
group:8 (by module)
// Change strengths after creation
view.setStrengths({ kind: 8, group: 1 }); // cluster by file type
view.setStrengths({ kind: 1, group: 8 }); // cluster by module

// Adjust topology smoothing
view.setAlpha(0.5); // pull connected nodes together

// Batch display options
view.setOptions({
  heatmapMode: 'density',
  edgeMode: 'lines',
  sizeBy: 'members',
  labelProps: ['file', 'kind'],
});

Auto-Tune

When loading an unfamiliar dataset, the default strengths may produce a collapsed or uniform layout. The auto-tune optimizer searches the strength/alpha/quant parameter space to find a configuration with good visual structure, then applies it automatically.

Synth Packages dataset: default strengths (left) vs auto-tuned (right).

Default strengths
autoTune: { strengths: true, alpha: true, quant: true }

Embedded usage

Pass autoTune in the options. The view renders immediately with defaults; the optimizer runs in the background and re-renders when done. A progress overlay appears on the canvas during optimization.

import { createBlitZoomView } from './blitzoom.js';

const view = createBlitZoomView(canvas, edgesText, nodesText, {
  // Auto-tune all parameters
  autoTune: { weights: true, alpha: true, quant: true },
});

// view is returned synchronously — usable immediately
// auto-tune runs async and re-renders when done

Control which parameters are tuned:

// Only tune strengths, keep alpha and quant as specified
const view = createBlitZoomView(canvas, edges, nodes, {
  smoothAlpha: 0.5,
  quantMode: 'gaussian',
  autoTune: { strengths: true, alpha: false, quant: false },
});

Explicit strengths, smoothAlpha, and quantMode in the options take precedence over tuned values.

Programmatic usage

Call autoTuneStrengths directly for full control:

import { autoTuneStrengths } from './blitzoom.js';

const result = await autoTuneStrengths(
  view.nodes, view.groupNames, view.adjList, view.nodeIndexFull,
  {
    weights: true, alpha: true, quant: true,
    onProgress(info) {
      console.log(`${info.phase} ${info.step}/${info.total}`);
    },
  }
);

console.log(result);
// { strengths: {group:8, ...}, alpha: 0.5, quantMode: 'gaussian',
//   labelProps: ['group'], score: 0.42, blends: 87, quants: 174, timeMs: 320 }

// Apply to an existing view
for (const g of view.groupNames) view.propStrengths[g] = result.strengths[g];
view.smoothAlpha = result.alpha;
view.quantMode = result.quantMode;
view.setStrengths(result.strengths); // triggers re-blend and render

How it works

The optimizer maximizes spread × clumpiness at zoom level L5 (32×32 grid). Spread (fraction of cells occupied) penalizes collapse. Clumpiness (coefficient of variation of per-cell node counts) penalizes uniform scatter and rewards clusters with gaps between them. The search runs in two phases:

  1. Preset scan. Balanced strengths and each group solo, crossed with alpha values and quant modes. Picks the best starting point.
  2. Coordinate descent. Optimizes one parameter at a time from the best preset. Three rounds, early exit if no improvement.

Each evaluation runs a full blend + quantize cycle (~1-3ms per evaluation depending on graph size). Total time is typically 200-800ms for graphs under 5K nodes.

SVG Export

Export the current graph view as a standalone SVG. In the viewer, press S to download. Programmatically:

import { exportSVG } from './blitzoom.js';

const svgString = exportSVG(view);
// Returns a complete <svg> string with background, grid, edges,
// density heatmap contours, circles, labels, and legend

The export reads the same view state (nodes, edges, zoom, pan, level, colors) as the canvas renderer, producing a vector image suitable for print or embedding in documents.

Headless SVG (no DOM)

Use createSVGView to build a view from pipeline data without any browser APIs — works in Deno, Node.js, or Web Workers:

import { createSVGView, exportSVG } from './blitzoom.js';

// After running the pipeline and blending (see Offline Pipeline below)
const view = createSVGView(nodes, result.edges, {
  width: 800, height: 600,
  colorBy: 'group',
  heatmapMode: 'density',
  showLegend: 1,
});

const svg = exportSVG(view);
Deno.writeTextFileSync('graph.svg', svg);

Offline Pipeline

Run the full pipeline without a canvas, in Node.js or Deno. Useful for batch processing, precomputation, or analysis scripts.

import { runPipeline } from './blitzoom-pipeline.js';
import { unifiedBlend, normalizeAndQuantize, buildLevel } from './blitzoom-algo.js';

// Parse + tokenize + project (no DOM needed)
const result = runPipeline(edgesText, nodesText);
console.log(result.nodeArray.length, 'nodes');
console.log(result.groupNames);  // ['group', 'label', 'structure', ...]

// Hydrate nodes with projections
const G = result.groupNames.length;
const nodes = result.nodeArray.map((n, i) => {
  const projections = {};
  for (let g = 0; g < G; g++) {
    const off = (i * G + g) * 2;
    projections[result.groupNames[g]] = [result.projBuf[off], result.projBuf[off + 1]];
  }
  return { ...n, projections, px: 0, py: 0, gx: 0, gy: 0, x: 0, y: 0 };
});

// Build adjacency
const nodeIndex = Object.fromEntries(nodes.map(n => [n.id, n]));
const adjList = Object.fromEntries(nodes.map(n => [n.id, []]));
for (const e of result.edges) {
  if (adjList[e.src] && adjList[e.dst]) {
    adjList[e.src].push(e.dst);
    adjList[e.dst].push(e.src);
  }
}

// Blend with custom strengths
const strengths = { group: 5, edgetype: 8 };
for (const g of result.groupNames) strengths[g] ??= 1;
unifiedBlend(nodes, result.groupNames, strengths, 0, adjList, nodeIndex, 5, 'rank');

// Build a zoom level and inspect supernodes
const level = buildLevel(4, nodes, result.edges, nodeIndex,
  n => n.group, n => n.label || n.id, () => '#888');

console.log(level.supernodes.length, 'supernodes at L4');
console.log(level.snEdges.length, 'super-edges');

// Find the largest supernode
const biggest = level.supernodes.sort((a, b) => b.members.length - a.members.length)[0];
console.log(biggest.cachedLabel, biggest.members.length, 'members');
The pipeline modules are pure ES modules with no DOM dependencies. They work in Deno (deno run --allow-read), Node.js 18+ (with --experimental-vm-modules), and browsers.

Performance Guide

BlitZoom scales well because the pipeline is O(n) in node count (MinHash + projection) and rendering uses hierarchical aggregation (only visible supernodes are drawn). Here are typical timings on a modern desktop:

ScalePipelineRenderNotes
~1K nodes<10ms<1msInstant, any mode
~5K nodes<50ms<5msSmooth at 60fps
~50K nodes<500ms<20msConsider disabling heatmap
~367K nodes (Amazon)~4s (GPU)~5ms (WebGL)GPU pipeline + WebGL rendering recommended

Tips for large graphs

Common Pitfalls

SymptomCauseFix
All nodes collapse to a single point All property values are identical across nodes (zero variance) Add distinguishing properties, increase smoothAlpha to use topology, or run auto-tune
Uniform random scatter, no clusters No .nodes file (edge-only dataset) Set smoothAlpha: 1.0 to lay out purely by topology. Provide a .nodes file if properties are available
Slow pipeline on very large graphs CPU-bound MinHash + projection at scale Use useGPU: true for WebGPU compute. Disable heatmap with heatmapMode: 'off'
Rank quantization falls back to CPU Rank sort requires float64 precision not available in GPU compute Use quantMode: 'gaussian' (the default) for GPU-compatible quantization
Slow rendering at high zoom Many visible nodes/edges at detailed zoom levels Enable webgl: true, use edgeMode: 'none' or 'lines' instead of 'curves'

Similarity Queries

Use MinHash directly to estimate Jaccard similarity between token sets:

import { computeMinHash, jaccardEstimate } from './blitzoom-algo.js';

const sigA = computeMinHash(['group:Person', 'label:jeffrey', 'label:epstein']);
const sigB = computeMinHash(['group:Person', 'label:ghislaine', 'label:maxwell']);
const sigC = computeMinHash(['group:Organization', 'label:us', 'label:government']);

console.log(jaccardEstimate(sigA, sigB)); // ~0.2 (share group:Person)
console.log(jaccardEstimate(sigA, sigC)); // ~0.0 (no shared tokens)
console.log(jaccardEstimate(sigA, sigA)); // 1.0 (identical)

For on-demand signatures of existing nodes (using the same tokenization as the pipeline):

import { computeNodeSig } from './blitzoom-pipeline.js';

const sig = computeNodeSig(node); // Float64Array[128]

Hierarchy Access

The zoom hierarchy is the core trick: 14 aggregation levels from two stored uint16 coordinates per node, derived via bit shifts. You can access any level programmatically.

import { cellIdAtLevel, buildLevel } from './blitzoom-algo.js';

// Which cell is a node in at level 4? (16x16 grid)
const bid = cellIdAtLevel(node.gx, node.gy, 4);
const cx = bid >> 4;          // cell column
const cy = bid & 0b1111;     // cell row

// Build level 6 (64x64 grid)
const lv = buildLevel(6, nodes, edges, nodeIndex,
  n => n.group, n => n.label, val => colors[val] || '#888');

// Each supernode: { bid, members[], ax, ay, cachedColor, cachedLabel, ... }
for (const sn of lv.supernodes) {
  console.log(`Cell (${sn.cx},${sn.cy}): ${sn.members.length} nodes, label: ${sn.cachedLabel}`);
}

// Super-edges: { a: bidA, b: bidB, weight: edgeCount }
for (const e of lv.snEdges) {
  console.log(`${e.a} ↔ ${e.b}: ${e.weight} edges`);
}
Bit-prefix containment is exact: a node's cell at level L is always a sub-cell of its cell at level L-1. This is guaranteed by the bit-shift construction, not by any heuristic.

WebGL Rendering

Pass webgl: true to render geometry (circles, edges, heatmaps, grid) via WebGL2 instanced draw calls. Text (labels, counts, legend) stays on a Canvas 2D overlay. The two canvases are stacked automatically.

Epstein dataset: Canvas 2D (left) vs WebGL2 (right). Same data, same layout, same interaction.

Canvas 2D (default)
webgl: true
import { createBlitZoomView } from './blitzoom.js';

// Canvas 2D (default)
const view2d = createBlitZoomView(canvas1, edges, nodes, {
  edgeMode: 'curves', showLegend: true,
});

// WebGL2
const viewGL = createBlitZoomView(canvas2, edges, nodes, {
  webgl: true,
  edgeMode: 'curves', showLegend: true,
});

// Toggle at runtime
viewGL.useWebGL = false;  // switch back to Canvas 2D
viewGL.useWebGL = true;   // switch to WebGL2
WebGL rendering produces identical visual output to Canvas 2D. The benefit is reduced CPU load for large views — geometry draw calls are offloaded to the GPU via instanced rendering. See WebGL Rendering for the full architecture: SDF circles, GPU-tessellated Bezier curves, two-pass density heatmap.

<bz-graph> Web Component

Embed a graph with a single HTML element — no JavaScript needed for basic use. Include bz-graph.js in your page and use the <bz-graph> tag. Always add the :not(:defined) CSS rule to prevent a flash of raw text content before the component loads — especially important for inline JSON/SNAP data.

From files

<script type="module" src="dist/blitzoom.bundle.js"></script>
<style>bz-graph:not(:defined) { visibility: hidden; }</style>

<bz-graph edges="data/epstein.edges" nodes="data/epstein.nodes"
          level="3" heatmap="density" legend
          strengths="group:5,edgetype:8" label-props="label">
</bz-graph>

Inline JSON

Wrap data in <script type="application/json"> to avoid any flash of raw text — no :not(:defined) CSS needed. Format is auto-detected from the script type:

<bz-graph level="2" legend color-scheme="6">
  <script type="application/json">
  {
    "nodes": [
      {"id": "alice", "group": "eng", "label": "Alice", "lang": "Go"},
      {"id": "bob",   "group": "eng", "label": "Bob",   "lang": "Go"},
      {"id": "carol", "group": "des", "label": "Carol", "lang": "JS"}
    ],
    "edges": [
      {"src": "alice", "dst": "bob"},
      {"src": "bob", "dst": "carol"}
    ]
  }
  </script>
</bz-graph>

You can also use raw text with format="json" (requires the :not(:defined) CSS rule to prevent FOUC).

Inline SNAP

<bz-graph level="2" legend edge-mode="lines">
alice	bob
bob	carol
carol	alice
</bz-graph>

Attributes

AttributeExampleDescription
edges"data/karate.edges"URL to .edges file
nodes"data/karate.nodes"URL to .nodes file (optional)
format"json"Inline data format: json or snap (default)
level"3"Initial zoom level (0-indexed)
heatmap"density"Heatmap mode: off, density, splat
edge-mode"curves"Edge mode: curves, lines, none
alpha"0.5"Topology blend alpha (0–1)
color-scheme"1"Color scheme index (0–8)
strengths"group:5,kind:8"Property strengths
label-props"label,group"Properties shown as labels
legend(boolean)Show color legend
reset-btn(boolean)Show reset button
light-mode(boolean)Light theme
webgl(boolean)WebGL2 rendering
color-by"group"Override color group (null = auto, highest-strength group)

Access the underlying BlitZoomCanvas via element.view for programmatic control:

const el = document.querySelector('bz-graph');
el.view.colorScheme = 3;      // change at runtime
el.view.cycleColorScheme();   // next scheme
el.setAttribute('level', '5'); // reactive attribute

The ready event fires when the graph finishes loading and is available for programmatic access:

el.addEventListener('ready', () => {
  console.log(el.view.groupNames); // ['group', 'label', ...]
});

<bz-compass> Radial Control

The <bz-compass> component provides a radial 2D control for adjusting property strengths and bearings simultaneously. Each property group is a spoke — drag distance from center sets the strength, angular offset sets the bearing rotation.

Connect it to a <bz-graph> with the for attribute — no JavaScript needed:

<script type="module" src="bz-compass.js"></script>

<bz-graph id="myGraph" edges="data/synth-packages.edges" nodes="data/synth-packages.nodes"
          level="3" legend auto-tune='{"strengths":true,"alpha":true}'>
</bz-graph>
<bz-compass for="myGraph"></bz-compass>

The compass auto-discovers groups when the graph loads and keeps in sync bidirectionally — dragging a compass handle updates the graph, and external changes (e.g. auto-tune) update the compass.

AttributeExampleDescription
for"myGraph"ID of a <bz-graph> to bind to
max-strength"10"Maximum strength value (default 10)

Interaction: drag handles freely (strength + bearing), Shift+drag for strength only, Alt+drag for bearing only. Double-click resets a handle to zero. Right-click resets bearing only. Keyboard: Tab cycles handles, ↑↓ adjust strength, ←→ adjust bearing.

Standalone (programmatic) usage without <bz-graph>:

const compass = document.createElement('bz-compass');
compass.groups = [
  { name: 'group', color: '#e64', strength: 8, bearing: 0 },
  { name: 'kind',  color: '#4ae', strength: 3, bearing: 0.5 },
];
compass.addEventListener('input', e => {
  console.log(e.detail); // { name, strength, bearing }
});

See the full demo page for live examples.

Required Files

BlitZoom has no build step and no npm package.

Quickest: single-file bundle

One file, ~98KB minified (gzipped), includes everything:

<script type="module" src="dist/blitzoom.bundle.js"></script>

Use <bz-graph> elements or import createBlitZoomView from the same file.

Unbundled: ES modules

Copy all .js files from docs/ into one directory. Import from blitzoom.js — it re-exports the full public API:

<script type="module">
  import { createBlitZoomView } from './blitzoom.js';
</script>

Offline / headless (Deno, Node, Web Worker)

Only two files needed, zero DOM dependencies:

import { runPipeline } from './blitzoom-pipeline.js';

Requires blitzoom-algo.js + blitzoom-pipeline.js.

Full file list

FileRole
blitzoom.jsPublic API entrypoint (recommended import)
blitzoom-canvas.jsCanvas component, createBlitZoomView() factory
blitzoom-algo.jsMinHash, projection, blend, quantization
blitzoom-pipeline.jsSNAP parsing, graph building, tokenization
blitzoom-renderer.jsCanvas 2D rendering, hit testing, layout
blitzoom-utils.jsAuto-tune optimizer
blitzoom-gl-renderer.jsWebGL2 instanced rendering
blitzoom-gpu.jsWebGPU compute acceleration
blitzoom-svg.jsSVG export (exportSVG, createSVGView)
blitzoom-colors.jsColor schemes (vivid, viridis, plasma, etc.)
bz-graph.js<bz-graph> web component
dist/blitzoom.bundle.jsMinified single-file bundle (all of the above)

How It Works · Viewer · GitHub