4  Writing practice

How does a question about your data turn into a specification? Reading was recognition; writing is recall. Psychologists use those two words for two kinds of remembering. Recognition starts from something in front of you, and asks only whether you have seen it before. That is what the last chapter did: it gave you finished sentences and asked what they say. Recall starts from nothing in front of you: you produce the answer yourself. It is the harder of the two, and it is what writing a specification asks of you.

This chapter works in the recall direction from here on. Three worked examples come first, showing the method you will use: start with the minimum sentence, look, refine. Five questions follow with nothing in front of you, each answered by a sentence you met in the last chapter: write yours before you scroll, then check it against the printed one. Last comes the other half of writing: what happens when a sentence is wrong, and how quickly a refusal teaches the rule.

4.1 Which continents live longest?

Before reading on, try to write the sentence that will answer it: which mark, which transform, which positions? Then build it the way this chapter always will. Start with the minimum sentence, the smallest one that shows the data:

gapminder_2007: first 5 of 142 rows
country continent year life population gdp
Afghanistan Asia 2007 43.828 31889923 974.5803
Albania Europe 2007 76.423 3600523 5937.0295
Algeria Africa 2007 72.301 33333216 6223.3675
Angola Africa 2007 42.731 12420476 4797.2313
Argentina Americas 2007 75.320 40301927 12779.3796
data(gapminder_2007) + point + x(continent) + y(life)
data(gapminder_2007) + point + x(col.continent) + y(col.life)
data(gapminder_2007) + point + x(:continent) + y(:life)
plot(data(gapminder_2007), point, x(col.continent), y(col.life))
Asia Europe Africa Americas Oceania 40 50 60 70 80 Life Continent

“Given gapminder 2007: points, x is continent, y is life.” Informative, but the question asked for a comparison of typical values, and this shows every country. Swap the mark and give it a derivation; everything else stays:

data(gapminder_2007) + bar * mean + x(continent) + y(life)
data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life)
data(gapminder_2007) + bar * mean + x(:continent) + y(:life)
plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
  y(col.life))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Life Continent

“Given gapminder 2007: bars derived by mean, x is continent, y is life.”

A comparison reads best in order. A categorical column can carry a fixed order, declared where the data is made. The order wanted here depends on the means this plot just computed, so only the sentence can ask for it: order() sorts the axis by the column you name:

data(gapminder_2007) + bar * mean + x(continent) + y(life) +
  order(life, desc = TRUE)
(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life) +
  order(col.life, desc = True))
data(gapminder_2007) + bar * mean + x(:continent) + y(:life) +
  order(:life, desc = true)
plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
  y(col.life), order(col.life, { desc: true }))
Oceania Europe Americas Asia Africa 0 20 40 60 80 Life Continent

“Given gapminder 2007: bars derived by mean, x is continent, y is life, ordered by life, largest first.”

Three steps, and no step rewrote the sentence: words were added or swapped, never restructured. That is what composition gives you: each step keeps every word you already wrote.

4.2 How do petal lengths differ by species?

The last chapter drew both of these. Write the two sentences from memory: the one-variable question first, then the same question split by species.

iris_flowers: first 5 of 150 rows
sepal_length sepal_width petal_length species
5.1 3.5 1.4 setosa
4.9 3.0 1.4 setosa
4.7 3.2 1.3 setosa
4.6 3.1 1.5 setosa
5.0 3.6 1.4 setosa
data(iris_flowers) + bar * bin + x(petal_length)
data(iris_flowers) + bar * bin + x(col.petal_length)
data(iris_flowers) + bar * bin + x(:petal_length)
plot(data(iris_flowers), layer(bar, bin), x(col.petal_length))
2 4 6 0 10 20 30 40 Count Petal Length

“Given the iris flowers: bars derived by bin, x is petal length.”

data(iris_flowers) + bar * bin + x(petal_length) + color(species)
data(iris_flowers) + bar * bin + x(col.petal_length) + color(col.species)
data(iris_flowers) + bar * bin + x(:petal_length) + color(:species)
plot(data(iris_flowers), layer(bar, bin), x(col.petal_length),
  color(col.species))
2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica

“Given the iris flowers: bars derived by bin, x is petal length, color by species.”

If the overlapping bars are hard to compare, one mark changes that: step draws each bin’s height as an unfilled line, so the three species never cover each other:

