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, and asks you to produce the thing itself. It is the harder of the two, and it is what writing a specification asks of you.

This chapter builds three sentences from three questions, the way you will actually work: start with the smallest legal sentence, look, refine. Then it introduces the other half of writing: what happens when a sentence is wrong, and why gog’s refusals are the fastest teacher in the book.

4.1 Which continents live longest?

Start by looking at everything, the smallest sentence that shows the data:

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

“Points, x is continent, y is life.” Informative, but the question asked for a comparison of typical values, not a cloud. Derive the summary, swap the mark’s derivation, keep everything else:

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

Comparison wants order. order() sorts a categorical axis by a value, something no bare column can say:

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

Three steps, and each step added a word or swapped one: no rewriting, no second syntax. That is what compositionality buys you in practice.

4.2 How do petal lengths differ by species?

The one-variable question first, then the split:

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
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

And if the overlap bothers you, the outline form is one mark away; the staircase silhouette holds each bin’s height as an unfilled stroke, so the three species never paint over 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

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

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

The full sentence from the first chapter, written in one breath this time:

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, titled.” Six atoms past the minimum, and still one sentence with no new grammar in it.

4.4 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 absorbed into a silently wrong picture. Meet the refusals you are most likely to see this week.

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 + ...`.

Quoting a column name, a habit from other packages. The refusal says which fix it wants:

data(gapminder_2007) + point + x("gdp") + y("life")
Error:
! gog: `x("gdp")` is quoted, so it names a column called `gdp` — and there is no such column. Column names are written bare: `x(gdp)`. If you meant a fixed value rather than a column, that is `style()`.
gog: `y("life")` is quoted, so it names a column called `life` — and there is no such column. Column names are written bare: `y(life)`. If you meant a fixed value rather than a column, that is `style()`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

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)
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.

Asking a transform to read a column you never named:

data(gapminder_2007) + bar * mean + x(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)
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 way forward. Nothing rendered behind your back; nothing was dropped to make the picture come out. When you hit a refusal outside this list, read it the same way: it is the grammar telling you which law your sentence broke, and the fix is in the message.

Every refusal closes with a second offer, to set GOG_STRICT=0 and draw the plot anyway. That is an environment variable, and what it does to each kind of refusal (along with the one line that sets it in R, Python, Julia and JavaScript) is Drawing it anyway. For now, take the first offer: at this stage the refusals are the lesson.

4.5 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 did its job. Part II gives every letter its full chapter; or jump straight to the cookbook and start answering your own questions, following the links back whenever a recipe uses a word you want more of.