Decorators
Spytial decorators describe what should hold about a layout — not how to render it. They're declarative constraints, attached to a type once with an attribute, applied everywhere a value of that type appears.
A decorator like #[align(selector = "reports_to", direction = "horizontal")]
does not say "draw an arrow from A to B." It says "atoms related by
reports_to should line up horizontally." The layout engine picks the
actual coordinates, as long as the constraint holds. Three things follow:
- They compose. Add an orientation rule, then a color, then a hide rule — the diagram refines step by step. No rule has to know about any other.
- They're per-type, not per-instance. Decorate
RBNodeonce; everyRBNodein the value picks up the same rules, however deeply nested. - They're registered and collected transitively. Decorating
Personis enough for those rules to apply whereverPersonappears inside aVec<T>,Option<T>,Box<T>, or their nested combinations.
A red-black tree, one rule at a time
The default layout gets you most of the way for trees and lists; a few
decorators do the rest. This is the full
examples/rbt.rs
demo, built up stage by stage — each snippet is an attribute you add to
the struct.
Stage 1 — no decorators. Debug + Serialize is enough for a correct but
flat structural graph; you can't read it as a tree yet.
#![allow(unused)] fn main() { #[derive(Debug, Serialize)] struct RBNode { key: u32, color: Color, left: Option<Box<RBNode>>, right: Option<Box<RBNode>>, } }
Stage 2 — show the key. Add the derive and the first decorator. Each
RBNode atom now carries its key as a label, so you can see what's where.
#![allow(unused)] fn main() { #[derive(Debug, Serialize, SpytialDecorators)] #[attribute(field = "key")] }
Stage 3 — make it a tree. Left children go down-and-left, right children down-and-right; the layout now encodes BST order.
#![allow(unused)] fn main() { #[orientation(selector = "{x, y : RBNode | x->y in left}", directions = ["left", "below"])] #[orientation(selector = "{x, y : RBNode | x->y in right}", directions = ["right", "below"])] }
The selector reads: "for any pair (x, y) of RBNodes where x -> y is
in the left relation, place y to the left of and below x."
Stage 4 — make it a red-black tree. Color nodes by their color
field. The pattern matches any matching node, not specific instances.
#![allow(unused)] fn main() { #[atom_style(selector = "{x : RBNode | @:(x.color) = \"Red\"}", border_style(color = "red"))] #[atom_style(selector = "{x : RBNode | @:(x.color) = \"Black\"}", border_style(color = "black"))] }
\"Red\" is a string literal in the query, escaped because the selector is
itself a Rust string. Quoting matters: a bare name that resolves to nothing
is the empty relation, so the comparison would just be false and the rule
would quietly never fire — the diagram still renders, only unstyled. The
diagram flags this with a ⚠ n selector warnings bar naming the decorator
and its selector.
A raw string carries the same query without the backslashes, if you prefer to read the selector as the query engine sees it:
#![allow(unused)] fn main() { #[atom_style(selector = r#"{x : RBNode | @:(x.color) = "Red"}"#, border_style(color = "red"))] }
Stage 5 — hide the scaffolding. The Color enum atoms, the u32
keys, and the None sentinels are already implied by node color, labels,
and absent edges. Drop them from the canvas.
#![allow(unused)] fn main() { #[hide_atom(selector = "Color + u32 + None")] }
Color + u32 + None is a set expression: match any atom whose type is
Color, u32, or the literal None. Each rule refined an existing
structure rather than imposing an external aesthetic.
Attribute reference
Every decorator is a Rust attribute on a type that derives
SpytialDecorators. They group into three families.
Display
| Attribute | What it does |
|---|---|
#[attribute(field = "...", selector = "...", filter = "...")] | Promote a field's value into the node's label. |
#[flag(name = "...")] | Set a global display flag. name is required and is one of hideDisconnected, hideDisconnectedBuiltIns. |
Layout constraints
| Attribute | What it does |
|---|---|
#[orientation(selector = "...", directions = [...])] | Place matched pairs in a direction. Required; each value is one of "above", "below", "left", "right", or a "directly*" variant. above/below and left/right are mutually exclusive, and a directly* value admits only its own plain counterpart alongside it. |
#[align(selector = "...", direction = "horizontal" | "vertical")] | Force matched atoms to share an axis. |
#[cyclic(selector = "...", direction = "clockwise" | "counterclockwise")] | Arrange matched atoms around a ring. direction defaults to clockwise. |
#[group(selector = "...", name = "...", show_label = ...)] | Cluster related atoms into a region. A binary selector builds one group per distinct first-column atom, keyed by it; a unary selector builds a single unkeyed group. Set show_label = false to hide the group caption and its background pill while keeping the hull. |
orientation, align, cyclic, and group each take an optional
negated = true — see Negated constraints below.
Styling, filtering, and overrides
Styling follows spytial-core's block system, introduced in 3.x. Blocks are written as nested groups that mirror the YAML 1:1:
line_style(color = "...", pattern = "solid" | "dashed" | "dotted", weight = 2.0, highlight = "...")— a drawn edge line;text_style(size = "small" | "normal" | "large", color = "...")— any label;border_style(color = "...", width = 2.0)/fill_style(color = "...")— an atom's outline and interior;icon_style(path = "...", placement = "full" | "badge", opacity = 0.4)— an atom's icon.
Every block field is optional — set only what you mean.
Which keys each attribute accepts, and which values are legal, are generated
from spytial-core's own language manifest (see spec-codegen/). A key or value
this crate rejects is one spytial-core would reject or silently ignore, so
typos, unknown block leaves, out-of-vocabulary values, and out-of-range numbers
are all compile errors rather than a diagram that renders without them.
| Attribute | What it does |
|---|---|
#[atom_style(selector = "...", border_style(...), fill_style(...), icon_style(...), text_style(...), show_label = ...)] | Style matched atoms' border, interior fill, icon, and label independently. |
#[size(selector = "...", height = ..., width = ...)] | Override node dimensions (in diagram units). |
#[icon(selector = "...", path = "...", show_labels = ...)] | Deprecated by spytial-core 4.2; rewrites to atom_style. Its single show_labels boolean drove label visibility and icon geometry at once — prefer icon_style(...) plus show_label, which are independent. |
#[edge_style(field = "...", line_style(...), text_style(...), show_label = ..., hidden = ...)] | Style relation arrows: the drawn line, the edge's label, and visibility. |
#[hide_field(field = "...", selector = "...", filter = "...")] | Suppress a relation from the rendering. |
#[hide_atom(selector = "...")] | Suppress matched atoms entirely. |
#[inferred_edge(name = "...", selector = "...", draw = "...", line_style(...), text_style(...))] | Define a synthetic edge derivable from the data, optionally styled. draw attaches its ends to group hulls — see below. |
#[tag(to_tag = "...", name = "...", value = "...", text_style(...))] | Attach a computed attribute to matched atoms. |
#[attribute(field = "...", text_style(...))] | (See Display above; text_style styles the attribute's line.) |
An inferred_edge additionally takes draw = "<end> -> <end>", which decides
what each end of the edge attaches to. Each end is either _ — the tuple's
own atom, which is what an edge without draw does — or the name of a group
constraint, in which case the end lands on that group's hull:
#![allow(unused)] fn main() { #[group(selector = "region", name = "regions")] // One edge per `connected` pair, drawn hull to hull. #[inferred_edge(name = "connected", selector = "connected", draw = "regions -> regions")] // Each person to the hull of the region-group they manage. #[inferred_edge(name = "manages", selector = "manages", draw = "_ -> regions")] }
draw never decides which pairs get edges or which way they point — the
selector does (transpose it, e.g. ~connected, to flip one). A keyed group
constraint builds one group per key and the end's atom picks which; a unary one
builds a single group the end attaches to directly. With draw, the selector
may also be unary: the single atom feeds both ends.
Malformed draw strings are compile errors, including the redundant
"_ -> _" (that's the default — drop the key). The group name, though, is
resolved by spytial-core against the assembled spec: decorators compose
across types, so no single attribute site can see which group constraints
will end up in the spec. A name that no group defines is a layout-time
warning (since spytial-core 5.1.0) and that edge is skipped; the rest of the
diagram still renders.
A selector-based group additionally takes add_edge — the connector between
the group's key and the group. Bare form add_edge = "togroup" (or
"fromgroup"/"none"), or the styled block
add_edge(points = "togroup", line_style(...), text_style(...)) — plus a
top-level text_style(color = "...") for the group's own label.
Migrating from the 2.x flat forms: #[atom_color(selector, value)] and
#[edge_style(field, value, style, weight, ...)] still compile and are
rewritten onto the block forms — atom_color's value becomes the border
colour (that is what 2.x drew; reach for fill_style only if you want a
filled look), and edge_style's value/style/weight become
line_style's color/pattern/weight. Mixing flat keys and blocks in one
edge_style is a compile error.
Deprecation warnings: every form spytial-core has deprecated now warns at
compile time, naming the replacement and how the fields map across. That covers
whole attributes (#[atom_color], #[icon]) and single shapes of attributes
that are otherwise current — #[edge_style(value = ...)] warns while
#[edge_style(field = ..., line_style(...))] does not. Nothing stops
compiling; the deprecated forms still work exactly as before. To keep one
deliberately, put #[allow(deprecated)] on the type:
#[derive(Serialize, SpytialDecorators)]
#[allow(deprecated)]
#[atom_color(selector = "Node", value = "red")]
struct Node { /* ... */ }
The warning text comes from spytial-core's own language manifest, so it moves when upstream's does.
Breaking in spytial-core 3.0: two style rules that set the same property of the same edge/atom to different values now raise a
StyleCollisionErrorat render time (2.x silently kept the first). Set each property in exactly one matching rule.
These map onto the same builder and YAML layer that spytial-core consumes,
so anything expressible here is also expressible as a hand-written spec
passed to diagram_with_spec.
Where a rule came from
Every rule the derive emits is stamped with a source block: the attribute
exactly as you wrote it and its file:line. When the viewer reports a
conflict — two rules that cannot both hold — or warns about a rule that
matched nothing, it cites that text in place of its own description of the
rule, so the report reads #[orientation(selector = "…", directions = ["left", "below"])] at src/tree.rs:14 and you can go straight to the line. Nothing to
enable; a hand-written spec passed to diagram_with_spec needs no source,
because there the YAML is what you wrote. The builder's .source(text, location) stamps the rule you pushed last, if you assemble decorators by hand
and want the same treatment.
Attribute reference
The reference below is generated from spytial-core's own language manifest, the same file the derive's compile-time checks come from, so the keys, vocabularies, bounds, defaults, and selector arities are the engine's description of the language at the vendored version. It is the same text docs.rs shows on the derive.
Generated from spytial-core 6.4.1's language manifest, so every key, vocabulary, bound, default, and selector arity below is the engine's own description of the language (dated 2026-09-18). Keys are the Rust spellings; negated = true is spytial-core's hold: never. Where a selector lists more than one accepted shape, the first is the one the field is designed for. spytial-core's own guides: YAML reference, constraints, directives, selectors.
Constraints
#[orientation(…)]
Place the two ends of each selected tuple relative to one another. (a layout constraint; takes negated = true)
#[orientation(selector = "parent", directions = ["above"])]
selector: selector; required. Returns (source, target) pairs. The constraint applies to each pair. (a bad value makes spytial-core reject the whole spec)- binary (2 columns): Each tuple is one (first, last) pair, and the constraint applies to it.
- n-ary (3 or more columns): Accepted: the pair is the tuple's first and last atom. The columns between are ignored.
directions: one or more ofabove,below,left,right,directlyAbove,directlyBelow,directlyLeft,directlyRight; required. Where the TARGET sits relative to the SOURCE.directions: [above]on selectorparentplaces each tuple's target above its source. Thedirectly*variants additionally enforce axis alignment. The direction is target-relative-to-source. Getting this backwards is the most common spec bug — transpose the selector (~parent) rather than flipping the direction if the relation reads the other way. (a bad value makes spytial-core reject the whole spec)negated:trueasserts the relationship must not hold (spytial-core'shold: never).
Contradictory direction sets are rejected at parse time:
abovewithbelow,leftwithright, and adirectly*variant combined with anything other than its own non-direct counterpart. Values outside the listed set are NOT rejected — they parse and then match nothing.
#[align(…)]
Put the selected atoms on a shared horizontal or vertical line. (a layout constraint; takes negated = true)
#[align(selector = "siblings", direction = "horizontal")]
selector: selector; required. Returns the pairs of atoms to align with one another. (a bad value makes spytial-core reject the whole spec)- binary (2 columns): Each tuple is one (first, last) pair, and the constraint applies to it.
- n-ary (3 or more columns): Accepted: the pair is the tuple's first and last atom. The columns between are ignored.
direction: one ofhorizontal,vertical; required.horizontalgives the atoms a shared Y coordinate;verticala shared X coordinate. An unrecognized value is a parse error. (a bad value makes spytial-core reject the whole spec)negated:trueasserts the relationship must not hold (spytial-core'shold: never).
#[cyclic(…)]
Arrange the selected atoms around the perimeter of a circle, in the order the selector defines. (a layout constraint; takes negated = true)
#[cyclic(selector = "nextState", direction = "clockwise")]
selector: selector; required. Returns (a, b) pairs meaning "b follows a around the circle". (a bad value makes spytial-core reject the whole spec)- binary (2 columns): Each tuple is one (first, last) pair, and the constraint applies to it.
- n-ary (3 or more columns): Accepted: the pair is the tuple's first and last atom. The columns between are ignored.
direction: one ofclockwise,counterclockwise; defaultclockwise. Which way the cycle runs.negated:trueasserts the relationship must not hold (spytial-core'shold: never).
Two cyclic constraints on the same selector with different directions is a parse error. An unrecognized direction string is not rejected at parse time.
#[group(…)]
Draw a hull around a set of atoms. For tuples (a, b), (a, c), … the group is keyed by a and contains {b, c, …}. (a layout constraint; takes negated = true)
#[group(selector = "Team.members", name = "Team")]
selector: selector; required. First column is the group key, second the members. A unary selector builds a single unkeyed group. Agroupwith neitherselectornorfieldis silently dropped — it matches neither grouping form. That is the one case where omitting this field is not an error but a no-op.- binary (2 columns): One group per distinct first atom: the first column is the key, the last the members. The group is named
<name>[<key>], which is what a keyedinferredEdgedrawend attaches to. - n-ary (3 or more columns): As binary — the key is the tuple's first atom and the member its last, ignoring the columns between.
- unary (1 column): One single group holding every selected atom, with no key. It is named
<name>with no[key]suffix, and adrawend naming it attaches there whatever the end's atom is.
- binary (2 columns): One group per distinct first atom: the first column is the key, the last the members. The group is named
name: string; required. Display name on the group, and the handle aninferredEdgedrawendpoint refers to. Keyed captions use<name>[<key label>], adding the key ID only to distinguish duplicate captions. Required unless the constraint is negated (hold: never), where a name is generated. (a bad value makes spytial-core reject the whole spec)add_edge: one ofnone,togroup,fromgroup; or theadd_edge(…)block, which also styles it (see Style blocks); defaultnone. Draw a connector between the group key and the group:togrouppoints key → group,fromgrouppoints group → key,nonedraws nothing. The legacy booleantrueis still accepted and meanstogroup. (spytial-core drops a bad value silently)show_label:trueorfalse; defaulttrue. Whether the group caption and its background pill are drawn. The group hull remains visible.text_style:text_style(…)block. The group's own label. Onlycolorapplies today — group labels auto-fit their box, sosizeis reserved.negated:trueasserts the relationship must not hold (spytial-core'shold: never).
#[size(…)]
Set the width and height of matching nodes, in pixels. (a layout constraint)
#[size(selector = "ImportantNode", width = 150, height = 80)]
width: number greater than 0; required. Node width in pixels. Required, and must be a number greater than 0. (a bad value makes spytial-core reject the whole spec)height: number greater than 0; required. Node height in pixels. Required, and must be a number greater than 0. (a bad value makes spytial-core reject the whole spec)selector: selector. Which nodes to resize. Omit to resize every node.- unary (1 column): The nodes to resize.
hold: neveris not supported and is silently ignored.
#[hide_atom(…)]
Remove matching atoms from the diagram. (a layout constraint)
#[hide_atom(selector = "HelperNode")]
selector: selector; required. Which atoms to hide.- unary (1 column): The atoms to hide.
hold: neveris not supported and is silently ignored. Hiding an atom that a layout constraint places, or that a group contains, makes the spec unsatisfiable: the layout reports a hidden-node conflict and draws a counterfactual with the conflicting atoms outlined. Hiding a keyed group's key is fine — the key is not inside the group.
Directives
#[attribute(…)]
Render a relation as a key-value line on its source node instead of as an edge.
#[attribute(field = "age", selector = "Person")]
field: relation (field) name; required. The relation to fold into the source node. Its edges are removed from the graph.selector: selector. Apply only to these source atoms. Omit to apply to every source atom.- unary (1 column): The set of source atoms the directive is narrowed to.
filter: selector. Apply only to the (source, target, …) tuples this selector returns. Use it to scope a directive to part of a relation, e.g. only the tuples whose value is True.- n-ary (2 or more columns): A tuple matches on its first and last atom, so a longer tuple filters the same relation ends a pair would. A unary result matches nothing — the directive is then scoped to no tuples at all.
text_style:text_style(…)block. This attribute line's own styling.
Multiple targets for the same source become a list. Unlike
tag, this removes the edge.
#[flag(…)]
Toggle a whole-diagram rendering behaviour.
#[flag(name = "hideDisconnectedBuiltIns")]
name: one ofhideDisconnected,hideDisconnectedBuiltIns; required.hideDisconnectedhides every atom with no edges;hideDisconnectedBuiltInshides only the disconnected built-in atoms (Int, String, …). Any other value is silently ignored.
Written as a bare scalar:
- flag: hideDisconnected, not a mapping.
#[atom_color(…)]
Set the color of matching atoms. (deprecated, rewrites to #[atom_style])
#[atom_color(selector = "Person", value = "#ff5733")]
value: color; required. Any CSS color. Applies to the node's outline, not its fill.selector: selector; required. Which atoms to recolor. Omitting it drops the directive — it never means "every atom".- unary (1 column): The atoms to recolor.
#[atom_style(…)]
Style matching atoms. An atom is a composite of an interior fill, an outline, an icon, and its label, so each is its own block.
#[atom_style(selector = "Person", border_style(color = "#0369a1"))]
selector: selector. Which atoms to style. Omit to style every atom.- unary (1 column): The atoms this rule styles.
fill_style:fill_style(…)block. The node's interior fill.border_style:border_style(…)block. The node's outline.icon_style:icon_style(…)block. An icon drawn on the node.text_style:text_style(…)block. The node's own label.show_label:trueorfalse; defaulttrue. Whether the atom's label is drawn. Independent of the icon:iconStyle.placementcontrols the icon's geometry, this controls the label.
Rules compose: a supertype selector already returns subtype atoms, so a
Noderule and aRedNoderule both apply to a RedNode atom and their set properties gap-fill each other. Two rules setting the SAME property to DIFFERENT values is a hard error at layout time — styles never silently override.
#[icon(…)]
Assign an icon to matching atoms. (deprecated, rewrites to #[atom_style])
#[icon(selector = "Person", path = "person", show_labels = true)]
selector: selector; required. Which atoms get the icon. Omitting it drops the directive entirely — it never means "every atom".- unary (1 column): The atoms that get the icon.
path: icon name, icon-pack reference, URL, or path; required. Bundled name, icon-pack reference, URL, or path. Omitting it drops the directive.show_labels:trueorfalse; defaultfalse. Whether the atom's label is drawn alongside the icon.
#[edge_style(…)]
Style the edges of a relation: the drawn line, the label, and visibility.
#[edge_style(field = "parent", line_style(color = "blue", pattern = "dashed"))]
field: relation (field) name; required. The relation whose edges this styles.selector: selector. Apply only to edges whose source atom is in this set. Omit to apply to every source atom.- unary (1 column): The set of source atoms the directive is narrowed to.
filter: selector. Apply only to the (source, target, …) tuples this selector returns. Use it to scope a directive to part of a relation, e.g. only the tuples whose value is True.- n-ary (2 or more columns): A tuple matches on its first and last atom, so a longer tuple filters the same relation ends a pair would. A unary result matches nothing — the directive is then scoped to no tuples at all.
line_style:line_style(…)block. The drawn line.text_style:text_style(…)block. The edge label.show_label:trueorfalse; defaulttrue. Whether the edge label is drawn.hidden:trueorfalse; defaultfalse. Hide the edge entirely while keeping the relation in the data.value: color; the deprecatededgeColorform, which rewrites onto the blocks above; mixing the two shapes is a compile error. Line color.style: one ofsolid,dashed,dotted; the deprecatededgeColorform, which rewrites onto the blocks above; mixing the two shapes is a compile error. Dash pattern. Trimmed and lowercased before matching, unlikelineStyle.pattern. (spytial-core drops a bad value silently)weight: number greater than 0; the deprecatededgeColorform, which rewrites onto the blocks above; mixing the two shapes is a compile error. Line thickness in pixels. (spytial-core drops a bad value silently)
Like atomStyle, overlapping rules compose and a genuine disagreement is an error rather than a silent override.
#[hide_field(…)]
Hide a relation's edges.
#[hide_field(field = "internal")]
field: relation (field) name; required. The relation to hide.selector: selector. Apply only to edges whose source atom is in this set. Omit to apply to every source atom.- unary (1 column): The set of source atoms the directive is narrowed to.
filter: selector. Hide only the tuples this selector returns.- n-ary (2 or more columns): A tuple matches on its first and last atom, so a longer tuple filters the same relation ends a pair would. A unary result matches nothing — the directive is then scoped to no tuples at all.
#[inferred_edge(…)]
Draw an edge that is not in the data, computed from a selector.
#[inferred_edge(name = "reachable", selector = "^parent", line_style(color = "gray", pattern = "dotted"))]
name: string; required. The label drawn on the edge.selector: selector; required. Returns the (source, target) pairs to connect. May be unary whendrawis given — the single atom then feeds both ends.- binary (2 columns): One edge per tuple, from its first atom to its last.
- n-ary (3 or more columns): One edge from the tuple's first atom to its last. Unlike the constraints, the columns between are not discarded: their labels are appended to the edge label as
<name>[m1,m2]. - unary (1 column); needs
draw: One edge per atom, the atom feeding both ends — the shapedraw: _ -> <group>needs, connecting each atom to its own group. Withoutdrawa unary selector draws nothing at all: the engine reads this field as tuples, and single-atom tuples are dropped before any edge is made.
draw: string. Reinterpret what each end attaches to, as<end> -> <end>. Each end is_(the atom itself, the default) or the name of agroupconstraint, in which case the end attaches to that group's hull. The left end applies to the first atom of each tuple, the right end to the last. Parsed strictly: a value that is not a string, or that does not contain exactly one->, is a parse error. A group name that nogroupconstraint defines is not — it raises a warning (codeunresolved-reference, specTypeinferredEdge) and the edge is skipped at layout time, so a fragment that names a group defined in another fragment still parses.drawnever reorders — transpose the selector (~connected) to flip an edge. (a bad value makes spytial-core reject the whole spec)line_style:line_style(…)block. The drawn line.text_style:text_style(…)block. The edge label.
Using any of the legacy inline
color/style/weight/highlightkeys raises a deprecation warning with specTypeinferredEdge. The block form wins when both are given. If an end's atom keys no group of that name in a given instance, that one edge is skipped with a console warning — data-dependent, not a spec error.
#[tag(…)]
Add a computed key-value line to matching nodes, without touching the graph structure.
#[tag(to_tag = "Person", name = "age", value = "age")]
to_tag: selector; required. Which atoms receive the tag.- unary (1 column): The atoms that receive the tag.
name: string; required. The literal label shown before the value; this field is not a selector.value: selector; required. Evaluated once; each tuple is attached to the tagged atom in its first column. A binary result shows asname: value; a longer tuple shows one line per tuple, asname[k1,k2]: value. A unary result tags each selected atom with its own label.- n-ary (2 or more columns): One line per tuple whose first atom is the tagged atom: the last column is the value and any columns between become one comma-separated key tuple, as
name[k1,k2]: value. A binary result is the plainname: valuecase. - unary (1 column): A membership tag: an atom in the result is tagged with its own label, as
name: <label>. The value carries no more than "this atom is in the set".
- n-ary (2 or more columns): One line per tuple whose first atom is the tagged atom: the last column is the value and any columns between become one comma-separated key tuple, as
text_style:text_style(…)block. This tag line's own styling.
Unlike
attribute, tags do not remove edges.
Style blocks
Blocks are written as nested groups that mirror the YAML: line_style(color = "gray", pattern = "dotted"). Every leaf is optional.
line_style(…)
Styling for an edge's drawn line. Reused by edgeStyle, inferredEdge, and a group's addEdge connector.
color: color. Any CSS color.pattern: one ofsolid,dashed,dotted. Dash pattern of the line. (spytial-core drops a bad value silently)weight: number greater than 0. Line thickness in pixels. Must be > 0; a non-positive or non-numeric weight is dropped. (spytial-core drops a bad value silently)highlight: color. A wider, translucent underlay drawn beneath the line. Orthogonal to color and pattern.
text_style(…)
Styling for a label. Reused by every form that renders text — an atom's own label, attribute and tag lines, edge labels, and a group label.
size: one ofsmall,normal,large; defaultnormal. Font-size tier relative to the node label:largerenders bigger than the label,normalsmaller,smallsmaller still. (spytial-core drops a bad value silently)color: color. Any CSS color. Unset inherits the node label color, so dark mode still adapts.
border_style(…)
An atom's outline.
color: color. Any CSS color.width: number greater than 0. Outline thickness in pixels. Must be > 0; anything else is dropped. (spytial-core drops a bad value silently)
fill_style(…)
An atom's interior fill. Opt-in: the default look is unfilled, with only the border and label marking the node.
color: color. Any CSS color.
icon_style(…)
An icon drawn on an atom. Every leaf is optional, including path, so a supertype rule can supply the icon and a subtype rule tune only its opacity.
path: icon name, icon-pack reference, URL, or path. A bundled icon name (person), an icon-pack reference (bi:person-fill), a URL, or a path. Resolved to a concrete URL or data URI at parse time. An iconStyle with no path draws nothing.placement: one offull,badge; defaultfull.full— the icon occupies the box, which stays transparent unless a fillStyle.color is given.badge— a small marker in the top-right corner, secondary to the label. (spytial-core drops a bad value silently)opacity: number between 0 and 1; default1. Alpha in [0,1]. Out-of-range values are dropped rather than clamped. Fade afullicon to use it as a watermark behind the label. (spytial-core drops a bad value silently)
add_edge(…) (on #[group])
Block form also styles the connector, which is an edge and so takes the shared edge blocks: { points: <direction>, lineStyle: {…}, textStyle: {…} }.
points: one ofnone,togroup,fromgroup; defaultnone. The connector direction — the same value the bare string form carries.line_style:line_style(…)block. The connector's line.text_style:text_style(…)block. The connector's label.
Deprecated forms
Each still compiles and still works, with a compile-time deprecation warning naming the replacement; #[allow(deprecated)] on the type keeps one deliberately.
#[icon(…)]→#[atom_style]. The singleshowLabelsboolean drove label visibility and icon geometry at once. atomStyle splits those into two independent knobs, which is what makes a faded watermark, or a hidden label with no icon, expressible. Mapping:path→icon_style.path;show_labels: false→show_label: false + icon_style.placement: full;show_labels: true→show_label: true + icon_style.placement: badge.#[atom_color(…)]→#[atom_style]. atomStyle expresses the same recolor and also reaches the fill, the icon, and the label. The rewrite is border-preserving, so an existing diagram is unchanged. Mapping:value→border_style.color.#[edge_style]written withvalue/style/weight→#[edge_style]. edgeStyle groups the same knobs into the shared lineStyle/textStyle blocks that every other form uses. Mapping:value→line_style.color;style→line_style.pattern;weight→line_style.weight;highlight→line_style.highlight.
Negated constraints
orientation, align, cyclic, and group accept negated = true,
which flips the constraint from "this must hold" to "this must not
hold." It's most useful alongside positive constraints — the positive ones
say what shape the layout should take, the negated ones rule out a
degenerate arrangement the solver might otherwise pick:
#![allow(unused)] fn main() { #[align(selector = "{x, y : Node | x->y in default_edge}", direction = "vertical")] #[align(selector = "{x, y : Node | x->y in exception_edge}", direction = "vertical", negated = true)] }
"Default edges align vertically; exception edges must not." A diagram with only negated constraints is under-specified, and the solver falls back to defaults.