Syntax
One rule per line. Lines starting with # or // are comments.
NODES.property: expression
EDGES.property: expression
CONNECT: expression
ANIM.varname: min..max
CRAWL[N].property: expression
Node Visual Properties
Set appearance per node. The expression receives node (metadata), _node (full object), and graph (helpers).
# Color by a metadata field
NODES.color: node.type
# Conditional color
NODES.color: node.age > 50 ? "red" : "blue"
# Size by connection count
NODES.size: graph.degree(_node)
# Custom label
NODES.label: `${node.name} (${node.type})`
# Transparency
NODES.opacity: node.active ? 1 : 0.3
# Shape
NODES.shape: node.kind
# Position — center-point (locks in place, disables drag)
NODES.x: node.col * 200
NODES.y: node.row * 80
# Position — edge-based (left/top of bounding box)
NODES.left: node.start_date * 100
NODES.top: node.priority * 80
# Dimensions (independent width/height in pixels)
NODES.width: node.duration * 50
NODES.height: 40
Properties: color, size, label, description, opacity, shape, x, y, left, top, width, height
Colors can be hex strings ("#ff0000"), or any value — strings and numbers are auto-mapped to distinct colors.
x/y set the node's center point. left/top set the upper-left corner (auto-converted using width/height). If both are set, left/top win. Any position property locks the node in place — no dragging, pinned during force layout.
Dimensions (width/height) override the default 140×50 box. If size is set without explicit width/height, it scales both proportionally.
Edge Visual Properties
Set appearance per edge. The expression receives edge (metadata), from/to (endpoint metadata), _edge/_from/_to (full objects), and graph.
# Color edges by source type
EDGES.color: from.type
# Width by weight
EDGES.width: edge.weight
# Label
EDGES.label: edge.relationship
# Transparency
EDGES.opacity: edge.confidence
# Physics spring strength (0–1, default 0.4)
EDGES.spring: edge.weight / 10
Properties: color, width, label, opacity, spring
Filtering (Visibility)
Hide nodes or edges that don't match. Multiple visible rules are AND-ed — all must be true.
# Only show people
NODES.visible: node.type === "Person"
# Only show high-weight edges
EDGES.visible: edge.weight > 5
# Combine filters (both must pass)
NODES.visible: node.type === "Person"
NODES.visible: node.age > 30
Grouping
Collapse nodes that return the same value into a single group node. Groups with only 1 member are not collapsed. First NODES.group rule wins.
# Group by type
NODES.group: node.type
# Group by computed bucket
NODES.group: node.age > 40 ? "senior" : "junior"
Group nodes expose metadata: _groupKey, _memberCount, _memberIds. Edges between groups are collapsed with a _count.
Group Aggregation
Compute metadata on group nodes/edges with NODES.group.<key>: / EDGES.group.<key>: rules. Expressions receive nodes or edges (member metadata arrays) and helpers: sum, avg, min, max.
# Aggregate member volumes onto group nodes
NODES.group.total_volume: sum(nodes.map(n => n.volume_mbd))
NODES.group.avg_volume: avg(nodes.map(n => n.volume_mbd))
NODES.group.big_exporters: nodes.filter(n => n.volume_mbd > 3).length
# Aggregate edge flows
EDGES.group.total_flow: sum(edges.map(e => e.volume_mbd))
# Use aggregated values in visual rules
NODES.size: node.total_volume * 3
EDGES.width: edge.total_flow * 2
Programmatic Edges (CONNECT)
Create edges between every node pair where the expression is true. Receives from/to (metadata), _from/_to (full objects), and graph.
# Link nodes that share an attribute
CONNECT: from.department === to.department
# Link assignees to their tasks
CONNECT: from.assignee === to.email
# Use optional chaining for sparse data
CONNECT: from.tags?.includes(to.name)
Edges where both sides of a comparison are undefined are suppressed (prevents false matches on missing fields).
Use Bake Selected / Bake All to convert programmatic edges into permanent manual edges.
Animation Variables (ANIM)
Declare scrub-able variables that can be used in any expression. A slider bar appears below the toolbar when ANIM rules are present.
# Numeric range
ANIM.threshold: 0..100
# With step size
ANIM.t: 0..1 step 0.01
# Date range (ISO format)
ANIM.cutoff: 2020-01-01..2025-12-31
Use the variable in expressions via _anim.varname:
# Filter nodes by animated threshold
NODES.visible: node.score > _anim.threshold
# Animate position over time
NODES.left: _anim.t * 800
# Date filtering with date() helper
NODES.visible: date(node.created) < _anim.cutoff
The date() helper converts strings or values to epoch milliseconds. Available in all expressions.
date("2024-01-15") # parse ISO string → ms
date(node.timestamp) # convert value → ms
date() # current time (Date.now())
Graph Helpers
The graph object provides structural queries. Pass _node, _from, or _to (the full node objects).
graph.degree(_node) # total edges
graph.inDegree(_node) # incoming edges
graph.outDegree(_node) # outgoing edges
graph.neighbors(_node) # [{metadata}, ...]
graph.inbound(_node) # [{metadata}, ...]
graph.outbound(_node) # [{metadata}, ...]
graph.edges(_node) # [{metadata}, ...]
graph.nodes # all node metadata
graph.min("field") # min value across all nodes
graph.max("field") # max value across all nodes
graph.scale("field") # normalize to 0..1
graph.scale("field", lo, hi) # normalize to lo..hi
Normalizing Numeric Data
graph.scale() maps a field to a range across the whole graph — ideal for size, width, opacity:
# Node size from 0.5× to 3× based on weight
NODES.size: graph.scale("weight", 0.5, 3)
# Edge width 1..8 based on flow
EDGES.width: graph.scale(_from, "flow", 1, 8)
Or compose the primitives for full control:
# Equivalent longhand
lerp(norm(node.weight, graph.min("weight"),
graph.max("weight")), 0.5, 3)
# Explicit node form (also works)
NODES.size: graph.scale(_node, "weight", 0.5, 3)
Built-in Functions
when(cond, value) # returns value if cond is truthy, undefined otherwise
no(value) # true if value is null or undefined
date(value) # convert to days since epoch (for date arithmetic)
norm(val, min, max) # normalize to 0..1, clamped
lerp(t, a, b) # linear interpolation: a + t*(b-a)
clamp(val, lo, hi) # clamp to [lo, hi]
when() is useful for applying rules to a subset of nodes/edges. Because undefined results don’t overwrite earlier rules, multiple when() rules for the same property compose cleanly:
# Color countries by volume, chokepoints by throughput
NODES.color: when(node.type === "country", node.volume_mbd)
NODES.color: when(node.type === "chokepoint", node.throughput_mbd)
Context Variables
| Context | Variables |
| Node rules | node _node graph _anim date() |
| Edge rules | edge from to _edge _from _to graph _anim date() |
| CONNECT | from to _from _to graph _anim date() |
| ANIM | min..max range definition (not an expression) |
Shorthand variables (node, edge, from, to) expose metadata directly. Underscore-prefixed versions (_node, _edge, etc.) are the full objects with id, x, y, etc.
Tips
- Expressions are JavaScript — use ternaries, template literals, optional chaining, etc.
- Expressions are read-only — accidental assignment (
= instead of ===) throws an error instead of mutating your data.
- Rules apply top to bottom. Later rules for the same property overwrite earlier ones.
- A syntax error in one rule won't break others.
Crawlers (CRAWL[N])
Animated tokens that follow paths through the graph. Each crawler is an indexed group (CRAWL[0], CRAWL[1], ...) with its own path and style. Unindexed CRAWL.* sets defaults inherited by all crawlers.
ANIM.t: 0..1 step 0.01
CRAWL.t: _anim.t
CRAWL.color: "#ff6600"
# Crawler 0: appears at segment 2
CRAWL[0].path: [1, 34, 35, 18]
CRAWL[0].visible: [0, 0, 1, 1]
# Crawler 1: different route, blue
CRAWL[1].path: [2, 34, 37, 36]
CRAWL[1].color: "#0066ff"
CRAWL[1].sizes: [10, 8, 6, 4]
| Property | Scope | Default | Description |
path | CRAWL[N] | — | Node ID sequence [A, B, C, ...] |
visible | CRAWL[N] | all visible | Per-segment visibility [0|1, ...] |
sizes | CRAWL[N] | — | Per-segment radius [num, ...] |
color | both | "#ff6600" | Circle fill color |
size | both | 5 | Base circle radius in px |
t | both | _anim.t | Position along path, 0..1 |
CRAWL[N].* overrides CRAWL.* defaults for that crawler. path, visible, and sizes are per-crawler only.