data
Declares a named dataset — written once and reached from anywhere in the document. It has one of two SHAPES, and the shape decides what can be done with it. TABULAR is columns of strings: inline, the body is pipe-delimited rows under a header row that names the columns; attached, a .csv supplies both. GEO is boundary geometry — an attached .geojson, which has no columns at all and is bound whole, as a canvas.geo basemap. Every tabular cell is a string until an attribute type parses it — the graph's series attributes, for one, read month names, weekday names and ISO dates as axis positions. Reach a column with {{#name.column}} to splice its cells as comma-separated text at compile time, or bind #name.column where an attribute takes a reference.
#Declaring a dataset
declares a named dataset — written once and reached from anywhere in the document. It has one of two shapes, and the shape decides what can be done with it. Tabular is columns of strings. Inline, the body is pipe-delimited rows under a header row that names the columns:
@data#islands{
island | area | species
Baltra | 25.09 | 58
Bartolomé | 1.24 | 31
Santa Cruz | 903.82 | 444
}Attached, a .csv supplies both — #survey:galapagos-plants-1973.csv — with src as the implicit attribute and column names taken from the file's header row. Nothing downstream distinguishes the two forms. An id is required in either, because a dataset is reached as #id.column.
#Boundaries as a dataset
The second shape is geo: boundary geometry, attached as a .geojson. It is what a environment draws when the boundaries it needs — electorates, catchments, council wards — are not one of the committed packs. The extension is the format declaration: a .csv is tabular, a .geojson is geo, and .json is not admitted, so the compiler never guesses at a file's shape or reads it to find out.
@data#wards:sydney-wards.geojson
@data#turnout:turnout.csv
canvas.geo{
choropleth(in: #wards; regions: #turnout.ward; values: #turnout.pct)
}(
projection: conic
)A geo dataset is opaque. It has no columns, so it is bound whole — in: #wards — and #wards.name is a compile error that names the fix rather than a lookup that quietly finds nothing. Which feature property names each region is said on the environment consuming it, with names:, because that is a question about the join and not about the file. Only a tabular dataset can be written inline: a { } body is always columns.
#Reaching columns
A tabular dataset's columns are reached by ordinary member access, fully qualified. There is no bare-column shorthand: attribute values are unquoted, so label: island must remain the literal string island. Two mechanisms consume a column, distinguished by the braces:
{{#survey.area}}— an expression: the cells, joined with commas, resolved while the document compiles. Works in prose, code bodies and attribute values alike.#islands/#islands.column— a binding: a reference carried through to the renderer, for attributes typed to accept one.
#Substitution against binding
A column reading resolves at compile time because a dataset is immutable by construction — nothing replaces it and no cell depends on a varying value, so there is nothing for the expression to wait on. A value that does vary at render time — a derived attribute, an animated attribute, a reader-set parameter — makes the expression bound instead of refusing it: {{#fit.slope}} compiles, and ships as a tree the renderer evaluates every frame rather than as compile-time text.
#Tables from data
A binds a dataset directly and projects columns out of it:
table(data: #islands; columns: island, area, species)columns: is a list of plain strings the table resolves against its own data: — library-interpreted names rather than references, which is why they are written bare. Every other consumer receives a column as text through substitution: a splices {{#survey.area}} into a numpy array literal.
#Columns as values
Beyond the table, a column is bound where an attribute is typed to take one, and every such attribute takes one column. The consumers:
- The graph's data marks — , , , — take a series per axis,
x:andy:, and aperiod:. - takes
regions:,values:,intensity:andperiod:; takeslat:,lon:andsize-by:. - takes the
period:column it scrubs, in any environment. - Everything else — prose, a , an attribute typed for a list — receives a column as text by substitution,
{{#survey.area}}.
#One column per attribute
A column binds one attribute, never a pair: scatter(x: #islands.area; y: #islands.species), marker(lat: #cities.lat; lon: #cities.lon). Parallel columns stay row-aligned — a blank cell is kept in place as no value rather than dropped — and where a consumer pairs two columns it drops the pair whose either half it cannot read, so a gap in one column never shifts the readings of the other. The graph marks alone also take a series written out beside a column:
scatter(x: 1, 2, 3; y: 2.0, 2.4, 3.1)
scatter(x: #islands.area; y: #islands.species)
bar(x: North, South, East; y: 42, 31, 55)
choropleth(regions: #gdp.country; values: #gdp.gdppc)#The two readings of a series entry
A written series entry is read two ways, because the axis it lands on is what decides. A number is a number. 2 * k is an expression of the parameter k — and so, to the expression parser, is Red, which reads as R·e·d. So an entry keeps both its expression and the text it was written as, and the axis picks: on a numeric axis the expression is evaluated, on a categorical one the text is looked up among the axis's names. An entry that is neither — Strongly agree, which no expression grammar accepts — is a name and nothing else.
Which way an undeclared (auto) axis goes follows from that. An entry counts as a name when it is not a number and not an expression whose every letter is a declared ; a series with a name on it rules the axis in names, in the order they first appear. So x: a, b beside parameter(var: a) and parameter(var: b) is two numbers, and beside nothing is two categories. The whole rule is on the graph guide; the consequence for data is that a column of names needs no declaration to be read as names, and a name the axis does not have — under x format: North, South, a row reading West — is a compile error naming the row.
#What a column reads as
A column's kind is decided for the column, not cell by cell: reading 2024-03-15 one cell at a time gives 2024, and a year of daily readings lands on one position. A column is numeric when every non-blank cell is a number; a date when every one is an ISO date, 2024-03-15 or 2024-03, read as a fractional year so that a domain and a fit's slope still speak plain numbers; and text otherwise — names. Each consumer reads the kind its own way. A graph axis takes numbers and dates as positions and labels a date column in whole years under auto; text rules the axis in names, and a text column on an axis declared linear is an error rather than a column of dropped rows. A choropleth shades a numeric values: column through its ramp and enumerates a text one into categories, each with a flat hue. A marker's lat: and lon: are numeric or nothing. Month and weekday names are text like any other, matched by full name or first three letters against the month and weekday presets, and never turned into numbers on the way in.
#Rows over time
Data with a state per period — per year, per month, per hour — is written long-format: one row per thing per period, with a column naming the period. A consumer names that column as its period: — a graph mark, a choropleth — and a in the same environment names it too. The consumer then shows the slice at whatever the axis holds, gliding between the two periods that bracket it, so a reader stopped between 2023 and 2024 sees half way; a thing whose own rows start late or end early holds its nearest reading rather than vanishing. Where the quantity is not a continuous function of time — a count of days, a result per election — discrete on the parameter.time makes the axis stand on one period at a time, and the consumer jumps from slice to slice. A graph's frame is built from every period at once and holds still as they scrub.
@data#calendar{
year | month | days
2023 | January | 31
2023 | February | 28
2024 | January | 31
2024 | February | 29
}
canvas.graph{
bar(x: #calendar.month; y: #calendar.days; period: #calendar.year)
parameter.time#years(period: #calendar.year; name: year)
}(
x format: months
)The period column is read by kind, and the scrubber reads it back in the same form: years and plain numbers as themselves; ISO dates as fractional years, shown as the nearest day; month and weekday names as their positions, shown by name; HH:MM as hours; YYYY-Www as weeks. The axis owns a letter like any parameter — t unless given — and {{#years.value}} cites the period in prose. One environment holds one time axis; a second period: column in it slices against the same one.
#Scope and limits
- Directives have no placement rules — a
is document-visible wherever it is written, including inside an d partial. - Attached data is
.csvor.geojson, and nothing else — not.json, not a URL. - No inline geo data. Boundaries arrive as a file.
- No computed columns, row filters or aggregates on a tabular dataset. A transform belongs upstream, stored in the data, which keeps column access a pure lookup and columns substitutable.
holds tables of values; holds scalars and repeated fragments.
#Attributes
from: a .csv, whose header row names the columns, or a .geojson, which becomes boundaries for a canvas.geo environment. The extension IS the format declaration, so .json is not admitted and nothing else is guessed at. Omit it to declare tabular rows inline in the body instead. Written as shorthand: #survey:plants.csv.Can be written as shorthand: @data:value#Allowed content
#Examples
@data#islands{
island | area | species
Baltra | 25.09 | 58
Pinta | 60.0 | 94
}@data#survey:galapagos-plants-1973.csvcanvas.code{
lines{
area = np.array([{{#survey.area}}])
}
}(
language: python
)@data#wards:sydney-wards.geojson
@data#turnout:turnout.csv
canvas.geo{
choropleth(in: #wards; regions: #turnout.ward; values: #turnout.pct)
}(
projection: conic
)#Notes
- An attached
.geojsonis OPAQUE: it has no columns, so#wards.nameand{{#wards.name}}are both compile errors that name the fix — bind the dataset itself,in: #wards. Which feature property names each region is said on the consuming environment withnames:, not here. - Only a tabular dataset can be written inline. A
{ }body is always columns, so there is no inline form of geo data.