canvas.network

A network environment on the scene's canvas: a graph in the discrete-maths sense, a network in the network-science sense, or a causal diagram. Vertices are placed by the named layout: and the environment measures what was written — degree, betweenness, components, communities, shortest paths — and can draw those measurements back onto the picture through the vertices and edges tables' size-by:, colour-by: and width-by: — encodings live on the tables, never on the environment. The layout and every measurement are taken from the whole declared network, whatever step each part arrives on, so nothing moves and no cited number changes as a later cue reveals more of it. Edges are written one at a time with edge, or bound from an @data edge list with edges, and the two mix. Positions are fractions of the frame, top-left origin, y DOWN, as in canvas.diagram. The frame is a camera: focus: names what it looks at — fit, #ids of vertices or paths, or a point — and the camera finds the zoom from it; animated with !cue.to, it flies between frames. Vertices keep their size as the frame closes in.

canvas.networkNode
A small network of six lettered vertices placed by the force layout, sized by degree.A small network of six lettered vertices placed by the force layout, sized by degree.
Writing with it:

#The network environment

canvas.network draws structure as the subject: a graph in the discrete-maths sense, a social or citation network, a food web, a causal loop. A vertex is a point with a name — its body is its label — and an edge joins two of them by #id. The environment places the vertices, and measures what was written.

A network is undirected unless direction: says otherwise. direction: arrows makes every edge run from from to to: edges carry heads, in- and out-degree are counted apart, and a path follows the edges' direction. Two edges between the same pair bow apart so both stay visible, and an edge from a vertex to itself is a loop over it. sign: positive or negative puts a + or − beside an edge's head, and shape: none draws a vertex as its words alone — together, a causal-loop diagram.

