Schema guidelines
A schema decides how a construct reads before any document is written in it. These are the conventions the first-party libraries follow — the precedent a new element, environment or mark should match so the language stays one language. How a schema is declared belongs to each SDK; this page is about the choices made inside one.
#Names
An attribute name is a lowercase hyphenated identifier: x-domain, show-grid, text-size. Source is forgiving — attribute keys match case-insensitively, and runs of whitespace fold to a single hyphen, so an author may write x domain — but the declared name and every alias must be strict identifiers, with no spaces.
British spellings are canonical — colour, centre, grey — with the American form supplied as an alias, never the reverse.
A unit never appears in a name. The type carries it: a duration attribute is duration, typed so that 500ms and 3s both parse — not duration-ms.
Suffixes have settled meanings: -format names what an axis holds (x-format: radians), -scale how it places a value (x-scale: log), -style chooses among fixed looks (grid-style: dots), and paired axis attributes share the x-/y- prefixes. Enum values follow the same casing as names: lowercase, hyphenated (equal-earth, lines-minor).
#Booleans
A flag is named for what it enables, so asserting it reads as plain statement: editable, interactive, runnable, show-grid. In source a boolean is a bare word and its negation is !show-grid — each spelling carries one idea.
A negated name breaks that arithmetic: hidden: false stacks a negation on a negative and has to be decoded rather than read. Name the affirmative state and let the default carry the common case — chrome that is visible by default is show-ticks, show-labels, switched off with !, never a family of hide- flags switched on. The universal mark flag hidden predates this rule and keeps its name; it is not precedent for new schemas.
Choose the default so a typical document never mentions the flag: show-ticks defaults true because most graphs want ticks; runnable defaults false because most code environments are read, not run.
#Types
Every attribute declares a type — there is no implicit string. An untyped attribute would silently receive the most permissive type, and a value nobody considered would be indistinguishable from a deliberately textual one; library validation is fail-fast instead.
The base set is sealed. A type is a base plus language-defined refinements; a schema cannot invent a base, and an unknown refinement is a load error. union is the only combinator: at least two branches, provably disjoint, never nested.
Optionality is required and nothing else. A type never means “this, or nothing” — an optional attribute is one that leaves required unset, not one with a looser type.
Prefer a named type over a loose string. series, duration, colour and their peers each carry a value grammar and the diagnostics that come with it; an attribute that accepts “numbers or a dataset column” typed as plain string re-invents series without either.
#The implicit attribute and order
A construct names its implicit attribute with implicit — the attribute the name: value head form assigns. A construct that omits the tag has no head form: line:0, 1, 2 is a parse error against it, and the named spelling is the only one. Tag the attribute the construct is most naturally of.
Order carries no meaning, but the convention still reads: required payload first, then styling, with bookkeeping — label, hidden, lock — last. Abridged from the line mark:
line: {
attributes: {
x: { type: 'series', required: true, animatable: 'number-list' },
y: { type: 'series', required: true, animatable: 'number-list' },
colour: { type: 'colour', aliases: ['color'], default: 'pink', animatable: 'colour' },
width: { type: 'parametised-number', default: '2', animatable: 'number' },
endpoints: { type: 'bool', aliases: ['dots'], default: true },
label: { type: { base: 'inline', content: 'all' } },
hidden: { type: 'bool', default: false, animatable: 'discrete' },
lock: { type: 'bool', aliases: ['locked'], default: false },
},
implicit: 'x',
},The tag may name a canonical attribute or an alias, and it is stored canonical. It may not name a derived attribute, and it may not name one a dynamic case withdraws. The implicit attribute’s name is reserved inside the construct’s own attribute group.
#Defaults
A default is written the way an author would write the value. Booleans and numbers are literal (default: false, default: 20); every custom-parsed type takes its default as a source string — '500ms', '[-10, 10]', 'teal' — parsed through the type exactly as an authored value would be.
An absent default is a statement, not an omission. It means the attribute is required, or that the value derives at runtime — a graph domain comes from the data it shows; a highlight takes the colour of what it highlights. A filled-in placeholder would foreclose that behaviour, so omit deliberately.
#Aliases
The canonical name is the full, unabbreviated, British-English spelling. Aliases hold the forms an author will plausibly type instead: the American spelling (colour aliases color), abbreviations (show-background aliases background, show-bg, bg), and near-synonyms (endpoints aliases dots).
An alias is a spelling, not a second identity: examples and documentation use the canonical name only.
#Reserved and derived names
No construct declares an attribute named id. Identity is head syntax — point#p1, read back through #p1 references — and the name is reserved for it.
Derived attributes — the read-only values a construct publishes, like #fit.slope — live in the separate derived map, never as flagged entries among the attributes. Each declares a type like any attribute; a derived name colliding with a declared one is a schema error, and writing one from source is a compile error.
#The animatable surface
An attribute joins the animatable surface by declaring an interpolation kind — number, number-list, colour, point, interval, edges, or discrete for values that flip rather than blend. !cue.to refuses any attribute without one.
The surface grows deliberately, attribute by attribute. Declare a kind when animating the attribute means something on the surface, not because the type happens to be interpolable.