| 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 an edge, a stroke between two nodes.
Nothing in such a table says where any node should be placed. Choosing those positions is the problem, and this chapter shows how gog solves 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 this table raises is who trades with whom. One stroke per row answers exactly that. Write the edge mark derived by layout(), give it the from column first and the to column second, and add 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 row of the table becomes one stroke. Five pairs of countries trade in both directions, and each pair’s two strokes lie on one line. So you can see only 17 separate strokes for the 22 rows. The layout reads both columns and makes one node for each distinct name. The engine then pushes every node away from every other node, and pulls the two nodes of each row toward each other. Two countries that trade are therefore placed near each other. The positions are computed by the engine that all four languages share, never by the language you write in. So this sentence draws the same picture in R, Python, Julia and JavaScript, and the four image files are identical.
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. gog therefore draws a layout only inside network(), and refuses it in every other space.
37.2 The nodes and their names
The strokes alone do not say which node is which country, or which countries have the most trade relations. Two more layers finish the diagram. A point layer draws the nodes, and a text layer names them. Each one carries the same layout, and the engine computes it once per layer. The layout uses no random numbers, so all three layers get the same positions. That is why every dot is exactly where its strokes end.
The layout also computes two columns for the nodes: name, and degree, the number of rows the node appears in. A pair that trades both ways counts twice, so China, which trades with 7 countries, has a degree of 9. size(degree) then draws a node larger when it appears in more rows. The text layer also composes repel, the collision modifier that moves each label off the dots and off the other labels. So a name is just outside 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, sized by degree, and also text derived by layout from exporter to importer and repel, label by name, in the network.”
China, the United States and Germany have the largest dots, and they are near the middle. No row of the table says that these three have the most trade relations. The layout found them by counting each country’s rows.
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. Each edge is drawn from one row, so its color, its dash and its opacity can come from that row’s own columns. color and pattern need a categorical column, and opacity needs a continuous one. opacity(billions) maps each stroke’s opacity to that row’s billions, so a larger trade draws a darker stroke. A pair that trades both ways is two strokes on one line, so it looks darker than either row alone:
data(trade_partners) +
edge * layout(exporter, importer) + opacity(billions) +
point * layout(exporter, importer) + size(degree) +
text * layout(exporter, importer) * repel + label(name) +
network() +
title("More 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("More 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("More 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("More 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, sized by degree, and also text derived by layout from exporter to importer and repel, label by name, in the network.”
A node is different. It stands for every row it appears in, and those rows can hold different values. So the point and text layers map only name and degree, and gog refuses any other column there. Every other column belongs on the edge layer, where one row carries one value.
37.4 An angle asks for the cube
Every picture so far is flat. A plane gives the layout two directions for placing its nodes. A cube gives it three, and a cube can be turned. A crowded network has more room in three directions, and turning the cube shows which strokes pass in front of the others. 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. The sentence below leaves out the names, so the shape of the network is easy to see:
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, sized 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. The box outlines the cube, so you can see how it is turned. It has no numbers along its edges, because a layout’s positions are not measurements. The flat form has no axes for the same reason.
In the web edition the cube turns with a drag, the way the cube in Space turns. The reset button returns it to the angle the sentence asked for. In the PDF it keeps the view it was given.
A text layer names the nodes in the cube as it does on the flat page. Each name is drawn at its node, and repel moves the names off each other and off the dots:
data(trade_partners) +
edge * layout(exporter, importer) + opacity(billions) +
point * layout(exporter, importer) + size(degree) +
text * layout(exporter, importer) * repel + label(name) +
network(turn = 35, tilt = 20) +
title("The same network, named in the cube")(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(turn = 35, tilt = 20) +
title("The same network, named in the cube"))data(trade_partners) + edge * layout(:exporter, :importer) +
opacity(:billions) + point * layout(:exporter, :importer) +
size(:degree) + text * layout(:exporter, :importer) * repel +
label(:name) + network(turn = 35, tilt = 20) +
title("The same network, named in the cube")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({ turn: 35, tilt: 20 }),
title("The same network, named in the cube"))“Given trade partners: edges derived by layout from exporter to importer, opacity by billions, and also points derived by layout from exporter to importer, sized by degree, and also text derived by layout from exporter to importer and repel, label by name, in the network, turned 35, tilted 20.”
Each country is named beside its node, and the names do not cover the dots.
37.5 What the network refuses
Each refusal in this section is a mistake in the sentence. The most likely mistake is to leave network() off, because most plots need no space at all. Without it the plot would draw axes under the layout, 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: 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: 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 sentence can be right, and the table can still hold rows a layout cannot use. A row from a node to itself is refused, because such a stroke would have no length. The first row of this small table goes from China to China:
self_trade <- data.frame(exporter = c("China", "Mexico"),
importer = c("China", "United States"),
billions = c(500, 455))
data(self_trade) + edge * layout(exporter, importer) + network()self_trade = {
"exporter": ["China", "Mexico"],
"importer": ["China", "United States"],
"billions": [500, 455],
}
data(self_trade) + edge * layout(col.exporter, col.importer) + network()self_trade = (
exporter = ["China", "Mexico"],
importer = ["China", "United States"],
billions = [500, 455],
)
data(self_trade) + edge * layout(:exporter, :importer) + network()const self_trade = {
exporter: ["China", "Mexico"],
importer: ["China", "United States"],
billions: [500, 455],
};
plot(data(self_trade), layer(edge, layout(col.exporter, col.importer)),
network())Error:
! gog: 1 row(s) of `self_trade` connect a node to itself, and a stroke from a point to the same point has no extent. Remove those rows in the host language: an edge joins two different nodes.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
Remove the rows that connect a node to itself before you draw the plot. A row with a missing value in the from or the to column is not refused: it draws no stroke, and a message says how many rows were left out.
You may know more about each country than the edges say, such as its continent. gog does not read a second table describing the nodes, so a node shows only what the edges give it: its name and its degree. You can still show such a fact, but only on the edges. Before you draw the plot, join it onto each row in the host language, as a column such as the exporter’s continent. Then map that column on the edge layer.
You may want to drag a rectangle over part of the picture and select what it covers. A brush is refused. A brush selects a range of values on the plot’s axes, and a network has no axes. The fix is again in your table: add a column that marks the rows you want, and map it with color() on the edge layer.
If your table also has a year column, you may want to show how trade changes. For most plots, play does that: it draws one frame per year and shows the frames in order. In a network, play is valid grammar that this engine does not draw.
Instead, split the plot by year with | facet(year), which draws one panel per year. All panels share one layout, so a country is in the same place in every panel where it appears. Each panel draws only the edges from its own rows. An edge missing from a later panel is a trade relation that ended.
37.7 What comes next
A network draws one table at one moment. The section above suggested panels for comparing years, because this engine does not play a network. Most other plots can be played. Play adds time to a plot: it cuts the plot into frames and runs them in order, so the reader watches the picture change.