data(iris_flowers) + step * bin + x(petal_length) + color(species)
data(iris_flowers) + step * bin + x(col.petal_length) + color(col.species)
data(iris_flowers) + step * bin + x(:petal_length) + color(:species)
plot(data(iris_flowers), layer(step, bin), x(col.petal_length),
  color(col.species))
2 4 6 0 10 20 30 40 Count Petal Length Species setosa versicolor virginica

“Given the iris flowers: step outlines derived by bin, x is petal length, color by species.”

The transform never changed. bin cut the same intervals all three times; only the mark drawing them changed. A derivation you learn once works with every mark that can take it.

4.3 Where is the world’s wealth, and its health?

Income and life expectancy fill both positions. A reader also wants to know how many people each dot stands for. Chapter one built this sentence step by step. Write it in one pass now, adding size for population, then check:

data(gapminder_2007) + point +
  x(gdp, scale = "log") + y(life) +
  color(continent) + size(population) +
  title("The health and wealth of nations, 2007")
(data(gapminder_2007) + point +
  x(col.gdp, scale = "log") + y(col.life) +
  color(col.continent) + size(col.population) +
  title("The health and wealth of nations, 2007"))
data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
  color(:continent) + size(:population) +
  title("The health and wealth of nations, 2007")
plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
  y(col.life), color(col.continent), size(col.population),
  title("The health and wealth of nations, 2007"))
1K 10K 40 50 60 70 80 The health and wealth of nations, 2007 Life Gdp Continent Asia Europe Africa Americas Oceania Population 199.6K 659.4M 1.3B

“Given gapminder 2007: points, x is gdp on a log scale, y is life, color by continent, size by population.” Three atoms added to the minimum sentence (color, size and title; the log scale is written inside x rather than counting as a word), and still no new grammar in it.

4.4 Five questions, nothing in front of you

Each question below matches one plot from the last chapter. Write your sentence first; the printed one is there to check against, not to read first.

Which countries won the most gold? Every country name has to stay readable, so lay the bars down.

medals: all 5 rows
country gold silver bronze
USA 46 37 38
China 38 31 22
Great Britain 29 17 19
Russia 19 18 9
Germany 17 10 15
data(medals) + bar + x(gold) + y(country)
data(medals) + bar + x(col.gold) + y(col.country)
data(medals) + bar + x(:gold) + y(:country)
plot(data(medals), bar, x(col.gold), y(col.country))
0 10 20 30 40 Germany Russia Great Britain China USA Country Gold

“Given the medals: bars, x is gold, y is country.” One swap of the two bindings lays the bars down, and no new word was needed.

How is life expectancy distributed, drawn as a smooth curve rather than counted into bins?

data(gapminder_2007) + line * density + x(life)
data(gapminder_2007) + line * density + x(col.life)
data(gapminder_2007) + line * density + x(:life)
plot(data(gapminder_2007), layer(line, density), x(col.life))
40 60 80 0.00 0.01 0.02 0.03 0.04 Density Life

“Given gapminder 2007: a line derived by density, x is life.” The histogram’s sibling: one atom swapped, same question.

How did life expectancy move across the decades in the five Asian countries, one line per country?

gapminder_asia: first 5 of 60 rows
country continent year life population gdp
China Asia 1952 44.00000 556263527 400.4486
China Asia 1957 50.54896 637408000 575.9870
China Asia 1962 44.50136 665770000 487.6740
China Asia 1967 58.38112 754550000 612.7057
China Asia 1972 63.11888 862030000 676.9001
data(gapminder_asia) + line + x(year) + y(life) + color(country)
data(gapminder_asia) + line + x(col.year) + y(col.life) + color(col.country)
data(gapminder_asia) + line + x(:year) + y(:life) + color(:country)
plot(data(gapminder_asia), line, x(col.year), y(col.life),
  color(col.country))
1960 1980 2000 40 50 60 70 80 Life Year Country China India Indonesia Japan Korea, Rep.

“Given gapminder Asia: lines, x is year, y is life, color by country.” color splits first and colors second, exactly as it did in the last chapter.

Does the wealth and health relationship hold inside each continent, one panel per continent?

data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) | facet(continent)
data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) | facet(col.continent)
data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) |
  facet(:continent)
plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
  y(col.life), across(col.continent))
1K 10K 40 50 60 70 80 1K 10K 1K 10K 1K 10K 1K 10K Asia Europe Africa Americas Oceania Life Gdp

