4  Writing practice

How does a question about your data turn into a sentence? 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 Reading practice 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 sentence asks of you.

From here on, this chapter asks you to recall. Three worked examples come first. The first two show the method you will use: start with the minimum sentence, look, refine. The third asks you to write the whole sentence at once. Five questions follow, each answered by a sentence you met in Reading practice. For each, write your sentence before you look at the printed one, then check it. Last comes the other half of writing: what happens when a sentence is wrong, and what a refusal tells you to do next.

4.1 Which continents live longest?

This is the first of the three worked examples. Before reading on, try to write the sentence that answers the question above: which mark, which transform, which positions? Then build it one step at a time. 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.” The plot shows how each continent’s countries spread, but the question asked for a comparison of typical values, and this one shows every country. Swap point for bar, and attach the transform mean with *; 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.” Now there is one bar per continent, as tall as the mean life expectancy of its countries.

Bars are easiest to compare when they are sorted, with the largest first. A categorical column can carry a fixed order, declared when the table is built (Declaring the order of categories). The order you want here depends on the means the plot has just computed, so the table cannot carry it. The sentence has to ask for it: order(life) sorts the axis by the bars’ values for life, here the means, and desc = TRUE puts the largest first:

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

That took two changes: point became bar * mean, and order() was added. Neither rewrote the sentence. Each added or swapped words and left the rest in place, and every word you kept still means the same thing.

4.2 How do petal lengths differ by species?

Reading practice drew the first two plots below. 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.” Now split the same bars by species, with one word.

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 outline with no fill, 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. What changed was first color(species), which split the bars by species, and then the mark that draws them. A transform you learn once works with every mark that can take it.

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

The first two questions were each about one measurement. This one is about two, income and life expectancy, and they fill both positions. Someone reading the plot also wants to know how many people live in each country. A first plot built this sentence, without size, step by step. Write the whole sentence 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 were added to the minimum sentence: color, size and title. The log scale is written inside x, so it adds no word, and the sentence uses no word that the first chapter and the grammar chapter have not shown.

4.4 Five questions: write first, then check

Each question below matches one plot from Reading practice. 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 the bars should be horizontal.

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.” Compared with the first plot in Reading practice, the two bindings were swapped: gold on x, country on y. That makes the bars horizontal, 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.” A histogram answers the same question; here line replaces bar, and density replaces bin.

How did life expectancy change across the decades in the five Asian countries of gapminder_asia, 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 does two things here. It splits the rows into one line per country, then gives each line its own color, exactly as it did in Reading practice.

Do richer countries also live longer 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 | operator and facet split the plot into panels. All the panels share one pair of scales, so they can be compared; free = TRUE in a position gives each panel its own axis (Faceting).

Are the species apart when you see all three iris measurements 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.” This is the 3-D plot from the end of Reading practice, and this time you wrote it yourself.

4.5 When gog says no

Every sentence so far was correct. The other half of writing is what happens when one is not.

You will write wrong sentences. Everyone does. gog promises that a wrong sentence is refused with a message that says how to fix it, never drawn with the wrong part quietly removed. Here are five refusals that new users see most often.

Forgetting to bind the table. A sentence starts with data(), which names the table:

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

The message above is R’s. Python, Julia and JavaScript give the same advice, each writing a column the way that language does.

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

That message is R’s too. Python and JavaScript name the fix x(col.gdp), and Julia names it x(:gdp).

Mapping a channel to a column of the wrong type. size shows an amount, and a category has none, so the message suggests color or shape instead:

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` or `shape` 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, because mean must be told which column to average:

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 * mean` needs `y()`, because `mean` averages the column named there, and none is set. Add `y(<column>)` with that column. To count the rows in each slot instead, write `bar * count`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Binning a category. bin divides a continuous axis into intervals, and the refusal names count, the transform that tallies rows per category:

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. No wrong plot was drawn, and no binding was dropped to complete one. Read any refusal outside this list the same way.

The last three refusals end with a second option: set GOG_STRICT=0 and draw the plot anyway. The first two are caught by the language’s package before the engine sees the sentence, so there is nothing to draw. GOG_STRICT is an environment variable, and Drawing it anyway shows which refusals GOG_STRICT affects, with the one line that sets it in each language. For now, make the fix the message asks for. At this stage, reading a refusal and making its fix is the practice.

4.6 The morning checkpoint

That is the whole method: start with the minimum sentence, look, refine, and read the refusal when one comes. If you have read this part from the start, check yourself against the three skills A morning promised:

  • Read any gog sentence aloud before looking at its plot.
  • Given a question (distribution? comparison? relationship? change over time?), write a sentence that answers it.
  • Predict whether gog will draw a sentence or refuse it, and roughly what the refusal will say.

If you can do most of that, you have done what Part I set out to teach. You can read the next part in order, or go directly to the cookbook and answer your own questions, following a link back whenever a recipe uses a word you want explained.

4.7 What comes next

Part I taught how a sentence is built. It did not show everything a single word can do; Part II gives each kind of word chapters of its own. Its first chapter, Data, covers the table every sentence starts from: the shape gog needs, and how gog reads each column’s type.