A causal-loop diagram of Births, Population and Deaths as bare words: Births and Population reinforce each other, Population raises Deaths, and Deaths lowers Population, each arrow marked + or −.A causal-loop diagram of Births, Population and Deaths as bare words: Births and Population reinforce each other, Population raises Deaths, and Deaths lowers Population, each arrow marked + or −.
canvas.network:circle{
    vertex#births{Births}(shape: none)
    vertex#population{Population}(shape: none)
    vertex#deaths{Deaths}(shape: none)

    edge(from: #births; to: #population; sign: positive)
    edge(from: #population; to: #births; sign: positive)
    edge(from: #population; to: #deaths; sign: positive)
    edge(from: #deaths; to: #population; sign: negative)
}(
    direction: arrows
)

On a crowded directed network, heads pile up round every popular vertex. direction: curves is the same directed network with no heads: each edge bows to the right of the way it travels, so read clockwise, an edge runs forward, and two vertices pointing at each other get two separate curves.

A directed citation network of eight papers drawn with no arrowheads, each edge bowing to the right of the way it runs, so a pair of papers citing each other would sit on opposite sides.A directed citation network of eight papers drawn with no arrowheads, each edge bowing to the right of the way it runs, so a pair of papers citing each other would sit on opposite sides.
@data#citations{
    paper       | cites
    Method      | Survey
    Dataset     | Survey
    Benchmark   | Method
    Benchmark   | Dataset
    Extension   | Method
    Extension   | Benchmark
    Critique    | Benchmark
    Replication | Extension
    Replication | Dataset
    Review      | Extension
    Review      | Critique
    Review      | Replication
    Review      | Survey
}

canvas.network{
    edges(from: #citations.paper; to: #citations.cites)
}(
    direction: curves
)

canvas.diagram also draws things joined by arrows. The difference is what the picture is for: a diagram routes arrows round boxes of text to say what leads to what, and a network is measured.

#Layouts

layout: is the implicit attribute, so canvas.network:circle{…} names its layout in the head. force, the default, draws connected vertices together and pushes every vertex away from the others. It is seeded, so it comes out the same every time; a different seed: is a different arrangement of the same network. circle sets the vertices round a ring, each group: kept together, and grid sets them in rows in the order written.

Three layouts rank the vertices instead. tree hangs a spanning tree from root:, each parent centred over its children. layered ranks a directed network so its edges run one way. flow: right turns either on its side.

A tree hung from Vertebrates, branching to Fish, Amphibians and Amniotes, with Amniotes branching to Reptiles, Birds and Mammals.A tree hung from Vertebrates, branching to Fish, Amphibians and Amniotes, with Amniotes branching to Reptiles, Birds and Mammals.
canvas.network:tree{
    vertex#vertebrates{Vertebrates}
    vertex#fish{Fish}
    vertex#amphibians{Amphibians}
    vertex#amniotes{Amniotes}
    vertex#reptiles{Reptiles}
    vertex#birds{Birds}
    vertex#mammals{Mammals}

    edge(from: #vertebrates; to: #fish)
    edge(from: #vertebrates; to: #amphibians)
    edge(from: #vertebrates; to: #amniotes)
    edge(from: #amniotes; to: #reptiles)
    edge(from: #amniotes; to: #birds)
    edge(from: #amniotes; to: #mammals)
}(
    root: #vertebrates
)

bipartite sets the two sides of a two-sided network on two ranks: the two group:s when exactly two are written — here as each vertex's implicit attribute, vertex#ana:students{Ana} — and otherwise the sides a two-colouring finds.

A bipartite network: three students on the top rank, four modules on the bottom, each student joined to the modules they take.A bipartite network: three students on the top rank, four modules on the bottom, each student joined to the modules they take.
canvas.network:bipartite{
    vertex#ana:students{Ana}(colour: blue)
    vertex#ben:students{Ben}(colour: blue)
    vertex#cai:students{Cai}(colour: blue)
    vertex#stats:modules{Statistics}(colour: orange)
    vertex#algebra:modules{Algebra}(colour: orange)
    vertex#logic:modules{Logic}(colour: orange)
    vertex#physics:modules{Physics}(colour: orange)

    edge(from: #ana; to: #stats)
    edge(from: #ana; to: #algebra)
    edge(from: #ben; to: #algebra)
    edge(from: #ben; to: #logic)
    edge(from: #cai; to: #logic)
    edge(from: #cai; to: #physics)
}
The layout is computed from the whole network, cues included: a cue controls when a vertex is seen, never where it sits, so nothing moves as a later step reveals more of it.

#Placing a vertex by hand

A vertex with at: sits at that fraction of the frame — top-left origin, y down, as in canvas.diagram. The force layout arranges the others around it; the rest leave it out of their arrangement. at: is animatable, and the layout reads its first value, so a keyframe moves that vertex alone.

#Networks from @data

edges binds an edge list: one edge per row, endpoints named by two columns, and weights: a third. Each distinct cell is a vertex labelled with its text. vertices binds a vertex table: one vertex per row, keyed by names:, which an edge list's cells are matched against. A table's vertex with no edges is drawn on its own.

An inline vertex whose label or id matches a name — case and spacing aside — is that vertex, which is how a data vertex is coloured, pinned, or given an #id to point a path at. Inline edges, edge lists and vertex tables mix freely.

#Drawing the data on the network

Colour and size are set on the tables, never on the environment, so each table's rows follow their own rule. The columns draw what the network cannot measure — the data's own numbers and categories. On a vertex table, sizes: gives each vertex an area in proportion to a numeric column and groups: sorts them for colour by: group. On an edge list, widths: draws a numeric column as stroke width and groups: sorts the edges into kinds, each in its own hue under colour by: group. Every such attribute names a column, which is why they are plural.

An edge list can instead take its colour from the vertices each edge joins. colour by: ends draws an edge in the colour its two ends share, and an even mix of the two where they differ; colour by: gradient runs from the colour of the vertex it leaves to the colour of the one it reaches.

The data says what a vertex or an edge is, never what colour to draw it: each group is dealt a hue from one palette — vertex groups first, then edge groups — so no hue means two things in one picture. Where a hue carries meaning, the table's hues: pins it: hues: motorway = red, other road = grey. The groups it does not name are dealt from what is left. Where the data's spelling is not what a reader should see, hue labels: renames a group in the key alone: hue labels: neutral = Neutral.

Six French cities as vertices sized by population and coloured by region, joined by roads drawn thicker for more lanes, motorways in red and other roads in grey; beneath, keys to the regions, the population sizes, the road kinds and the lane widths.Six French cities as vertices sized by population and coloured by region, joined by roads drawn thicker for more lanes, motorways in red and other roads in grey; beneath, keys to the regions, the population sizes, the road kinds and the lane widths.
@data#cities{
    city      | population | region
    Paris     | 2.10       | north
    Lille     | 0.24       | north
    Lyon      | 0.52       | south
    Marseille | 0.87       | south
    Bordeaux  | 0.26       | west
    Nantes    | 0.32       | west
}

@data#roads{
    from      | to        | km  | lanes | kind
    Paris     | Lille     | 225 | 6     | motorway
    Paris     | Lyon      | 465 | 6     | motorway
    Lyon      | Marseille | 315 | 4     | motorway
    Paris     | Nantes    | 385 | 4     | motorway
    Nantes    | Bordeaux  | 345 | 4     | motorway
    Bordeaux  | Marseille | 645 | 2     | other road
    Lille     | Nantes    | 590 | 2     | other road
}

canvas.network{
    vertices(names: #cities.city; sizes: #cities.population; groups: #cities.region; colour by: group)
    edges(from: #roads.from; to: #roads.to; weights: #roads.km; widths: #roads.lanes; groups: #roads.kind; colour by: group; hues: motorway = red, other road = grey)
}

The footer keys whatever a reader could not decode unaided: the hue of each group, vertex and edge alike, and what vertex area and edge width stand for, as a wedge from the least reading to the greatest. Communities and components are not keyed: their colours are numbered arbitrarily, and the clusters show themselves. Nor is an edge coloured from its ends, whose colours are the vertices'. !legend turns the key off.

Hovering a vertex fades everything outside its reach. On a directed network that is what lies downstream of it — the vertices its edges point to, and theirs, and the edges on the way; on an undirected one, its whole connected piece of the network.

weights: and widths: are different claims. A weight is what the network measures an edge by — here the kilometres a shortest path adds up — and widths: is only drawn: the lanes, which make a road look major without making it any shorter.

Two bindings draw a network as large as its data. Below, 160 researchers and the 342 papers they wrote together — generated for the example, not collected — each vertex sized by its author's papers and coloured by department. A label is set only where it has room: inside a vertex large enough to hold it, or beside one on a side its edges leave clear, without covering another vertex or label or leaving the frame. A vertex whose label has no room shows it on hover.

A network of 160 researchers in six departments, each a coloured cluster, vertices sized by papers published; edges within a department take its colour, and the scattering between departments a mix of the two.A network of 160 researchers in six departments, each a coloured cluster, vertices sized by papers published; edges within a department take its colour, and the scattering between departments a mix of the two.
canvas.network{
    vertices(names: #researchers.name; sizes: #researchers.papers; groups: #researchers.department; colour by: group)
    edges(from: #coauthors.a; to: #coauthors.b; colour by: ends)
}

#Measuring the network

Each vertex derives degree, in-degree, out-degree and betweenness — the share of shortest paths between other vertices that pass through it, 0 to 1. The environment derives vertices, edges, components, communities and density. Prose cites them as any live value: {{#hub.degree}}, {{#net.components}}. They are measurements of the whole declared network, so a cited number does not change as the picture builds.

A vertex table's size by: gives each vertex an area in proportion to degree, in-degree, out-degree, betweenness or weight (the summed weight of its edges). Its colour by: colours vertices by connected component — which piece of the network a vertex is in — or by community: the densely knit clusters the Louvain method finds within a piece. An edge list's width by: weight thickens each edge by its weight. A vertex's own colour: and size: still apply.

A table with no names:, or a list with no from: and to:, lists no rows: its rules cover every vertex no named table lists, or every inline edge. A network written inline is sized by one line, vertices(size by: degree).

Two tightly knit clusters of four joined through one vertex, coloured by community, with the joining vertex drawn largest by betweenness.Two tightly knit clusters of four joined through one vertex, coloured by community, with the joining vertex drawn largest by betweenness.
@data#pairs{
    a | b
    A | B
    A | C
    B | C
    C | D
    B | D
    E | F
    E | G
    F | G
    G | H
    F | H
    D | X
    X | E
}

canvas.network{
    vertices(size by: betweenness)
    edges(from: #pairs.a; to: #pairs.b; colour by: community)
}

#Shortest paths

path is the shortest route through the vertices through: names, in turn: path(through: #a, #f) from a to f, path(through: #a, #c, #f) from a to c and on to f. It counts weights where the network has them and follows direction on a directed network. It is drawn as a broad stroke over the edges it takes, and derives length and hops. Under cue.draw it strokes in hop by hop.

Three frames of the shortest path from A to C drawing in over a weighted ring of four vertices: it runs A, D, C, total weight 4, rather than across the heavier A–B edge.Three frames of the shortest path from A to C drawing in over a weighted ring of four vertices: it runs A, D, C, total weight 4, rather than across the heavier A–B edge.
canvas.network:circle{
    vertex#a{A}
    vertex#b{B}
    vertex#c{C}
    vertex#d{D}
    edge(from: #a; to: #b; weight: 4)
    edge(from: #b; to: #c; weight: 1)
    edge(from: #a; to: #d; weight: 2)
    edge(from: #d; to: #c; weight: 2)

    cue.draw{
        path(through: #a, #c)
    }(
        in: 1
        in duration: 1.4s
    )
}(
    show weights
)

Where some leg has no route — a directed network with no way back, two components — the path draws nothing and derives nothing, so prose citing its length reads as absent rather than as a wrong number.

#focus and zoom

focus: names what the camera looks at, and the camera finds the zoom from it. fit, the default, is the whole network. #ids of vertices or paths are framed together; a path, by its route; a single vertex, among its neighbours, since what a vertex connects to is what it means. A fractional point centres the fit there. It animates with !cue.to, the camera flying between the frames: focus: fit !cue.to{#hub}(at: 1; over: 1.4s). Vertices keep their size as the frame closes in, so a dense neighbourhood pulls apart rather than magnifying the tangle. interactive hands the reader wheel-zoom and drag, with a double-click back to the authored frame.


#Attributes

layoutImplicit
How the vertices are placed. force draws connected vertices together and pushes every vertex away from the others, seeded by seed: so it comes out the same every time. circle sets them round a ring, each group: kept together. grid sets them in rows in the order written. tree hangs a breadth-first spanning tree from root:. layered ranks a directed network so its edges run one way. bipartite sets the two sides of a two-sided network on two ranks — the two group:s when exactly two are written, otherwise the sides a two-colouring finds.
forcecirclegridtreelayeredbipartite
Can be written as shorthand: canvas.network:value
Defaultforce
direction
Whether each edge runs one way, from from to to, and how that is shown. none is an undirected network. arrows and curves are directed — in- and out-degree are measured separately, and a path follows the edges' direction — and differ only in the drawing: arrows puts a head at the end; curves draws no head and bows every edge to the right of the way it travels, so an edge read clockwise runs forward and a reciprocal pair separates by itself — the convention for large directed networks, where heads crowd into every vertex.
nonearrowscurves
Defaultnone
flow
Which way tree, layered and bipartite run: ranks stack downward, or rightward. The other layouts ignore it.
downright
Defaultdown
root
#id of the vertex a tree hangs from, and an undirected layered ranking starts at. Absent, the first vertex written — on a directed network, the first with no edge pointing into it.
network-ref
seed
The force layout's starting shuffle. Each seed is a different arrangement of the same network; change it when one arrangement crosses two edges the narrative needs apart.
Default0
show-weights
Labels each weighted edge that has no label: of its own with its weight.
Also written: weights
Defaultfalse
legend
The footer's key to what colour and size mean: a row for the vertices and a row for the edges, each giving its groups' hues and, as a wedge from the least reading to the greatest, what its size stands for. On by default; off for a network whose prose already says it.
Defaulttrue
title
The caption set over the top-left of the environment.
focusAnimatable
What the camera looks at; the camera finds the zoom from it, so there is none to write. fit, the default, is the whole network. #ids of vertices or paths are framed together: several, the smallest frame holding them all; a path, its route; one vertex, the vertex among its neighbours. A fractional point centres the fit there. Animated with !cue.to, the camera flies between the frames: focus: fit !cue.to{#hub}(at: 1; over: 1.4s). Vertices keep their size as the frame closes in, so a dense neighbourhood pulls apart.
network-focus
Defaultfit
interactive
Reader exploration: wheel-zoom about the cursor, drag to pan, double-click to reset. The explored camera overrides the authored or keyframed one once touched.
Defaultfalse
continue
Inherit an earlier network environment's state: continue: #that-environment. The reader's explored camera carries forward; every authored attribute is the new environment's own.

#Derived attributes

Computed while the element renders — read one with #id.name in attribute position or {{#id.name}} in prose, never set.

verticesDerived
How many vertices the network has, data vertices included.
edgesDerived
How many edges the network has, every row of an edge list included.
componentsDerived
How many connected pieces the network falls into, ignoring the edges' direction.
communitiesDerived
How many communities the Louvain method finds.
densityDerived
The edges present over the edges possible, from 0 to 1. Self-loops are not counted.

#Allowed content

#Allowed in

#Examples

canvas.network:circle{
    vertex#a{A}
    vertex#b{B}
    vertex#c{C}
    vertex#d{D}
    edge(from: #a; to: #b; weight: 4)
    edge(from: #b; to: #c; weight: 1)
    edge(from: #a; to: #d; weight: 2)
    edge(from: #d; to: #c; weight: 2)
    cue.draw{
        path#route(through: #a, #c)
    }(
        in: 1
    )
}(
    show-weights: true
)
canvas.network{
    vertex#uk{United Kingdom}(colour: blue)
    vertices(size-by: weight; colour-by: community)
    edges(from: #trade.exporter; to: #trade.importer; weights: #trade.value; width-by: weight; colour-by: ends)
}(
    direction: arrows
)

Last updated