Portable SVG symbols

Build and share static SVG symbols with safe Ganter bindings and typed connection terminals.

View as Markdown

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 fileLogic > Assets > Symbols > Import symbol accepts a plain .svg or a complete .ganter-symbol.json package. 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 appNew 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_validate tool dry-runs the import and symbol_import persists 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 or data:/file:/http(s) URL. Only internal url(#id) and href="#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.
  • role is input, output or bidirectional; direction is north, east, south or west.
  • Exactly one of type (a global connection type) or typeConfiguration (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 viewBox that 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, footprint or any of the three arrays; a null entry inside an array.
  • A fit outside 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, spin or scale slot whose target has no finite data-ganter-pivot; a spinEnabled slot without a spin slot on the same element.
  • Two slots on one element's same channel. Every kind is its own channel except that fill and fillColor share the fill channel and stroke and strokeColor share 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.