“Given gapminder 2007: points, x is gdp on a log scale, y is life, split into panel columns by continent.” The | and its facet, with scales shared across panels.

Do the three iris measurements separate the species when seen together?

data(iris_flowers) + point +
  x(sepal_length) + y(sepal_width) + z(petal_length) +
  color(species)
(data(iris_flowers) + point +
  x(col.sepal_length) + y(col.sepal_width) + z(col.petal_length) +
  color(col.species))
data(iris_flowers) + point + x(:sepal_length) + y(:sepal_width) +
  z(:petal_length) + color(:species)
plot(data(iris_flowers), point, x(col.sepal_length), y(col.sepal_width),
  z(col.petal_length), color(col.species))
8 7 6 5 4 3 2 6 4 2 Sepal Length Sepal Width Petal Length Species setosa versicolor virginica

“Given the iris flowers: points, x is sepal length, y is sepal width, z is petal length, color by species.” The cube again, and you wrote it yourself this time.

4.5 When the engine says no

You will write wrong sentences. Everyone does. gog’s design promise is that a wrong sentence is refused with a direction, never drawn with the wrong part quietly left out. Here are the refusals you are most likely to see first.

Forgetting to bind the table. The sentence must start with the data:

gapminder_2007 + point + x(gdp) + y(life)
Error:
! gog: a plot starts with `data()`, which names the table — columns are bare names and the nearest named table wins, so the name matters. Write `data(gapminder_2007) + point + ...`.

Each binding words that advice for its own spelling of a column, so this one refusal is shown in R alone.

Quoting a column name, a habit from other packages. The refusal names the fix:

data(gapminder_2007) + point + x("gdp") + y("life")
data(gapminder_2007) + point + x("gdp") + y("life")
data(gapminder_2007) + point + x("gdp") + y("life")
plot(data(gapminder_2007), point, x("gdp"), y("life"))
Error:
! gog: `x("gdp")` binds a *value*, and a channel takes a *column*: `x(gdp)` maps the column called `gdp`.

Mapping a channel to a column that cannot answer its question. size encodes magnitude, and a category has none:

data(gapminder_2007) + point + x(gdp) + y(life) + size(continent)
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + size(col.continent)
data(gapminder_2007) + point + x(:gdp) + y(:life) + size(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  size(col.continent))
Error:
! gog: `size(continent)` maps a categorical (text) column, but `size` on `point` needs a continuous (numeric) column. Use `color`, `shape`, or `pattern` to distinguish categories.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Forgetting the column a summary reads. The refusal names the missing y first, because the bar cannot stand without it:

data(gapminder_2007) + bar * mean + x(continent)
data(gapminder_2007) + bar * mean + x(col.continent)
data(gapminder_2007) + bar * mean + x(:continent)
plot(data(gapminder_2007), layer(bar, mean), x(col.continent))
Error:
! gog: `bar` needs `y()` but none is set. Add `y(<column>)` — a bar cannot be drawn without it.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Binning a category. bin cuts a continuous axis, and the refusal points at the atom that does count categories:

data(gapminder_2007) + bar * bin + x(continent)
data(gapminder_2007) + bar * bin + x(col.continent)
data(gapminder_2007) + bar * bin + x(:continent)
plot(data(gapminder_2007), layer(bar, bin), x(col.continent))
Error:
! gog: `bin` cuts a continuous axis into intervals, and `x(continent)` is categorical — a category is one slot, with no width to cut. To tally rows per category, `count` is the transform that does it: `bar * count`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Each message names the rule and the fix. Nothing was drawn without telling you, and no binding was dropped to complete the plot. Read any refusal outside this list the same way: it names the law the sentence broke, and it names the fix.

The last three refusals close with a second offer: set GOG_STRICT=0 and draw the plot anyway. The first two never reach the engine, so there is nothing to draw. GOG_STRICT is an environment variable, and Drawing it anyway shows what it does to each kind of refusal, with the one line that sets it in each language. For now, make the fix the message asks for: at this stage the refusals are the lesson.

4.6 The morning checkpoint

If you started this part in one sitting, check yourself against its promise:

  • Read any sentence in this book aloud before looking at its plot.
  • Given a question (distribution? comparison? relationship? change over time?), name the mark and transform that answer it.
  • Predict whether a sentence will render or refuse, and roughly what the refusal will say.

If that mostly works, the morning worked. Part II gives every letter its full chapter. Or go straight to the cookbook and answer your own questions, following a link back whenever a recipe uses a word you want explained.