Portable SVG symbols
Build and share static SVG symbols with safe Ganter bindings and typed connection terminals.
Ganter symbols keep artwork and live data separate. The artwork is ordinary, static SVG. Ganter Lab owns every value mapping and animation, so an imported file cannot run scripts, event handlers, embedded animation code or network requests. This page is the complete reference for the authoring model: what a symbol declares, what the import accepts and refuses, and how symbols travel between stations.
A symbol declares three things on top of its artwork:
- Slots — named inputs (fill, stroke, motion, text, visibility) that a placed dashboard instance later feeds from a Logic address or a fixed value.
- Terminals — typed connection points on the perimeter, so the Dashboard can route pipes, ducts, wires and shafts between symbols.
- Suggestions — optional, explicitly posed compositions with other symbols.
System symbols and My symbols
The catalog has two origins, filterable everywhere symbols are listed:
| Origin | Reference | What it is |
|---|---|---|
| System | system:<stable-key> |
The built-in libraries that ship with the app. Inspectable and exportable, never editable in place. |
| Mine | user:<guid> |
Symbols you created, imported or duplicated. Editable and deletable. |
Every system symbol offers Duplicate to My symbols: the copy is a frozen, independently owned user asset (artwork, view box, fit, footprint, slots, type configurations, terminals and suggestions copied together), so it keeps behaving as selected even when a later app update changes the original. Deleting a user symbol asks for confirmation; dashboards that still reference it show a placeholder instead of breaking.
Ways to create a symbol
- Import a file — Logic > Assets > Symbols > Import symbol accepts a plain
.svgor a complete.ganter-symbol.jsonpackage. Every import creates a new asset with its own identity; it never overwrites an existing symbol, and a name collision is resolved by suffixing (" 2", " 3", …) rather than failing. The picker refuses files over 1 MB before any bytes are read. After a plain-SVG import the app reports how many unsafe items it removed, if any. - Author in the app — New user symbol opens an editor for the name, the SVG markup, the dashboard layout (fit and initial footprint) and the slots; once the symbol exists, valid changes save automatically. Terminal geometry is intentionally not editable in the app: typed terminals can only be supplied by a package, and package-authored terminals are preserved read-only through later edits.
- Ask an AI — two routes share the exact same contract. Over the embedded
MCP endpoint, the
symbol_validatetool dry-runs the import andsymbol_importpersists a new asset; both carry the full contract in their descriptions. From an external chat with no app access, Copy chat instruction in the Symbols panel copies a self-contained prompt; save the returned SVG or package as a file and import it here.
See Logic for the surrounding assets workspace.
Prepare a plain SVG
Give every part you want to bind a stable SVG id. After import, those ids appear in the slot
target pickers. Ids and slot names share one grammar: they start with a letter or _, continue
with letters, digits, _, ., : or -, and are at most 128 characters.
Optional data-ganter-* attributes are the only Ganter-specific markup a symbol may carry:
| Attribute | On | What it declares |
|---|---|---|
data-ganter-pivot="x y" |
any bindable element | The exact pivot point (view-box coordinates) for rotate, scale and spin slots. Required on the target element of those three kinds. |
data-ganter-spin-ratio |
a child of a spinning element | The relative speed of a visible child rotor. Finite, nonzero, magnitude at most 100; negative spins the other way. Removed unless the element also declares its own pivot. |
data-ganter-role |
any element | A stable semantic role for authoring tools. Informational only. |
data-ganter-fit-width |
<text> / <tspan> |
Opt-in bounded readout for a Text slot: when the bound text measures wider than this many view-box units, the font shrinks to fit. |
data-ganter-fit-min-font-size |
<text> / <tspan> |
The smallest font size the fit may shrink to. Below it, the element shows the fallback while the full value stays in the accessible name and tooltip. |
data-ganter-fit-fallback |
<text> / <tspan> |
The text shown when even the minimum font size cannot fit the value. |
Connection terminals do not live in SVG and do not target SVG element ids. Attributes named
data-ganter-port or data-ganter-port-* are not part of the format: a plain SVG import removes
them; a strict package containing them is rejected.
A plain SVG import always yields a valid visual-only symbol: zero terminals, zero slots. The inspector reports that no connectable terminal exists because role, type, external direction and internal path were not declared. Ganter does not infer those fields from the file name, category or artwork, and does not open an internal terminal or path editor.
What the sanitizer accepts
The same SVG policy applies to every route (file import, in-app editor, MCP, package). It is deliberately smaller than SVG:
- Kept — inert geometry and structure:
svg,g,defs,symbol,use,path,rect,circle,ellipse,line,polyline,polygon,text,tspan,title,desc,clipPath,mask,linearGradient,radialGradient,stop,pattern,marker, with presentation attributes (fill, stroke, opacity, transform, styling) and ARIA labels. - Removed or refused —
<script>,<style>,<foreignObject>, event handlers, embedded animation, and any external ordata:/file:/http(s)URL. Only internalurl(#id)andhref="#id"references survive. - Limits — the markup may be at most 512 KB and 4096 elements.
Safe paints are hex colors, the CSS named colors, rgb()/rgba()/hsl()/hsla(), and the
app's theme tokens var(--color-<name>) (for example var(--color-success),
var(--color-symbol-off)). Theme tokens are how one artwork adapts to light and dark: ship one
drawing that references tokens, never two theme variants.
How violations are handled depends on the route: a plain SVG is repaired (unsafe items are removed and each removal is reported as a warning); a package is strict and is rejected with the first violations named instead of being silently repaired.
Portable package v2
Exporting a symbol creates a .ganter-symbol.json file. Every field of the envelope is required;
the three contract arrays may be empty but must be present:
| Field | Requirement |
|---|---|
$schema |
Exactly https://ganterlab.com/schemas/ganter-symbol-v2.schema.json. |
format |
Exactly ganter-symbol. |
version |
Exactly 2. Version 1 packages are rejected instead of passed through a legacy parser. |
name |
The display name. An import may override it, and a collision is auto-suffixed. |
svg |
Sanitized static SVG whose root element carries a viewBox equal to the viewBox field. |
viewBox |
minX minY width height: four finite numbers with positive width and height. |
fit |
contain, stretch, stretch-x or stretch-y. |
footprint |
{ "width": 1..24, "height": 1..12 } — the suggested dashboard cells. |
slots |
Array of slot declarations (below). |
typeConfigurations |
Array of shared terminal type choices (below). |
terminals |
Array of up to 16 complete terminals (below). |
suggestions |
Array of ordered authored compositions (below). |
The public Ganter symbol v2 JSON Schema validates the
transport envelope. The importer then performs the semantic checks JSON Schema cannot express:
SVG target existence, internal-path geometry, suggestion compatibility. A package is strict in
every dimension: unsafe markup, unknown fields, null array entries and the former ports[],
anchors[], profile and connectionStyle contracts are rejected instead of repaired.
The package never contains a station address, a Dashboard binding, a per-instance type choice, a database GUID or a user identity. A slot that carries a Logic address cannot even be exported: the address belongs to the placed instance, not the reusable symbol.
Size, view box and fit
The view box is the symbol's natural coordinate space. Terminals and internal paths are
declared in it, and it must match the SVG root's own viewBox exactly; a package whose two view
boxes disagree is rejected. Non-square view boxes are fully supported, so a long conveyor does not
have to live in a square.
The fit mode says how the artwork uses the dashboard rectangle the operator draws:
| Fit | Behavior |
|---|---|
contain |
Keeps the aspect ratio; the artwork letterboxes inside the cell rectangle. The default. |
stretch |
Fills the rectangle in both axes, distorting if needed. |
stretch-x |
Stretches horizontally, keeps the natural vertical proportion. |
stretch-y |
Stretches vertically, keeps the natural horizontal proportion. |
The footprint is the initial size in dashboard grid cells when the symbol is first placed: width 1 through 24, height 1 through 12 (the editor defaults to 4 × 4). It is a suggestion, not a constraint; the operator resizes freely afterwards.
Two separate gates bound an import's size: the file picker refuses anything over 1 MB before reading it, and the sanitizer refuses markup over 512 KB or 4096 elements.
Slots — the binding surface
A slot declares one input: which element it drives, what it does to it, and how input values map to output. The placed dashboard instance later chooses each slot's source (a Logic address or a fixed value); the symbol itself never stores a source.
Each slot carries:
| Field | Meaning |
|---|---|
name |
The parameter name shown to the dashboard author. Identifier grammar above; unique across the symbol's slots. |
elementId |
The id of an existing SVG element. One element accepts at most one slot per channel, so two slots cannot fight over the same element's fill. |
kind |
One of the thirteen kinds below. |
inMin, inMax |
The input range: the two values mapped to outMin/outMax. Finite numbers; used by the continuous kinds and as the threshold midpoint by fill/stroke. |
outMin, outMax |
The output range in the kind's unit (degrees, view-box units, 0..1, degrees per second). Finite numbers. |
outMinSecondary, outMaxSecondary |
Optional second output axis, translate and scale only: the primary output is X and the secondary is Y. Supply both or neither. |
onColor, offColor |
Safe paints for fill/stroke (the two threshold states). For fillColor/strokeColor, onColor is the inspector's preview color. |
The thirteen slot kinds:
| Kind | What it drives | Notes |
|---|---|---|
rotate |
Rotates the element; input maps linearly to degrees. | Target must declare data-ganter-pivot. |
opacity |
The element's opacity; input maps to 0..1. | |
fill |
Paints the fill offColor below the midpoint of the input range and onColor at or above it. A Bool input switches directly. |
Targets fillable shapes (not <line>). |
visible |
Shows or hides the element by the value's truthiness. | |
text |
Replaces the element's text content with the formatted value. | Targets <text>/<tspan> only; combine with the data-ganter-fit-* attributes for a bounded readout. |
translate |
Moves the element in view-box units; primary output is X, optional secondary is Y. | |
scale |
Scales the element; primary output is X, secondary is Y (omitted = uniform). | Target must declare data-ganter-pivot. |
stroke |
Paints the stroke by the same threshold rule as fill. |
|
spin |
Continuous rotation over time; input maps to angular velocity in degrees per second. Children with data-ganter-spin-ratio co-rotate at their declared ratio. |
Target must declare data-ganter-pivot. Motion is host-scheduled and independent of operating-system animation preferences. |
spinEnabled |
Pauses or resumes the spin slot on the same element without hiding it. |
Requires a spin slot targeting the same element. |
fillColor |
Sets the fill directly from the bound color value (a Color asset, Color variable or safe CSS color). | |
strokeColor |
Sets the stroke directly from the bound color value. | |
strokeWidth |
Sets the stroke width directly, in SVG user units from 0.5 through 32; no unit suffix or CSS accepted. | Invalid, unresolved or bad-quality input restores the authored stroke-width; so do Reset, remount and disposal. |
These slots style artwork; they never style Dashboard connection routes, whose color and width are global per connection type.
Binding ownership
The symbol declares parameters and permitted type choices, not live sources or an instance's selection. After placing a Symbol component, the operator maps each slot to a Logic address or a fixed value and picks each configurable connection type on that instance. Those choices stay inside the station and are never exported with the reusable symbol. Unmapped slots stay neutral: the element keeps its authored appearance.
Global positions and connection types
Every terminal references one position from the fixed sixteen-anchor perimeter catalog. A package
stores only anchorId; it never repeats or overrides coordinates. The positions, as normalized
fractions of the view box:
- top:
top-left(0, 0),top-25(0.25, 0),top-50(0.5, 0),top-75(0.75, 0),top-right(1, 0); - right:
right-25(1, 0.25),right-50(1, 0.5),right-75(1, 0.75); - bottom, clockwise:
bottom-right(1, 1),bottom-75(0.75, 1),bottom-50(0.5, 1),bottom-25(0.25, 1),bottom-left(0, 1); - left, continuing clockwise:
left-75(0, 0.75),left-50(0, 0.5),left-25(0, 0.25).
These are perimeter positions, not a 5×5 grid. A symbol cannot add another position or store its own anchor coordinates.
The eight global connection types are liquid, gas, air-duct, electrical, signal,
network, material and mechanical-shaft. Use Logic > Assets > Connection types to
configure the color and visual stroke width associated with each type. Symbols and connections
retain the stable type id, so a global change restyles every route that uses it. A package never
copies the color or width and has no small, standard or large route variants.
Two endpoints form a defined connection when they resolve to the same type. The terminal role helps authoring and review but does not prohibit a network arrangement by itself.
Shared type choices
A symbol that can work with more than one medium declares a shared type configuration rather than duplicating its artwork:
{
"id": "process-type",
"name": "Process type",
"allowedTypes": ["liquid", "gas"],
"defaultType": "liquid"
}
The id is a stable key: lowercase letters and digits with single dashes, starting and ending
alphanumeric (uppercase is refused). allowedTypes is nonempty and contains unique global type
ids; defaultType must belong to it; name is required. Every terminal that references
process-type follows one choice made on the placed dashboard instance. A definition may carry
several independent configurations, such as process and vent media, while other terminals stay
fixed.
Complete terminals
Each connectable terminal declares one global position, a role, exactly one fixed type or one shared configuration, an outward direction in the original orientation, and an internal arrival path:
{
"anchorId": "left-50",
"role": "input",
"typeConfiguration": "process-type",
"direction": "west",
"internalPath": [
{ "x": 18, "y": 50 },
{ "x": 36, "y": 50 },
{ "x": 48, "y": 62 }
]
}
The rules, all enforced at import:
- At most 16 terminals, and each anchor id used at most once.
roleisinput,outputorbidirectional;directionisnorth,east,southorwest.- Exactly one of
type(a global connection type) ortypeConfiguration(a declared configuration id) — never both, never neither. - The global anchor converted to the SVG's natural view box is the implicit start of
internalPath; do not repeat it as the first point. Coordinates are absolute view-box values, not normalized fractions, and the path has at least one point. - Every segment stays inside the view box, has nonzero length, and is horizontal, vertical or exactly 45°; consecutive segments turn by at most 90°.
- Terminals require the SVG to carry a view box at all.
The Dashboard draws the internal path below the equipment artwork, transforms it with resize, quarter-turn rotation and flips, and keeps the global connection type's visual width instead of scaling it with the symbol, so one continuous stroke runs from inside one body to inside the other.
A terminal is all-or-nothing. A strict package with a partial terminal is invalid; a visual-only
package uses an empty terminals array instead. Nothing is ever inferred to fill a gap.
Ordered suggestions
A suggestion records an intentional composition, including the exact target pose:
{
"sourceAnchorId": "right-75",
"targetSymbol": "system:capping-station",
"targetAnchorId": "left-75",
"targetRotationDegrees": 0,
"targetFlipHorizontal": false,
"targetFlipVertical": false,
"connectionType": "material"
}
Validation: sourceAnchorId must be one of this symbol's own terminals; targetSymbol must be a
source-qualified reference, and in a portable package it may only be system:<stable-key> (a user
GUID is meaningful only inside its owning station) naming a symbol that exists in the system
catalog with a terminal at targetAnchorId; rotation is 0, 90, 180 or 270 and both flip
flags are required; each suggestion must be unique. connectionType can be omitted only when the
equal fixed types at both endpoints resolve the scenario unambiguously; it is required when either
endpoint is configurable, and it must then be allowed at both ends.
Suggestions are directional and array order sets priority, but they are never an allow-list for
ordinary Dashboard connections. With an empty suggestions array the inspector reports that no
suggested composition exists instead of inventing a catalog partner.
What the import refuses
A quick checklist of the refusals, so a failed import can be read instead of guessed at. For a
plain SVG, only the first group applies, and within it just the size caps, malformed XML and a
missing <svg> root refuse the import — disallowed markup is repaired with a warning instead. For
a package, everything below is a hard rejection.
Artwork
- A file over 1 MB, markup over 512 KB, or more than 4096 elements.
- Malformed XML, or a root that is not a sanitized
<svg>. - In a package: any markup the allow-list would have had to remove (scripts, event handlers, external references, disallowed elements or attributes).
- A
viewBoxthat is not four finite numbers with positive width and height, or that differs from the SVG root's; a package without a root view box at all.
Envelope
- A schema, format or version other than the v2 contract; unknown fields anywhere; a missing
name,fit,footprintor any of the three arrays; a null entry inside an array. - A
fitoutside the four modes; a footprint outside 1–24 × 1–12.
Slots
- A name or element id outside the identifier grammar; a duplicate slot name; a target id the SVG
does not contain; a kind the target element cannot take (text on a non-text element, fill on a
<line>). - A
rotate,spinorscaleslot whose target has no finitedata-ganter-pivot; aspinEnabledslot without aspinslot on the same element. - Two slots on one element's same channel. Every kind is its own channel except that
fillandfillColorshare the fill channel andstrokeandstrokeColorshare the stroke channel, so an element cannot take two rotations any more than two fills. - Non-finite range numbers; a secondary output with only one endpoint, or on a kind other than
translate/scale; an unsafe paint; a slot that carries a Logic address.
Connections
- More than 16 terminals; a repeated or unknown anchor id; a terminal with both or neither of
type/typeConfiguration; an unknown type or configuration id; an invalid internal path (outside the view box, zero-length or off-angle segments, a turn over 90°). - A type configuration with an invalid id, a missing name, duplicate or unknown allowed types, or a default outside its own list.
- A suggestion whose source terminal does not exist, whose target is not a system symbol with the named terminal, whose pose is not a quarter turn, or whose connection type is not accepted at both ends.
symbol_validate on the MCP endpoint runs exactly this import as a dry run and reports
every error, warning and note without persisting anything.
Packaging and sharing
Export package (available for system and user symbols alike) downloads
<name>.ganter-symbol.json with the sanitized artwork and the full contract: $schema, format,
version, name, svg, viewBox, fit, footprint, slots[], typeConfigurations[],
terminals[] and suggestions[], the three connection arrays present even when empty. What never
travels: station addresses, per-instance type choices, database identity, user identity, and the
station's connection-type colors and widths.
On the receiving station, the same file goes through Import symbol (or symbol_import over
MCP) and lands as a new user symbol, terminals and all. Because type identity is a stable id and
appearance is global, an imported symbol's routes immediately follow the receiving station's own
connection-type styling.