Skip to content

Repository files navigation

zxcc - ZX Calculus Components

CI Lit TypeScript prek Storybook

Framework-agnostic web component for rendering ZX-calculus diagrams.

Example basic layout Example algebraic layout
Basic layout demo Algebraic layout demo

Checkout the demo

Usage

npm install @adnathanail/zxcc
<script type="module" src="./node_modules/@adnathanail/zxcc/dist/index.bundle.js"></script>

<zx-diagram id="d"></zx-diagram>

<script type="module">
  document.getElementById('d').diagram = {
    nodes: [
      { id: 0, type: 'input',  ioId: 0 },
      { id: 1, type: 'spider', color: 'Z', phase: 'π/2' },
      { id: 2, type: 'output', ioId: 0 },
    ],
    edges: [
      { src: 0, tgt: 1 },
      { src: 1, tgt: 2 },
    ],
  }
</script>

diagram is a JS property, not an attribute — set it via a DOM reference so the object round-trips without JSON coercion.

Assign a new object to change the diagram; layout runs when the property's identity changes, so mutating the object already assigned paints nothing new. If you must mutate in place, call el.refresh() afterwards. Note that either way the re-layout resets the drawing: dragged nodes return to their laid-out positions and the selection is cleared.

The drawing responds to mouse, pen and touch alike: press a node or a dot to select it and drag to move it. A drawing wider than its box scrolls, and on a touch screen dragging the empty canvas is what scrolls it — so the rubber-band selection over blank canvas is a mouse and pen gesture only.

Diagram shape

interface DiagramData {
  nodes: DiagramNode[]
  edges: DiagramEdge[]
  boxes?: { kind: 'stack' | 'compose'; nodeIds: number[] }[]
  labels?: [number, string][]  // node-id → phase-label override
  pauliWeb?: { src: number; tgt: number; kind: 'X' | 'Y' | 'Z' | 'I' }[]
  scalar?: string        // global scalar, drawn below the diagram
}

interface DiagramNode {
  id: number
  type: 'spider' | 'input' | 'output' | 'hadamard' | 'wire'
      | 'w-input' | 'w-output' | 'z-box'
  color?: 'Z' | 'X'      // spider only
  phase?: string         // pre-formatted (e.g. "π/2", "-π/4")
  ioId?: number          // input/output index
  col?: number           // optional pre-computed column
  qubit?: number         // optional pre-computed qubit row
  ground?: boolean       // draws a ground symbol below the node
  vdata?: [string, unknown][]  // annotations drawn above the node
}

interface DiagramEdge {
  src: number
  tgt: number
  kind?: 'simple' | 'hadamard' | 'w-io' | string   // default 'simple'; see edgeColors
}

If any node carries col, auto-layout is skipped and every node is expected to carry both col and qubit. Otherwise a BFS from the inputs assigns rows and qubits.

An edge with src === tgt renders as a self-loop arc.

Element attributes

These mirror the keyword arguments of pyzx's draw_d3() and control presentation only — graph structure always lives in diagram.

Some can be set as attributes in HTML, some must be set with JS on the element, similarly to diagram above

Attribute Property Default Meaning
show-labels showLabels false Draw node/wire IDs
color-scheme colorScheme original original / rgb / grayscale
scale scale derived Pixels per row/qubit
view-mode viewMode graph graph / hypergraph / both-vertical / both-horizontal - see Hypergraph view.
disable-io-blobs-in-hypergraph disableIOBlobsInHypergraph false Leave out the single-dot blob around each input/output in the hypergraph view.
colors null Full palette override (Record<string, string>), overrides color-scheme.
edgeColors null Define custom wire 'kinds', to display wires in custom colours, overrides both of the above for the kinds named.
<zx-diagram show-labels color-scheme="grayscale" scale="40"></zx-diagram>

The palettes are exported too, if you want to build a variant:

import { ORIGINAL_COLORS, RGB_COLORS, GRAYSCALE_COLORS, COLOR_SCHEMES } from '@adnathanail/zxcc'

Wire colours and custom wire kinds

edgeColors maps edge kinds to colours — the kinds the diagram is written in, rather than pyzx's edge / Hedge / Xedge palette entries. Only the kinds you name move; the rest stay on whatever color-scheme or colors decided.

An edge's kind is only a colour: nothing in the layout or the geometry reads it. So it isn't limited to the three built-in kinds — use any string you like and give it a colour here:

const d = document.getElementById('d')
d.diagram = {
  nodes: [/* … */],
  edges: [
    { src: 0, tgt: 1, kind: 'control' },     // a kind of your own
    { src: 1, tgt: 2, kind: 'hadamard' },    // a built-in, recoloured
    { src: 2, tgt: 3 },                      // 'simple', left alone
  ],
}
d.edgeColors = { control: '#00aa55', hadamard: '#ff00aa' }

A kind you don't give a colour draws like a plain wire. Both views read a wire's colour from the same lookup, so the hypergraph view's dot for a wire always comes out the colour of the wire itself.

Hypergraph view

Set view-mode="hypergraph" (or the viewMode property) to draw the diagram's hypergraph dual instead of the diagram — the roles of wires and spiders swap. Every ZX wire becomes a node/dot; every ZX spider becomes a hyperedge, drawn as a blob enclosing the dots of the wires incident to it. An input/output is incident to one wire, so its blob is a circle around that one dot.

<zx-diagram id="d" view-mode="hypergraph"></zx-diagram>

Only Z/X spiders, I/O, and Hadamards are supported in this view, other node types will be rejected.

Input/output blobs

Each input/output gets a blob of its own, a circle around the single dot for its leg. That circle is what tells a boundary leg apart from a self-loop: both are one dot hanging off one spider, and the rule that every dot sits in two blobs — one per end of its wire — only reads off the drawing while the boundaries have blobs too. On a diagram with many boundaries it is also a lot of outline, so it can be turned off:

<zx-diagram id="d" view-mode="hypergraph" disable-io-blobs-in-hypergraph></zx-diagram>
document.getElementById('d').disableIOBlobsInHypergraph = true

The dots stay either way — a boundary leg is still a wire. Only the circle round it goes, and with it goes anything that circle could be pressed or counted for: an input/output can no longer be selected in this view, and a dot near one no longer counts as trespassing into it.

Both views

You can also have both the graph & hypergraph views, by setting the view-mode to both-vertical or both-horizontal. Selections will be synced across the two viewers.

Exported constants

The main entry defines a custom element as it loads, so it needs a DOM and won't import in Node. To allow build-time usage of constants by Node, we have a second entry point, @adnathanail/zxcc/constants:

import { VIEW_MODES, COLOR_SCHEMES } from '@adnathanail/zxcc/constants'

if (!VIEW_MODES.includes(mode)) throw new Error(`unknown view-mode: ${mode}`)

Development

Install npm dependencies:

npm install

Set up pre-commit hooks (install prek first):

prek --install

Demo

To view the Storybook example usages, and an interactive Playground story with controls:

npm install
npm run storybook

Testing

You can run tests from inside the Storybook web interface. If you want to run them via the terminal:

npm run test

And to run with coverage:

npm run coverage

Then open coverage/index.html in a browser.

Building from source

npm run build       # tsc → dist/, then rollup bundles to dist/index.bundle.js
npm run watch       # rollup --watch (rerun tsc manually on .ts changes)

The bundle is self-contained with no runtime dependencies.

Analyzing bundle composition

npm run analyze

About

Framework-agnostic web component for rendering ZX-calculus diagrams

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages