| exporter | importer | billions |
|---|---|---|
| China | United States | 500 |
| Mexico | United States | 455 |
| Canada | United States | 420 |
| United States | Mexico | 320 |
| United States | Canada | 355 |
37 Network
What is connected to what? Some tables record relations rather than measurements: each row says that one thing is related to another. A network diagram draws those relations. Each thing becomes a node, one marked place in the picture, and each relation becomes a stroke between two nodes. Nodes with many relations are drawn near the middle.
Nothing in such a table says where any node should sit. That is the whole difficulty, and this chapter shows how gog answers it. The engine computes a position for every node. A space named network() draws the result, with no axes at all.
trade_partners holds one row per trade relation: who exports, who imports, and the value of the goods, in billions of dollars, rounded:
37.1 One row is one edge
The first question of the trade table is who trades with whom, and one stroke per row draws exactly that. Write the edge mark derived by layout(), name the two endpoint columns in order, and say the space:
data(trade_partners) + edge * layout(exporter, importer) + network()data(trade_partners) + edge * layout(col.exporter, col.importer) + network()data(trade_partners) + edge * layout(:exporter, :importer) + network()plot(data(trade_partners),
layer(edge, layout(col.exporter, col.importer)), network())“Given trade partners: edges derived by layout from exporter to importer, in the network.”
Each stroke is one row of the table. The layout reads both columns and finds every distinct name. All nodes push apart, and nodes that share a row also pull together, so the connected ones settle near each other. The engine computes every position itself, and computes it the same way everywhere, so this sentence draws the same picture in R, Python, Julia and JavaScript, to the byte.
The panel has no axes, no ticks and no grid, because network() draws none. A layout’s positions mean nothing as quantities. Flip the picture left to right and it means the same, so a number on an axis would state nothing about the data. A layout therefore draws in network() and in no other space.
37.2 The nodes and their names
The strokes alone do not say which node is which country, or which countries trade the most. Two more layers finish the diagram. A point layer draws the nodes, and a text layer names them. Each one writes the same layout, and the engine computes it once per layer. Every computation lands on the same positions, because the placement is deterministic. The layout publishes two columns about each node: its name, and its degree, the count of its relations. size(degree) draws the nodes with the most relations largest. The text layer also composes repel, the modifier that moves labels off each other, so a name rests beside its dot instead of covering it:
data(trade_partners) +
edge * layout(exporter, importer) +
point * layout(exporter, importer) + size(degree) +
text * layout(exporter, importer) * repel + label(name) +
network() +
title("Who trades with whom")(data(trade_partners) +
edge * layout(col.exporter, col.importer) +
point * layout(col.exporter, col.importer) + size(col.degree) +
text * layout(col.exporter, col.importer) * repel + label(col.name) +
network() +
title("Who trades with whom"))data(trade_partners) + edge * layout(:exporter, :importer) +
point * layout(:exporter, :importer) + size(:degree) +
text * layout(:exporter, :importer) * repel + label(:name) + network() +
title("Who trades with whom")plot(data(trade_partners),
layer(edge, layout(col.exporter, col.importer)),
layer(point, layout(col.exporter, col.importer)), size(col.degree),
layer(text, layout(col.exporter, col.importer), repel), label(col.name),
network(), title("Who trades with whom"))“Given trade partners: edges derived by layout from exporter to importer, and also points derived by layout from exporter to importer, size by degree, and also text derived by layout from exporter to importer and repel, label by name, in the network.”
The nodes with the most relations sit near the middle. No row of the table says that the United States, China and Germany are centers of trade; saying it is what the layout is for.
37.3 An edge carries its own columns
Every stroke so far is drawn alike, whether the trade is 500 billion dollars or 50 billion. An edge is one row, so its color and its opacity can follow any column of the table. opacity(billions) maps each stroke’s opacity to its value, so the heavier relations draw darker:
data(trade_partners) +
edge * layout(exporter, importer) + opacity(billions) +
point * layout(exporter, importer) + size(degree) +
text * layout(exporter, importer) * repel + label(name) +
network() +
title("Heavier trade, darker stroke")(data(trade_partners) +
edge * layout(col.exporter, col.importer) + opacity(col.billions) +
point * layout(col.exporter, col.importer) + size(col.degree) +
text * layout(col.exporter, col.importer) * repel + label(col.name) +
network() +
title("Heavier trade, darker stroke"))data(trade_partners) + edge * layout(:exporter, :importer) +
opacity(:billions) + point * layout(:exporter, :importer) +
size(:degree) + text * layout(:exporter, :importer) * repel +
label(:name) + network() + title("Heavier trade, darker stroke")plot(data(trade_partners),
layer(edge, layout(col.exporter, col.importer)), opacity(col.billions),
layer(point, layout(col.exporter, col.importer)), size(col.degree),
layer(text, layout(col.exporter, col.importer), repel), label(col.name),
network(), title("Heavier trade, darker stroke"))“Given trade partners: edges derived by layout from exporter to importer, opacity by billions, and also points derived by layout from exporter to importer, size by degree, and also text derived by layout from exporter to importer and repel, label by name, in the network.”
A node is different. A node stands for many rows, and those rows carry different values. So a node layer maps only name and degree. Every other column belongs on the edge layer, where one row carries one value.
37.4 An angle asks for the cube
A plane gives the layout two directions to spread its nodes, and a cube gives it three, in a picture you can turn. Bare network() computes the layout in the plane. State a viewing angle and the same edges are laid out in a cube instead. Every node takes a new position, and the picture is drawn from the angle you asked for:
data(trade_partners) +
edge * layout(exporter, importer) + opacity(billions) +
point * layout(exporter, importer) + size(degree) +
network(turn = 35, tilt = 20) +
title("The same network, given depth")(data(trade_partners) +
edge * layout(col.exporter, col.importer) + opacity(col.billions) +
point * layout(col.exporter, col.importer) + size(col.degree) +
network(turn = 35, tilt = 20) +
title("The same network, given depth"))data(trade_partners) + edge * layout(:exporter, :importer) +
opacity(:billions) + point * layout(:exporter, :importer) +
size(:degree) + network(turn = 35, tilt = 20) +
title("The same network, given depth")plot(data(trade_partners),
layer(edge, layout(col.exporter, col.importer)), opacity(col.billions),
layer(point, layout(col.exporter, col.importer)), size(col.degree),
network({ turn: 35, tilt: 20 }), title("The same network, given depth"))“Given trade partners: edges derived by layout from exporter to importer, opacity by billions, and also points derived by layout from exporter to importer, size by degree, in the network, turned 35, tilted 20.”
Nothing in the table gives a node a third position, so there is nothing to bind to z. The third dimension belongs to the space, exactly as the viewing angle does, so asking for the angle is asking for the cube. The box is drawn for depth and carries no numbers, for the same reason the flat form has no axes.
In the web edition the cube turns with a drag, the way the cube in Space turns, and reset returns to the angle the sentence asked for. On paper it keeps the view it was given.
Naming a node in the cube is valid grammar. This engine does not draw those names:
data(trade_partners) +
edge * layout(exporter, importer) +
text * layout(exporter, importer) * repel + label(name) +
network(turn = 35, tilt = 20)(data(trade_partners) +
edge * layout(col.exporter, col.importer) +
text * layout(col.exporter, col.importer) * repel + label(col.name) +
network(turn = 35, tilt = 20))data(trade_partners) + edge * layout(:exporter, :importer) +
text * layout(:exporter, :importer) * repel + label(:name) +
network(turn = 35, tilt = 20)plot(data(trade_partners),
layer(edge, layout(col.exporter, col.importer)),
layer(text, layout(col.exporter, col.importer), repel), label(col.name),
network({ turn: 35, tilt: 20 }))Error:
! gog: names in the network's cube are valid grammar this engine does not draw yet. Drop the `text` layer, or drop the viewing angle and label the flat form.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
37.5 What the network refuses
The likeliest mistake is to leave network() off, since a flat plot needs no word for its space. A layout outside its space would draw axes, and the numbers along them would mean nothing:
data(trade_partners) + edge * layout(exporter, importer)data(trade_partners) + edge * layout(col.exporter, col.importer)data(trade_partners) + edge * layout(:exporter, :importer)plot(data(trade_partners),
layer(edge, layout(col.exporter, col.importer)))Error:
! gog: a `layout`'s positions are the graph's, not the data's — an axis under them would number something no reader can mean. Draw it in the network: `+ network()` flat, or `+ network(turn = 30, tilt = 25)` for the cube.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
Only three marks read a layout. Write any other mark and the refusal names all three:
data(trade_partners) + bar * layout(exporter, importer) + network()data(trade_partners) + bar * layout(col.exporter, col.importer) + network()data(trade_partners) + bar * layout(:exporter, :importer) + network()plot(data(trade_partners), layer(bar, layout(col.exporter, col.importer)),
network())Error:
! gog: `layout` places a graph — a position per node, computed from an edge table — and `bar` has no reading for that. Three marks do: `edge * layout(<from>, <to>)` draws the connections, `point * layout(...)` the nodes, and `text * layout(...) + label(name)` names them, all inside `network()`.
gog: `bar` does not stand in `network()` — it reads an axis, and this space has none. The layout's readers are `edge`, `point` and `text`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The layout has already placed every node, so x() has nothing left to set:
data(trade_partners) + x(billions) +
edge * layout(exporter, importer) + network()(data(trade_partners) + x(col.billions) +
edge * layout(col.exporter, col.importer) + network())data(trade_partners) + x(:billions) +
edge * layout(:exporter, :importer) + network()plot(data(trade_partners), x(col.billions),
layer(edge, layout(col.exporter, col.importer)), network())Error:
! gog: under `layout` the nodes place themselves, so `x(...)` has nothing left to say. Remove it — the third dimension, when you want it, is the space's: `network(turn = 30, tilt = 25)`.
gog: `x` cannot be bound to `edge` — an edge has no x feature. Remove the `billions` mapping from `x`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
And network() with no layout has nothing to place, so it refuses:
data(trade_partners) + point + network()data(trade_partners) + point + network()data(trade_partners) + point + network()plot(data(trade_partners), point, network())Error:
! gog: `network()` draws what a `layout` places, and nothing here carries one. Compose the syllable: `edge * layout(from, to) + network()`, with `point * layout(...)` for the nodes and `text * layout(...) + label(name)` for their names.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
37.6 Where the limits are
A row from a node to itself is refused. Such a stroke would have no length, and a fact about one node belongs on the node. A row missing an endpoint is left out, and the engine says how many it left out.
gog does not read a second table of node facts. A node shows only what the edges give it: its name and its degree. Joining more columns onto the nodes is a table operation, so do it in the host language, before the plot. A brush is refused too, and its answer is also upstream: a brush names a range on the data’s axes, and a network has none. Select the rows in your table first, then color the edges they make.