canvas.geo
A geographic environment on the scene's canvas. Its in: names the boundary pack the environment is drawn over — canvas.geo:world is a world map on its own, with no child node needed — and every overlay inside it inherits that pack. The body holds those overlays: a choropleth joined to an dataset, marker points, great-circle arc routes, further region layers, and the parameters that drive them. The frame is a camera rather than a pair of domains — centre: (latitude first) and zoom: (web-map doubling levels) — and both are animatable with !cue.to, so one keyframe on each is a fly-to. The committed packs ship with the library and an attached .geojson travels with the document, so nothing is ever fetched from a tile server: a map renders offline, under both themes, and identically in ten years.
#The geographic environment
is a map surface. Its implicit attribute is in, the boundary pack the environment is drawn over, so canvas.geo:world{ } is already a world map with nothing inside it. The committed packs are world (Natural Earth 110m countries) and six admin-1 sets — us-states, canada-provinces, australia-states, uk-counties, nz-regions and europe-admin1. Every overlay in the body inherits that pack unless it names its own. title captions the environment, projection chooses how the sphere is laid flat, and show-scale (on by default) says what a length on screen is worth on the ground.
canvas.geo:world{
marker(at: 48.85, 2.35; label: Paris)
}(
title: Where the survey ran
projection: equal-earth
)Boundaries a pack does not carry — electorates, catchments, council wards — arrive as an attached .geojson file, and in names it the same way it names a pack: in: #wards, pointing at an declaration. Either way the geometry is drawn as SVG, exactly as a graph mark is, and nothing is fetched from a tile server — which is what lets a map render offline, under both themes, and identically in ten years.
equal-earth keeps areas honest and is the right world framing; mercator keeps angles honest and is the frame on which a great-circle route visibly bends; conic tunes its standard parallels to whatever the environment contains and is the right choice for one country; albers-usa carries Alaska and Hawaii in as insets; globe is the sphere itself, the far hemisphere behind the horizon. Choose the one whose distortion your claim can afford.#centre, zoom and the fly-to
A geographic environment is framed by a camera rather than by a pair of domains: centre — latitude first — and zoom, in web-map doubling levels, where 1 fits the world's width and each level halves the span. Omit them both and the environment fits its own contents across the whole choreography, so a marker that travels stays framed at every point of its journey.
Both are animatable, and a !cue.to on each with the same anchor is the fly-to. A centre may also be written as #id naming a marker the environment already draws, so a map framed on Tokyo that also pins Tokyo states the place once; a keyframe onto a reference snaps rather than gliding, because a reference has no midpoint to interpolate through.
canvas.geo#tour:world{
region(in: world; only: France, Germany; fill: teal)
}(
centre: 20, 0 !cue.to{50.5, 15}(at: 1; over: 900ms)
zoom: 1 !cue.to{4}(at: 1; over: 900ms)
)interactive: true hands the camera to the reader — scroll to zoom, drag to pan, double-click to reset. Exploration stays on the environment's subject: the zoom floor is the frame that fits its packs, markers and arcs, so a reader cannot lose the map. Their camera then overrides the authored one for the session, and continue: #that-environment carries it into a later environment along with their parameter values.
#Overlays
Everything drawn over the basemap is an overlay, and the four share opacity and hidden (declared but not drawn, and animatable, so hidden: true !cue.to{false}(at: 2) is a reveal). Cues wrap overlays exactly as they wrap graph marks. The attention cues do not reach this environment: a highlight ring and a spotlight are plot machinery, and a geographic environment admits only and .
#Boundary layers
The environment's in: already draws its pack, so a is for what a basemap cannot say: a subset in its own colours, a second pack layered over the first, an attached.geojson over a committed pack, or boundaries that arrive on a cue. only and except narrow it by name or ISO alpha-3 code, matched case-insensitively — France, DEU and Ivory Coast all read — and fill, stroke and width paint what is left.
canvas.geo:world{
region(in: world; except: Antarctica)
region(in: world; only: France, Germany, Italy; fill: teal)
}#Joining a dataset to regions
joins a column onto the pack's regions — the geographic sibling of the graph's . regions names the column of region names and values the column to shade by; join keys match pack names and short codes case-insensitively, and a region the join does not cover keeps the neutral land fill rather than disappearing.
What values holds decides the reading. A numeric column shades through a ramp — multicolour, temperature, or a single hue's own intensity — with range pinning the window. A text column enumerates instead: each distinct value becomes a category with its own flat hue, named by key, and intensity gives depth within that hue. That pair is the electoral map's own grammar — the winner names the colour, the margin names how solid it is.
canvas.geo:australia-states{
choropleth(
regions: #vote.state
values: #vote.party
intensity: #vote.margin
key: Labor red, Coalition blue, Greens green
)
}(
title: Who led each state
)min, max, mean and count over the values the join actually covered, so prose can cite the spread the shading spans instead of a number typed in beside it: give the mark an id — choropleth#gdp(…) — and read {{#gdp.max}}.#Places
is a point on the earth, in two forms. Singly, at takes a coordinate, latitude first, with an optional label; either half may be a parameter expression, so a marker rides a slider or a keyframe. Data-bound, lat and lon name columns and the one node draws every row — add size-by and symbol area scales with that column, which is the proportional-symbol map.
canvas.geo:world{
marker(lat: #quakes.lat; lon: #quakes.lon; size-by: #quakes.magnitude; colour: red; opacity: 0.6)
}#Great-circle routes
draws the great-circle path between two places — the shortest route over the earth, drawn the way the earth actually is. On a mercator environment the London–Tokyo arc bends far north of the straight line between them, and that one element is the whole projection-distortion lesson. Either end may be #id naming a marker in the same environment, and an anchored end follows its marker. An arc publishes derived distance in kilometres — the number the arc is.
canvas.geo:world{
marker#london(at: 51.47, -0.46; label: London)
marker#tokyo(at: 35.55, 139.78; label: Tokyo)
cue.draw{
arc#route(from: #london; to: #tokyo; label: {{round(#route.distance, 0)}} km)
}(
in: 1
in-duration: 1200ms
)
}(
projection: mercator
)#Moving a map through time
A map moves through time in one of two ways, and which one is a fact about the data. For a handful of periods, chain the columns: values is animatable, so #y1990.rate !cue.to{#y2000.rate}(at: 1) eases every region between its own two readings. Pin range when you do, or the ramp re-normalises each frame and the change erases itself.
For long-format data — one row per region per period — is the better tool. It is an ordinary parameter under the skin, but it takes its range from the data rather than being told one, reads as a period, and can play. Name the period column and the environment gains a scrubber over exactly that span; marks carrying a period slice themselves against it, interpolating between the two periods that bracket wherever the reader is standing.
canvas.geo:world{
parameter.time#years(period: #rates.year; name: Year; duration: 12s)
choropleth(regions: #rates.country; values: #rates.rate; period: #rates.year; range: [0, 220])
}Parameters and cues gives the rule the choice actually turns on: a change belongs to a scrubber when one sentence holds across its whole range, and to a cue otherwise.
#Derived attributes
Two overlays compute something and publish it for reading: a choropleth's min, max, mean and count, and an arc's distance. A parameter.time publishes value, the period the reader is standing on. Reach one by member access on the node's id — #route.distance in an attribute, {{#route.distance}} in prose. Derived attributes covers how they behave.
#Attributes
world is Natural Earth 110m countries; the other six are admin-1 packs. It may instead be #id naming an attached .geojson dataset — in: #wards — which makes the attachment itself the boundaries. Normally written in the head, canvas.geo:world, the way canvas.code:python is.Can be written as shorthand: canvas.geo:value.geojson only: the feature property holding each region's name, which is what a choropleth joins on. Absent, the first of name, NAME, title or id the features carry..geojson only: a feature property holding a short join code — the custom equivalent of a pack region's ISO alpha-3..geojson only: Douglas–Peucker tolerance in degrees, thinning boundary detail as the file is read. 0 keeps every point; a large file wants about 0.01.0title: this one is visible content and not merely an accessible name. Rich text, so $maths$ and inline formatting render.equal-earth, the default, keeps AREAS honest and is the right world framing. mercator keeps ANGLES honest: the frame every slippy map uses, and the one on which a great-circle arc visibly bends away from the straight line. equirectangular plots latitude and longitude straight. conic is an Albers equal-area conic that tunes its standard parallels to whatever the environment contains — what national atlases use, and the right choice for a single country or region. albers-usa is the United States composite, the lower 48 on an equal-area conic with Alaska and Hawaii carried in as insets, so a US environment spends no width on the Pacific. globe is the sphere itself, orthographic, with the far hemisphere behind the horizon.equal-earthcentre: 48.85, 2.35 — or #id naming a marker this environment already draws, centre: #tokyo, so the place is stated once. Either half of a coordinate may be a parametised expression. Animatable: centre: 20, 0 !cue.to{50.5, 15}(at: 2; over: 900ms) pans the frame, and writing the same anchor on zoom: makes the two move as one fly-to. A keyframe targeting a #id snaps rather than gliding, because a reference has no midpoint to interpolate through. Omitted, the environment frames its own contents.levels: 1 fits the world's width, and each level halves the span. Omitted, the zoom is AUTOMATIC — the environment fits what is inside it across the whole choreography, so a marker that travels stays framed at every point of its journey; writing zoom: 1 asks for exactly 1 instead. Parametised, and animatable with !cue.to.falsetruefalse#Allowed content
#Allowed in
#Examples
canvas.geo:world(title: The world)canvas.geo:australia-states{
choropleth(regions: #vote.state; values: #vote.party; key: Labor red, Coalition blue)
}(
title: Who led each state
)canvas.geo#tour:world{
region(in: world; only: France, Germany; fill: teal)
}(
centre: 20, 0 !cue.to{50.5, 15}(at: 1; over: 900ms)
zoom: 1 !cue.to{4}(at: 1; over: 900ms)
)canvas.geo:canada-provinces{
choropleth(regions: #d.province; values: #d.rate)
}(
projection: conic
)#Notes
- An environment's own id is written on the node —
canvas.geo#europe{…}— andcontinue:points back at it with#europe. - Cues wrap overlays exactly as they wrap graph marks, and
cue.drawtraces anarcalong its length. - The cursor's coordinates, and the name of the region beneath it, read out in the corner of the environment.