16  Text

Which point is which? text draws a string, a piece of text such as a name or a number, at each (x, y) position. It is point’s sibling: both place one glyph per row. A point’s glyph is a dot by default. A text mark’s glyph is a string, and that string comes from a column, through the label channel. This chapter uses it three ways: to name the points of a scatter, to print a value on a bar, and to write a note on a plot.

16.1 The mark and its content

A point tells you where a row is; it cannot tell you which row. Five countries drawn as dots are five points with nothing to say which country is which:

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) + point + x(gold) + y(silver) +
  x_label("Gold") + y_label("Silver") +
  title("Five countries, and no names")
(data(medals) + point + x(col.gold) + y(col.silver) +
  x_label("Gold") + y_label("Silver") +
  title("Five countries, and no names"))
data(medals) + point + x(:gold) + y(:silver) + x_label("Gold") +
  y_label("Silver") + title("Five countries, and no names")
plot(data(medals), point, x(col.gold), y(col.silver), x_label("Gold"),
  y_label("Silver"), title("Five countries, and no names"))
20 30 40 10 20 30 Five countries, and no names Silver Gold

“Given the medals: points, x is gold, y is silver.”

Replace point with text and bind label to country, and each row’s name is drawn at its position:

data(medals) + text + x(gold) + y(silver) + label(country) +
  x_label("Gold") + y_label("Silver") +
  title("The same five, named")
(data(medals) + text + x(col.gold) + y(col.silver) + label(col.country) +
  x_label("Gold") + y_label("Silver") +
  title("The same five, named"))
data(medals) + text + x(:gold) + y(:silver) + label(:country) +
  x_label("Gold") + y_label("Silver") + title("The same five, named")
plot(data(medals), text, x(col.gold), y(col.silver), label(col.country),
  x_label("Gold"), y_label("Silver"), title("The same five, named"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 The same five, named Silver Gold

“Given the medals: text, x is gold, y is silver, label by country.”

That is the whole difference between the two marks. point draws the same dot at every row’s position. text draws one of the row’s own values there instead.

16.2 Both at once: the labeled scatter

A dot marks one position on the panel. A word is much wider than a dot, so a name on its own only shows roughly where its row is. The two marks can be drawn together: layer a point mark and a text mark on the same x and y, with the + operator. The dot gives the exact position and the string gives the name. That is a labeled scatter, built from two marks rather than a third mark of its own.

Each style() applies to the mark written before it, so style(size = 5, color = "#9e9e9e") colors the dots and not the words. Without a nudge, each label is drawn centered on its dot, so style(nudge = "up") moves every label above its dot:

data(medals) + point + style(size = 5, color = "#9e9e9e") +
  text + x(gold) + y(silver) + label(country) + style(nudge = "up") +
  x_label("Gold") + y_label("Silver") +
  title("Located by the dot, named just above")
(data(medals) + point + style(size = 5, color = "#9e9e9e") +
  text + x(col.gold) + y(col.silver) + label(col.country) + style(nudge = "up") +
  x_label("Gold") + y_label("Silver") +
  title("Located by the dot, named just above"))
data(medals) + point + style(size = 5, color = "#9e9e9e") + text +
  x(:gold) + y(:silver) + label(:country) + style(nudge = "up") +
  x_label("Gold") + y_label("Silver") +
  title("Located by the dot, named just above")
plot(data(medals), point, style({ size: 5, color: "#9e9e9e" }), text,
  x(col.gold), y(col.silver), label(col.country), style({ nudge: "up" }),
  x_label("Gold"), y_label("Silver"),
  title("Located by the dot, named just above"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 Located by the dot, named just above Silver Gold

“Given the medals: points and also text, x is gold, y is silver, label by country, with nudge up.”

16.2.1 Nudging the label off its point

Up is only one of four directions, and it is not always the side with room. A dot near the top of the panel, or one with a neighbor just above it, has no space for its name there. Those labels need a different direction. style(nudge = ) shifts a text label off its (x, y). It takes one direction: "up", "down", "left", or "right":

data(medals) + point + style(size = 5, color = "#9e9e9e") +
  text + x(gold) + y(silver) + label(country) + style(nudge = "right") +
  x_label("Gold") + y_label("Silver") +
  title("The same, nudged right")
(data(medals) + point + style(size = 5, color = "#9e9e9e") +
  text + x(col.gold) + y(col.silver) + label(col.country) + style(nudge = "right") +
  x_label("Gold") + y_label("Silver") +
  title("The same, nudged right"))
data(medals) + point + style(size = 5, color = "#9e9e9e") + text +
  x(:gold) + y(:silver) + label(:country) + style(nudge = "right") +
  x_label("Gold") + y_label("Silver") + title("The same, nudged right")
plot(data(medals), point, style({ size: 5, color: "#9e9e9e" }), text,
  x(col.gold), y(col.silver), label(col.country),
  style({ nudge: "right" }), x_label("Gold"), y_label("Silver"),
  title("The same, nudged right"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 The same, nudged right Silver Gold

You do not give a distance. The engine sets it from the font size, far enough that the word does not touch a dot of the default size. That is why nudge asks only for a direction. A nudge is one fixed shift, the same for every label, and nothing in the data changes it. jitter and dodge work differently: they compute each mark’s offset from the data, so marks can move by different amounts. Only text accepts a nudge. A point or a bar has no label to move, so the engine refuses the setting there and names the mark that does.

16.2.2 repel: moving labels off each other

A nudge shifts every label the same way, so it moves each label off its own dot and does nothing else. When the labels collide with each other, one fixed shift moves them all together and the words still overlap. Thirty European countries are enough to show it:

gm_europe: first 5 of 30 rows
country continent year life population gdp
Albania Europe 2007 76.423 3600523 5937.030
Austria Europe 2007 79.829 8199783 36126.493
Belgium Europe 2007 79.441 10392226 33692.605
Bosnia and Herzegovina Europe 2007 74.852 4552198 7446.299
Bulgaria Europe 2007 73.005 7322858 10680.793
data(gm_europe) + point + style(color = "#9e9e9e") +
  text + x(gdp) + y(life) + label(country) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("Thirty countries, named")
(data(gm_europe) + point + style(color = "#9e9e9e") +
  text + x(col.gdp) + y(col.life) + label(col.country) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("Thirty countries, named"))
data(gm_europe) + point + style(color = "#9e9e9e") + text + x(:gdp) +
  y(:life) + label(:country) + x_label("GDP per person") +
  y_label("Life expectancy") + title("Thirty countries, named")
plot(data(gm_europe), point, style({ color: "#9e9e9e" }), text,
  x(col.gdp), y(col.life), label(col.country), x_label("GDP per person"),
  y_label("Life expectancy"), title("Thirty countries, named"))
Albania Austria Belgium Bosnia and Herzegovina Bulgaria Croatia Czech Republic Denmark Finland France Germany Greece Hungary Iceland Ireland Italy Montenegro Netherlands Norway Poland Portugal Romania Serbia Slovak Republic Slovenia Spain Sweden Switzerland Turkey United Kingdom 10K 20K 30K 40K 50K 72 74 76 78 80 82 Thirty countries, named Life expectancy GDP per person

Every name is drawn on its own dot, and where the countries crowd together the words overlap. A nudge would move all thirty words the same distance in the same direction, so the same words would still overlap.

repel solves that. It is one of the four collision modifiers, with jitter, dodge and stack, so it is written after the mark with *, not inside style(). It measures where the labels and the dots actually are, then moves each label off the words around it:

data(gm_europe) + point + style(color = "#9e9e9e") +
  text * repel + x(gdp) + y(life) + label(country) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("The same thirty, moved apart")
(data(gm_europe) + point + style(color = "#9e9e9e") +
  text * repel + x(col.gdp) + y(col.life) + label(col.country) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("The same thirty, moved apart"))
data(gm_europe) + point + style(color = "#9e9e9e") + text * repel +
  x(:gdp) + y(:life) + label(:country) + x_label("GDP per person") +
  y_label("Life expectancy") + title("The same thirty, moved apart")
plot(data(gm_europe), point, style({ color: "#9e9e9e" }),
  layer(text, repel), x(col.gdp), y(col.life), label(col.country),
  x_label("GDP per person"), y_label("Life expectancy"),
  title("The same thirty, moved apart"))
Albania Austria Belgium Bosnia and Herzegovina Bulgaria Croatia Czech Republic Denmark Finland France Germany Greece Hungary Iceland Ireland Italy Montenegro Netherlands Norway Poland Portugal Romania Serbia Slovak Republic Slovenia Spain Sweden Switzerland Turkey United Kingdom 10K 20K 30K 40K 50K 72 74 76 78 80 82 The same thirty, moved apart Life expectancy GDP per person

“Given gapminder Europe: points and also text derived by repel, x is gdp, y is life, label by country.”

Notice three things in this plot. Every label is drawn just outside its dot, so no word covers the dot it names. A label with other words near it moves to whichever side has room. A label with space around it stays where it started, directly above its dot. The first two follow from one rule: repel pulls each label toward its own dot, and pushes it away from every other word and every dot, its own included.

In a more crowded plot, repel can move a label much further from its dot. Once the gap is larger than the height of one line of text, gog draws a thin line from the dot to the word, so you can still see which dot the name belongs to. No label in this plot moved that far, so none has a line.

The placement is deterministic. It uses no random-number generator and does not read the computer’s clock, so the same specification draws the same picture every time. It does depend on the order of the rows, though. repel settles the overlaps pair by pair, in the order of the rows, so the same rows in a different order can end with some labels in different places.

Sometimes no placement keeps every label apart. repel then still draws every label, and gog prints a message saying how many still overlap. Which names to leave out is your decision, so repel never removes one. In this plot all thirty fit, so gog prints no message.

Three things give crowded labels more room: fewer rows, a smaller style(size = ), and a larger plot. theme(width =, height =) sets the plot’s size in pixels, and the Composition chapter explains it.

Fewer rows is often the right choice. On a large table you usually name only the few rows that matter: give the text layer its own table that holds just those rows, as A note is a layer does with one row. In the web edition, clicking a dot on a plot with brush() shows its values beside it, which Selection calls a stamp. Stamps help you choose those rows. A stamp is gone when the page reloads, while a text label is part of the specification and is drawn every time.

In the next plot, the dots come from gapminder_2007, which holds all 142 countries. The names come from gm_populous, the ten countries with the most people in 2007. gm_populous has the same columns, so the text layer reads gdp and life from it:

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
gm_populous: first 5 of 10 rows
country continent year life population gdp
China Asia 2007 72.961 1318683096 4959.115
India Asia 2007 64.698 1110396331 2452.210
United States Americas 2007 78.242 301139947 42951.653
Indonesia Asia 2007 70.650 223547000 3540.652
Brazil Americas 2007 72.390 190010647 9065.801
data(gapminder_2007) + point + style(color = "#9e9e9e") +
  x(gdp, scale = "log") + y(life) +
  data(gm_populous) + text * repel + label(country) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("The ten most populous of 142 countries, named")
(data(gapminder_2007) + point + style(color = "#9e9e9e") +
  x(col.gdp, scale = "log") + y(col.life) +
  data(gm_populous) + text * repel + label(col.country) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("The ten most populous of 142 countries, named"))
data(gapminder_2007) + point + style(color = "#9e9e9e") +
  x(:gdp, scale = "log") + y(:life) + data(gm_populous) + text * repel +
  label(:country) + x_label("GDP per person") +
  y_label("Life expectancy") +
  title("The ten most populous of 142 countries, named")
plot(data(gapminder_2007), point, style({ color: "#9e9e9e" }),
  x(col.gdp, { scale: "log" }), y(col.life), data(gm_populous),
  layer(text, repel), label(col.country), x_label("GDP per person"),
  y_label("Life expectancy"),
  title("The ten most populous of 142 countries, named"))
China India United States Indonesia Brazil Pakistan Bangladesh Nigeria Japan Mexico 1K 10K 40 50 60 70 80 The ten most populous of 142 countries, named Life expectancy GDP per person

“Given gapminder 2007: points, x is gdp on a log scale, y is life, and also text from the ten most populous countries, derived by repel, label by country.”

The gdp axis uses a log scale (Scales), which gives the many poorer countries more room. repel moves the names off the 132 unnamed dots too, though they come from another table. It moves a name off those dots only a little, never far enough for gog to draw a thin line. So a name can still cover some of them: United States covers several at the top right, and Bangladesh covers two. A halo keeps such a name readable.

A nudge and repel can be used together, and each does one job. style(nudge = ) chooses the side each label is pulled toward, and repel then moves the labels that still overlap:

data(gm_europe) + point + style(color = "#9e9e9e") +
  text * repel + x(gdp) + y(life) + label(country) + style(nudge = "right") +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("Nudged right first, then moved apart")
(data(gm_europe) + point + style(color = "#9e9e9e") +
  text * repel + x(col.gdp) + y(col.life) + label(col.country) + style(nudge = "right") +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("Nudged right first, then moved apart"))
data(gm_europe) + point + style(color = "#9e9e9e") + text * repel +
  x(:gdp) + y(:life) + label(:country) + style(nudge = "right") +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("Nudged right first, then moved apart")
plot(data(gm_europe), point, style({ color: "#9e9e9e" }),
  layer(text, repel), x(col.gdp), y(col.life), label(col.country),
  style({ nudge: "right" }), x_label("GDP per person"),
  y_label("Life expectancy"),
  title("Nudged right first, then moved apart"))
Albania Austria Belgium Bosnia and Herzegovina Bulgaria Croatia Czech Republic Denmark Finland France Germany Greece Hungary Iceland Ireland Italy Montenegro Netherlands Norway Poland Portugal Romania Serbia Slovak Republic Slovenia Spain Sweden Switzerland Turkey United Kingdom 10K 20K 30K 40K 50K 72 74 76 78 80 82 Nudged right first, then moved apart Life expectancy GDP per person

“Given gapminder Europe: points and also text derived by repel, x is gdp, y is life, label by country, with nudge right.”

With every label pulled to the right of its dot, four labels run into their neighbors there, and repel moves those four far enough to need a thin line. Norway’s dot is at the right edge of the panel, so its label has no room on its right and is drawn over its own dot.

16.3 Number labels: the value on each mark

A label does not have to be a name, and it does not have to be drawn on a scatter. A bar shows which country won more. To read how much more, a reader has to measure each bar against the axis, and that measurement is only an estimate. Sometimes the number itself is the answer. Bind a column that holds numbers and each row’s value is drawn. By default, a whole number is printed without a decimal point, and any other number is rounded to at most three decimal places. Layer text over bar to print each bar’s height. Without a nudge, each number is centered on its bar’s top edge, and the edge crosses the digits. style(nudge = "up") moves each number just above its bar:

data(medals) + bar + style(color = "steelblue") +
  text + x(country) + y(gold) + label(gold) + style(nudge = "up") +
  y_label("Gold medals") +
  title("Each bar labeled with its height")
(data(medals) + bar + style(color = "steelblue") +
  text + x(col.country) + y(col.gold) + label(col.gold) + style(nudge = "up") +
  y_label("Gold medals") +
  title("Each bar labeled with its height"))
data(medals) + bar + style(color = "steelblue") + text + x(:country) +
  y(:gold) + label(:gold) + style(nudge = "up") + y_label("Gold medals") +
  title("Each bar labeled with its height")
plot(data(medals), bar, style({ color: "steelblue" }), text,
  x(col.country), y(col.gold), label(col.gold), style({ nudge: "up" }),
  y_label("Gold medals"), title("Each bar labeled with its height"))
46 38 29 19 17 USA China Great Britain Russia Germany 0 10 20 30 40 Each bar labeled with its height Gold medals Country

“Given the medals: bars and also text, x is country, y is gold, label by gold, with nudge up.”

Each bar now carries its number, so the difference between the USA’s 46 and China’s 38 can be read without the axis.

The medals table already holds the numbers. A bar chart that counts rows computes its numbers itself. In bar * count, the count transform writes the number of rows in each bar into a column called count. text * count uses the same transform, so label(count) can print that column on each bar. As in the medals plot, the numbers are nudged up:

data(gapminder_2007) + bar * count + x(continent) +
  text * count + label(count) + style(nudge = "up") +
  title("Each bar labeled with its count")
(data(gapminder_2007) + bar * count + x(col.continent) +
  text * count + label(col.count) + style(nudge = "up") +
  title("Each bar labeled with its count"))
data(gapminder_2007) + bar * count + x(:continent) + text * count +
  label(:count) + style(nudge = "up") +
  title("Each bar labeled with its count")
plot(data(gapminder_2007), layer(bar, count), x(col.continent),
  layer(text, count), label(col.count), style({ nudge: "up" }),
  title("Each bar labeled with its count"))
33 30 52 25 2 Asia Europe Africa Americas Oceania 0 20 40 Each bar labeled with its count Count Continent

“Given gapminder 2007: bars derived by count, x is continent, and also text derived by count, label by count, with nudge up.”

Africa’s bar is labeled 52, and Oceania’s bar is labeled 2.

The same sentence labels horizontal bars, with the category on y instead of x. The bars grow to the right. The text layer reads the same positions, so each number is centered on the right end of its bar. style(nudge = "right") moves it just past that end:

data(gapminder_2007) + bar * count + y(continent) +
  text * count + label(count) + style(nudge = "right")
(data(gapminder_2007) + bar * count + y(col.continent) +
  text * count + label(col.count) + style(nudge = "right"))
data(gapminder_2007) + bar * count + y(:continent) + text * count +
  label(:count) + style(nudge = "right")
plot(data(gapminder_2007), layer(bar, count), y(col.continent),
  layer(text, count), label(col.count), style({ nudge: "right" }))
33 30 52 25 2 0 20 40 Oceania Americas Africa Europe Asia Continent Count

Africa’s 52 is past the end of the longest bar, and Oceania’s 2 is past the shortest. The axis’s margin already holds Africa’s 52. A label that would not fit inside the panel widens its axis instead (Scales).

A histogram is labeled the same way. The bin transform also writes a column called count, the number of rows in each bin. So text * bin with label(count) prints how many countries fall in each bin of life expectancy:

data(gapminder_2007) + bar * bin + x(life) +
  text * bin + label(count) + style(nudge = "up")
(data(gapminder_2007) + bar * bin + x(col.life) +
  text * bin + label(col.count) + style(nudge = "up"))
data(gapminder_2007) + bar * bin + x(:life) + text * bin + label(:count) +
  style(nudge = "up")
plot(data(gapminder_2007), layer(bar, bin), x(col.life), layer(text, bin),
  label(col.count), style({ nudge: "up" }))
8 9 11 11 9 10 27 25 32 50 60 70 80 0 10 20 30 Count Life

“Given gapminder 2007: bars derived by bin, x is life, and also text derived by bin, label by count, with nudge up.”

The last bin, from about 78 to 83 years, holds the most countries: 32.

Add proportion to both layers, and each bar is a share of all the countries instead of a count. proportion turns each count into a share, so the rows hold a column called proportion and no count. The text layer therefore names label(proportion):

data(gapminder_2007) + bar * bin * proportion + x(life) +
  text * bin * proportion + label(proportion, decimals = 2) +
  style(nudge = "up")
(data(gapminder_2007) + bar * bin * proportion + x(col.life) +
  text * bin * proportion + label(col.proportion, decimals = 2) +
  style(nudge = "up"))
data(gapminder_2007) + bar * bin * proportion + x(:life) +
  text * bin * proportion + label(:proportion, decimals = 2) +
  style(nudge = "up")
plot(data(gapminder_2007), layer(bar, bin, proportion), x(col.life),
  layer(text, bin, proportion), label(col.proportion, { decimals: 2 }),
  style({ nudge: "up" }))
0.06 0.06 0.08 0.08 0.06 0.07 0.19 0.18 0.23 50 60 70 80 0.0 0.1 0.2 Proportion Life

“Given gapminder 2007: bars derived by bin and proportion, x is life, and also text derived by bin and proportion, label by proportion with 2 decimal places, with nudge up.”

The last bin is now labeled 0.23. Its 32 countries make a share of 0.2254 of the 142, and decimals = 2 rounds the share to two decimal places, as Number labels explains. These rows hold no count, so label(count) is refused, and the message names the transform that replaced it:

data(gapminder_2007) + bar * bin * proportion + x(life) +
  text * bin * proportion + label(count)
(data(gapminder_2007) + bar * bin * proportion + x(col.life) +
  text * bin * proportion + label(col.count))
data(gapminder_2007) + bar * bin * proportion + x(:life) +
  text * bin * proportion + label(:count)
plot(data(gapminder_2007), layer(bar, bin, proportion), x(col.life),
  layer(text, bin, proportion), label(col.count))
Error:
! gog: `label(count)` names the `count` that `bin` writes, and `proportion` then turns each count into a share, so the rows `text * bin * proportion` draws hold `proportion` and no `count`. `label(proportion)` reads the share. To read the counts, drop `proportion` from every layer that has it.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

After count, bin or proportion there is one row for each bar. A label can print only a column those rows hold. In bar * count + x(continent), those rows hold two columns, count and continent. Each row is a whole continent, so it holds no country to print:

data(gapminder_2007) + bar * count + x(continent) +
  text * count + label(country)
(data(gapminder_2007) + bar * count + x(col.continent) +
  text * count + label(col.country))
data(gapminder_2007) + bar * count + x(:continent) + text * count +
  label(:country)
plot(data(gapminder_2007), layer(bar, count), x(col.continent),
  layer(text, count), label(col.country))
Error:
! gog: `label(country)` names a column of your table, and `text * count` replaces your rows with its own, one for each `continent`, which have no `country`. `label(count)` prints each count, and `label(continent)` names each one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

After a summary such as mean, there is also one row for each bar. In bar * mean + x(continent) + y(life), each of those rows holds its continent’s mean, written into life itself. So text * mean with label(life) prints each continent’s mean life expectancy:

data(gapminder_2007) + bar * mean + x(continent) + y(life) +
  text * mean + label(life) + style(nudge = "up")
(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life) +
  text * mean + label(col.life) + style(nudge = "up"))
data(gapminder_2007) + bar * mean + x(:continent) + y(:life) +
  text * mean + label(:life) + style(nudge = "up")
plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
  y(col.life), layer(text, mean), label(col.life), style({ nudge: "up" }))
70.728 77.649 54.806 73.608 80.719 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, and also text derived by mean, label by life, with nudge up.”

Asia’s mean, 70.72848, is printed as 70.728. For sum, median, max, min and quantile, write label() with the column the summary reads.

Asia’s label, 70.728, has more decimal places than this plot needs. You cannot round a mean before gog computes it, so label() takes the number of decimal places to print. label(life, decimals = 1) prints Asia’s mean as 70.7:

data(gapminder_2007) + bar * mean + x(continent) + y(life) +
  text * mean + label(life, decimals = 1) + style(nudge = "up")
(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life) +
  text * mean + label(col.life, decimals = 1) + style(nudge = "up"))
data(gapminder_2007) + bar * mean + x(:continent) + y(:life) +
  text * mean + label(:life, decimals = 1) + style(nudge = "up")
plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
  y(col.life), layer(text, mean), label(col.life, { decimals: 1 }),
  style({ nudge: "up" }))
70.7 77.6 54.8 73.6 80.7 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, and also text derived by mean, label by life with 1 decimal place, with nudge up.”

Each label now has one decimal place, from Africa’s 54.8 to Oceania’s 80.7. Every label gets exactly one decimal place, so a mean of exactly 77 would print as 77.0, and all five labels show the same precision. decimals = 0 rounds each mean to a whole number, so Asia’s prints as 71. gog reads a categorical column as names, not numbers, so label(country, decimals = 1) is refused.

A sum can be a very large number. bar * sum with y(population) adds up the people of each continent, and label(population) prints Asia’s sum in full, as 3811953827. A number that long is hard to read. label(population, short = TRUE) prints it short, with K for thousands, M for millions, B for billions (a thousand millions) and T for trillions (a million millions):

data(gapminder_2007) + bar * sum + x(continent) + y(population) +
  text * sum + label(population, short = TRUE) + style(nudge = "up")
(data(gapminder_2007) + bar * sum + x(col.continent) + y(col.population) +
  text * sum + label(col.population, short = True) + style(nudge = "up"))
data(gapminder_2007) + bar * sum + x(:continent) + y(:population) +
  text * sum + label(:population, short = true) + style(nudge = "up")
plot(data(gapminder_2007), layer(bar, sum), x(col.continent),
  y(col.population), layer(text, sum),
  label(col.population, { short: true }), style({ nudge: "up" }))
3.81B 586M 930M 899M 24.5M Asia Europe Africa Americas Oceania 0 1B 2B 3B 4B Population Continent

“Given gapminder 2007: bars derived by sum, x is continent, y is population, and also text derived by sum, label by population written short, with nudge up.”

Asia’s label is now 3.81B, and Oceania’s is 24.5M. A short number keeps three significant digits. decimals sets the number of decimal places instead, so label(population, short = TRUE, decimals = 1) prints 3.8B for Asia. An axis takes short too: y(population, short = TRUE) takes each tick’s letter from the tick’s own size, as Scales shows. On this plot it would change nothing: the top tick is already 4B.

A share can be printed as a percent. label(proportion, percent = TRUE) prints the last bin’s share, 0.2254, as 22.5%:

data(gapminder_2007) + bar * bin * proportion + x(life) +
  y(proportion, percent = TRUE) +
  text * bin * proportion + label(proportion, percent = TRUE) +
  style(nudge = "up")
(data(gapminder_2007) + bar * bin * proportion + x(col.life) +
  y(col.proportion, percent = True) +
  text * bin * proportion + label(col.proportion, percent = True) +
  style(nudge = "up"))
data(gapminder_2007) + bar * bin * proportion + x(:life) +
  y(:proportion, percent = true) + text * bin * proportion +
  label(:proportion, percent = true) + style(nudge = "up")
plot(data(gapminder_2007), layer(bar, bin, proportion), x(col.life),
  y(col.proportion, { percent: true }), layer(text, bin, proportion),
  label(col.proportion, { percent: true }), style({ nudge: "up" }))
5.6% 6.3% 7.7% 7.7% 6.3% 7% 19% 17.6% 22.5% 50 60 70 80 0% 10% 20% Proportion Life

“Given gapminder 2007: bars derived by bin and proportion, x is life, y is proportion written as a percent, and also text derived by bin and proportion, label by proportion written as a percent, with nudge up.”

The axis takes the same argument on y(proportion), so the axis and the labels are written the same way. Without decimals, a percent keeps one decimal place and leaves it off when it is 0, so a share of 0.190 prints as 19%, not 19.0%. percent and short cannot be used together, because they are two ways to write one number.

label() prints a column of dates as dates, such as 2024-03-01, and a column of date-times with the hour and minute, such as 2024-03-01 14:05. A date is not written as a number, so gog refuses the three arguments on it.

After bin * cumulative, each bin holds two numbers: its own count, and the total so far. cumulative writes the total to a column named cumulative, so label(cumulative) prints the height of each bar:

data(gapminder_2007) + bar * bin * cumulative + x(life) +
  text * bin * cumulative + label(cumulative) + style(nudge = "up") +
  title("Each bar labeled with its running count")
(data(gapminder_2007) + bar * bin * cumulative + x(col.life) +
  text * bin * cumulative + label(col.cumulative) + style(nudge = "up") +
  title("Each bar labeled with its running count"))
data(gapminder_2007) + bar * bin * cumulative + x(:life) +
  text * bin * cumulative + label(:cumulative) + style(nudge = "up") +
  title("Each bar labeled with its running count")
plot(data(gapminder_2007), layer(bar, bin, cumulative), x(col.life),
  layer(text, bin, cumulative), label(col.cumulative),
  style({ nudge: "up" }),
  title("Each bar labeled with its running count"))
8 17 28 39 48 58 85 110 142 50 60 70 80 0 50 100 Each bar labeled with its running count Cumulative Life

“Given gapminder 2007: bars derived by bin and cumulative, x is life, and also text derived by bin and cumulative, label by cumulative, with nudge up.”

The last bar is labeled 142, and the bar before it 110. The difference, 32, is the count of the last bin alone, which label(count) would print instead.

16.3.1 Labels on dodged bars

Grouped bars set one bar per group side by side in each slot, with dodge. A label layer without dodge prints all of a slot’s numbers in its middle, between the bars. With the same dodge and a split of its own, each label is printed on its own bar. The gm_eras table holds two years, 1957 and 2007, with one row per country in each:

gm_eras: first 5 of 284 rows
country continent year life population gdp era
Afghanistan Asia 1957 30.332 9240934 820.8530 1957
Afghanistan Asia 2007 43.828 31889923 974.5803 2007
Albania Europe 1957 59.280 1476505 1942.2842 1957
Albania Europe 2007 76.423 3600523 5937.0295 2007
Algeria Africa 1957 45.685 10270856 3013.9760 1957

color(era) is written after bar, so it colors the bars alone (Encoding scope). group(era) splits the labels by era without coloring them:

data(gm_eras) + bar * mean * dodge + x(continent) + y(life) + color(era) +
  text * mean * dodge + group(era) + label(life, decimals = 1) +
  style(nudge = "up")
(data(gm_eras) + bar * mean * dodge + x(col.continent) + y(col.life) + color(col.era) +
  text * mean * dodge + group(col.era) + label(col.life, decimals = 1) +
  style(nudge = "up"))
data(gm_eras) + bar * mean * dodge + x(:continent) + y(:life) +
  color(:era) + text * mean * dodge + group(:era) +
  label(:life, decimals = 1) + style(nudge = "up")
plot(data(gm_eras), layer(bar, mean, dodge), x(col.continent),
  y(col.life), color(col.era), layer(text, mean, dodge), group(col.era),
  label(col.life, { decimals: 1 }), style({ nudge: "up" }))
49.3 66.7 41.3 56.0 70.3 70.7 77.6 54.8 73.6 80.7 Asia Europe Africa Americas Oceania 0 20 40 60 80 Life Continent Era 1957 2007

“Given the gapminder eras: bars derived by mean and dodge, x is continent, y is life, colored by era, and also text derived by mean and dodge, grouped by era, label by life with 1 decimal place, with nudge up.”

Each continent has two labels, one on each era’s bar. Africa’s 41.3 is on its 1957 bar, and its 54.8 is on its 2007 bar. A text layer with dodge and nothing to split it is refused, because there would be no groups to set apart.

16.4 Color

You see a hue faster than you can read a word, so a color split shows you the groups before you read a single name. Every label so far has been printed in one color, which is the default. Map a categorical column to color and the labels split by that column; a column of numbers is refused on a text mark. The mapping gets a legend to name the groups, and the colors come from the same palette that points and bars use. In the plot below each country is its own group, so the hue repeats the name:

data(medals) + text + x(gold) + y(silver) + label(country) + color(country) +
  x_label("Gold") + y_label("Silver") +
  title("One hue per country")
(data(medals) + text + x(col.gold) + y(col.silver) + label(col.country) + color(col.country) +
  x_label("Gold") + y_label("Silver") +
  title("One hue per country"))
data(medals) + text + x(:gold) + y(:silver) + label(:country) +
  color(:country) + x_label("Gold") + y_label("Silver") +
  title("One hue per country")
plot(data(medals), text, x(col.gold), y(col.silver), label(col.country),
  color(col.country), x_label("Gold"), y_label("Silver"),
  title("One hue per country"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 One hue per country Silver Gold Country USA China Great Britain Russia Germany

“Given the medals: text, x is gold, y is silver, label by country, color by country.”

The legend names the five hues. With a column that several rows share, such as a continent, one hue would mark every name in a group before you read any of them.

16.5 Styling: one color, one size

The color channel takes its hues from a column. When the words are the whole plot, with no dots or bars beside them, you may want one color of your own and a larger font, because the default is often too small to read comfortably. Neither comes from a column, so both are settings rather than mappings. style(color = ) sets the color of the words, and style(size = ) sets the font size in pixels. Each applies to every label in the layer:

data(medals) + text + x(gold) + y(silver) + label(country) +
  style(color = "seagreen", size = 17) +
  x_label("Gold") + y_label("Silver") +
  title("Bigger, greener labels")
(data(medals) + text + x(col.gold) + y(col.silver) + label(col.country) +
  style(color = "seagreen", size = 17) +
  x_label("Gold") + y_label("Silver") +
  title("Bigger, greener labels"))
data(medals) + text + x(:gold) + y(:silver) + label(:country) +
  style(color = "seagreen", size = 17) + x_label("Gold") +
  y_label("Silver") + title("Bigger, greener labels")
plot(data(medals), text, x(col.gold), y(col.silver), label(col.country),
  style({ color: "seagreen", size: 17 }), x_label("Gold"),
  y_label("Silver"), title("Bigger, greener labels"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 Bigger, greener labels Silver Gold

“Given the medals: text, x is gold, y is silver, label by country, colored seagreen, with size 17.”

A setting gives every label in the layer the same font size. To size each label by a value of its own, map a column instead, as the next section shows.

16.6 Sizing and fading labels by a column

size and opacity are channels on text, as they are on point. A mapped size draws each label at a font size from 8 to 24 pixels. The smallest value gets 8, the largest gets 24, and the others fall in proportion between them. Here, the more bronze medals a country won, the larger its name:

data(medals) + text + x(gold) + y(silver) + label(country) + size(bronze) +
  x_label("Gold") + y_label("Silver") +
  title("Each name sized by its bronze medals")
(data(medals) + text + x(col.gold) + y(col.silver) + label(col.country) + size(col.bronze) +
  x_label("Gold") + y_label("Silver") +
  title("Each name sized by its bronze medals"))
data(medals) + text + x(:gold) + y(:silver) + label(:country) +
  size(:bronze) + x_label("Gold") + y_label("Silver") +
  title("Each name sized by its bronze medals")
plot(data(medals), text, x(col.gold), y(col.silver), label(col.country),
  size(col.bronze), x_label("Gold"), y_label("Silver"),
  title("Each name sized by its bronze medals"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 Each name sized by its bronze medals Silver Gold Bronze a 9 a 23.5 a 38

“Given the medals: text, x is gold, y is silver, label by country, size by bronze.”

The legend shows the letter a at three of those sizes, because a label is read by the size of its letters. A mapped opacity works the same way. The smallest value is drawn at opacity 0.15, faint but visible, and the largest at 0.95:

data(medals) + text + x(gold) + y(silver) + label(country) + opacity(bronze) +
  x_label("Gold") + y_label("Silver") +
  title("Fewer bronze medals, fainter name")
(data(medals) + text + x(col.gold) + y(col.silver) + label(col.country) + opacity(col.bronze) +
  x_label("Gold") + y_label("Silver") +
  title("Fewer bronze medals, fainter name"))
data(medals) + text + x(:gold) + y(:silver) + label(:country) +
  opacity(:bronze) + x_label("Gold") + y_label("Silver") +
  title("Fewer bronze medals, fainter name")
plot(data(medals), text, x(col.gold), y(col.silver), label(col.country),
  opacity(col.bronze), x_label("Gold"), y_label("Silver"),
  title("Fewer bronze medals, fainter name"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 Fewer bronze medals, fainter name Silver Gold Bronze a 9 a 23.5 a 38

“Given the medals: text, x is gold, y is silver, label by country, opacity by bronze.”

Both channels are scaled over every row of the layer, so one value draws at the same size and opacity in every panel and every frame.

A word cloud also sizes each word by a value, but then moves each word to wherever there is room. A text mark draws each word at the position its row gives, so gog draws no word cloud.

16.7 Labels in the cube

A 3-D scatter has the same problem as a flat one: a dot does not say which row it is. A label can be drawn in the cube, as a point can. Bind z, and each label is drawn at the place its row’s three positions give it. Nearer labels are drawn over farther ones:

data(medals) + point + text * repel + x(gold) + y(silver) + z(bronze) +
  label(country) + title("Each country at its three medal counts")
(data(medals) + point + text * repel + x(col.gold) + y(col.silver) + z(col.bronze) +
  label(col.country) + title("Each country at its three medal counts"))
data(medals) + point + text * repel + x(:gold) + y(:silver) + z(:bronze) +
  label(:country) + title("Each country at its three medal counts")
plot(data(medals), point, layer(text, repel), x(col.gold), y(col.silver),
  z(col.bronze), label(col.country),
  title("Each country at its three medal counts"))
Germany Russia Great Britain China USA 40 30 20 30 20 10 30 20 10 Gold Silver Bronze Each country at its three medal counts

“Given the medals: points and also text derived by repel, x is gold, y is silver, z is bronze, label by country.”

repel moves labels off each other and off the dots, as on a flat plot. A nudge moves a label on the picture, not in the cube: "up" is toward the top of the image at every angle.

16.8 A halo around each label

A label drawn over a dot or a line can be hard to read, because the mark shows around its letters. style(border_size = ) draws a halo: a band of the panel’s background color around each letter, drawn under the text. It hides the mark next to each letter and does not change the letters:

data(medals) + point + style(color = "#9e9e9e", size = 9) +
  text + x(gold) + y(silver) + label(country) +
  style(size = 12, border_size = 3) +
  x_label("Gold") + y_label("Silver") +
  title("Names with a halo")
(data(medals) + point + style(color = "#9e9e9e", size = 9) +
  text + x(col.gold) + y(col.silver) + label(col.country) +
  style(size = 12, border_size = 3) +
  x_label("Gold") + y_label("Silver") +
  title("Names with a halo"))
data(medals) + point + style(color = "#9e9e9e", size = 9) + text +
  x(:gold) + y(:silver) + label(:country) +
  style(size = 12, border_size = 3) + x_label("Gold") +
  y_label("Silver") + title("Names with a halo")
plot(data(medals), point, style({ color: "#9e9e9e", size: 9 }), text,
  x(col.gold), y(col.silver), label(col.country),
  style({ size: 12, border_size: 3 }), x_label("Gold"), y_label("Silver"),
  title("Names with a halo"))
USA China Great Britain Russia Germany USA China Great Britain Russia Germany 20 30 40 10 20 30 Names with a halo Silver Gold

“Given the medals: points and also text, x is gold, y is silver, label by country, with size 12 and border size 3.”

A thin band of the panel’s color now separates each name from the gray dot behind it. border_size sets the width of the stroke in pixels. Half of it lies under the letters, so a width of 3 shows as a band 1.5 pixels wide. border_color sets its color, if you want one other than the background. Either setting alone draws a halo: an unset width is 3 pixels, and an unset color is the panel’s background color. With neither, a label has no halo.

A halo hides the marks it covers, so use it where the words matter more than those marks. It never hides another label’s letters, because every halo in a layer is drawn before any letter. It does not move overlapping labels apart. That is the job of repel.

16.9 Turning a label

A note beside a vertical line is easiest to read when it is written along the line, not across it. style(angle = ) turns every label in its layer by the number of degrees you give, counterclockwise from horizontal, so 90 reads upward and -90 downward. Each label turns around its own position, the point its x and y give. A sunburst turns and, if needed, shrinks its names by itself, each to fit its own sector, unless you set an angle. The milestones table holds two notes. Each is drawn at the income in at and the height in value:

milestones: all 2 rows
at value note
10000 72 less steep from here
40000 80 almost flat here

In this plot a vertical rule marks each income, and each note is drawn along its rule:

data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
  style(color = "#9e9e9e") +
  data(milestones) + rule + x(at) + style(color = "steelblue") +
  data(milestones) + text + x(at) + y(value) + label(note) +
  style(angle = 90, border_size = 3) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("Notes turned 90 degrees")
(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
  style(color = "#9e9e9e") +
  data(milestones) + rule + x(col.at) + style(color = "steelblue") +
  data(milestones) + text + x(col.at) + y(col.value) + label(col.note) +
  style(angle = 90, border_size = 3) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("Notes turned 90 degrees"))
data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
  style(color = "#9e9e9e") + data(milestones) + rule + x(:at) +
  style(color = "steelblue") + data(milestones) + text + x(:at) +
  y(:value) + label(:note) + style(angle = 90, border_size = 3) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("Notes turned 90 degrees")
plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
  y(col.life), style({ color: "#9e9e9e" }), data(milestones), rule,
  x(col.at), style({ color: "steelblue" }), data(milestones), text,
  x(col.at), y(col.value), label(col.note),
  style({ angle: 90, border_size: 3 }), x_label("GDP per person"),
  y_label("Life expectancy"), title("Notes turned 90 degrees"))
less steep from here almost flat here less steep from here almost flat here 1K 10K 40 50 60 70 80 Notes turned 90 degrees Life expectancy GDP per person

“Given gapminder 2007: points, x is gdp on a log scale, y is life; then given the milestones: rules, x is at; then given the milestones: text, x is at, y is value, label by note, with angle 90.”

Each note is now parallel to its line, and the halo from the section above hides the line behind its letters. gog measures a turned label by the space it takes after turning. repel uses that space to keep labels apart, and in nest a name is drawn only if it fits its rectangle when turned. A flow draws every name, so turning is how a long name stays inside its tall, narrow slot.

16.10 Why label is a channel

A setting is a value you type into the sentence. You might expect to type a label into the sentence in the same way, like a title. A label is not a title: it is one value per row, and that makes it data. One rule of the grammar decides whether a label is a setting or a channel: data reaches a mark only through a channel. That makes label a channel, so no mark takes the words themselves as an argument. A fixed note is therefore a table with one row, given to data(), and never a value given to style().

Only text accepts label; every other mark refuses it, which is why a labeled scatter is point + text and never a label on point. text requires a label: the label is part of the mark’s minimum syllable, as x and y are. Ask for a text mark with no label and the engine refuses, and says what to write instead, as What it refuses shows. text is the mark, and label is the channel that carries the column it draws.

16.11 A note is a layer

A plot often needs one note written on it, at a place you choose. It may say what a region of the scatter means, or why a point is where it is. If you have used ggplot2, a note there is a separate function, annotate(). In gog it is a table with one row, and the plot below draws it. A second data() gives the next layer its own table (the Data chapter’s rule), and a table with one row in it puts one string on the plot:

income_note: its one row
gdp life what
35000 66 high income, short lives
data(gapminder_2007) + point + x(gdp) + y(life) +
  data(income_note) + text + label(what) +
  title("A note is a layer, not a function")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  data(income_note) + text + label(col.what) +
  title("A note is a layer, not a function"))
data(gapminder_2007) + point + x(:gdp) + y(:life) + data(income_note) +
  text + label(:what) + title("A note is a layer, not a function")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  data(income_note), text, label(col.what),
  title("A note is a layer, not a function"))
high income, short lives 0 10K 20K 30K 40K 50K 40 50 60 70 80 A note is a layer, not a function Life Gdp

“Given gapminder 2007: points, x is gdp, y is life, and also text from the income note, label by what.”

The note is in an empty corner of the scatter: no country in 2007 has an income that high with lives that short.

That plot needed nothing new in the grammar. A mark written directly after a data() already reads that table, and text already draws one string per row. The note is data like any other, so it is scaled like any other. Give the note an x far from the points, and the axes stretch to cover it, so check the axes each time you place a note.

One rule applies while writing a note. The text layer names no position of its own, so it reads the columns the axes use, gdp and life, from its own table. That is why income_note has a column with each of those names. A note with its own names writes its positions after text: + text + x(at) + y(value) + label(what). Any other mark at a place you choose is written the same way: a small table, and a layer over it. A dot on the plot is a point over the same one-row table, and several notes are several rows.

16.12 Text that a transform places

You choose where a note goes. A transform can choose the place instead, and then the text names whatever that transform built. In the first six rows, a second layer with the same transform draws the shape the text labels:

Text derived by What it labels
text * layout the nodes of a network, beside point * layout and edge * layout
text * flow each stage’s slots in a flow, beside ribbon * flow and zone * flow
text * partition the regions of a hierarchy, beside zone * partition
text * count, text * bin the count of each bar, beside bar * count or bar * bin (Number labels)
text * mean, text * sum and the other summaries the summary of each bar, beside bar * mean or bar * sum (Number labels)
text * dodge each group’s bar in a slot, beside bar * dodge (Labels on dodged bars)
text * repel the scatter labels of repel, moved off each other

The chapters that draw the first three are Network, Flow and Transforms. In each one the text is a layer over the mark that drew the shape, and both layers read the same transform.

16.13 What you can set

The Styling section set a color and a size. Those are two entries in a longer list, and the whole of it is here: which color, how transparent, which size, which way to shift the label, by how many degrees to turn it, and the color and width of its halo. None of that is in a column, so it is set rather than mapped, and each mark takes its own settings. These are a text mark’s, with the values each accepts:

Setting Value
style(color = ) any CSS color name or hex
style(opacity = ) 0 to 1
style(size = ) pixels
style(border_color = ) any CSS color name or hex
style(border_size = ) pixels
style(nudge = ) up, down, left, right
style(angle = ) degrees, counterclockwise from horizontal

nudge and angle are the two settings only text has. nudge exists so a layered point + text does not draw the label on top of the dot:

data(medals) + x(country) + y(gold) +
  point + style(color = "#9e9e9e", size = 6) +
  text + label(gold) + style(nudge = "up", size = 11, color = "black") +
  y_label("Gold medals") + title("Labels nudged off their points")
(data(medals) + x(col.country) + y(col.gold) +
  point + style(color = "#9e9e9e", size = 6) +
  text + label(col.gold) + style(nudge = "up", size = 11, color = "black") +
  y_label("Gold medals") + title("Labels nudged off their points"))
data(medals) + x(:country) + y(:gold) + point +
  style(color = "#9e9e9e", size = 6) + text + label(:gold) +
  style(nudge = "up", size = 11, color = "black") +
  y_label("Gold medals") + title("Labels nudged off their points")
plot(data(medals), x(col.country), y(col.gold), point,
  style({ color: "#9e9e9e", size: 6 }), text, label(col.gold),
  style({ nudge: "up", size: 11, color: "black" }),
  y_label("Gold medals"), title("Labels nudged off their points"))
46 38 29 19 17 USA China Great Britain Russia Germany 20 30 40 Labels nudged off their points Gold medals Country

Each count is just above its dot, and neither covers the other. The four directions and the fixed offset are under Nudging the label off its point, with repel for labels that collide with each other.

The grid of every mark and every setting shows which other marks share this list. For the channels a mark maps rather than sets, read its row on the companion grid.

16.14 What it refuses

A text mark always refuses three things: a missing label, the shape channel, and any pattern. It refuses group only when no statistic and no dodge uses it. The label is part of the mark’s minimum syllable, as x and y are: with no string, a text mark has nothing to draw. The engine refuses and names the channel to add:

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

A reader who knows point will try its channels on text. color draws with a categorical column, and size and opacity draw with a continuous one, as on a point. shape is refused outright, because it cannot change a string. A point’s glyph can change shape because the dot itself says nothing. A text mark’s glyph is the string itself, so there is nothing for shape to change:

data(medals) + text + x(gold) + y(silver) + label(country) + shape(country)
data(medals) + text + x(col.gold) + y(col.silver) + label(col.country) + shape(col.country)
data(medals) + text + x(:gold) + y(:silver) + label(:country) +
  shape(:country)
plot(data(medals), text, x(col.gold), y(col.silver), label(col.country),
  shape(col.country))
Error:
! gog: `shape` cannot be bound to `text` — a text has no shape feature. Remove the `country` mapping from `shape`, or use a mark that maps it: `point`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

A string takes no texture either. pattern dashes a stroke or hatches a filled region, and a word is neither:

data(medals) + text + x(gold) + y(silver) + label(country) +
  style(pattern = "hatch")
(data(medals) + text + x(col.gold) + y(col.silver) + label(col.country) +
  style(pattern = "hatch"))
data(medals) + text + x(:gold) + y(:silver) + label(:country) +
  style(pattern = "hatch")
plot(data(medals), text, x(col.gold), y(col.silver), label(col.country),
  style({ pattern: "hatch" }))
Error:
! gog: `style(pattern = )` is a stroke's dash or a fill's texture, and a `text` is a glyph, not either — a point's form is set by `shape`, a text's by its content. The strokes (`line`/`step`/`interval`/`path`/`rule`/`edge`) take a dash; the fills (`area`/`bar`/`box`/`ribbon`/`zone`) a texture.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

On a text layer, group splits the rows that a statistic summarizes or that dodge sets side by side (Labels on dodged bars). A text layer with neither draws one label for each row, so group has nothing to split. A scatter has no slot to dodge in, so gog suggests color instead:

data(medals) + text + x(gold) + y(silver) + label(country) + group(country)
data(medals) + text + x(col.gold) + y(col.silver) + label(col.country) + group(col.country)
data(medals) + text + x(:gold) + y(:silver) + label(:country) +
  group(:country)
plot(data(medals), text, x(col.gold), y(col.silver), label(col.country),
  group(col.country))
Error:
! gog: `group` on a `text` needs `dodge` or a statistic to act on, and on this plot neither helps: there is no category axis whose bars the labels could stand over. Each row is already its own label, so splitting the rows by `country` would change nothing. To show the groups, map `country` to `color`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

16.15 In short

A text mark will not draw without a label, and no other mark accepts one. Its words come from a column, so a fixed note is a table with one row. repel draws every label and reports how many still overlap, because which names to leave out is your decision.

16.16 What comes next

Without repel, row order never moves a word: each one is drawn at the position its own row gives it. For a path, row order decides which rows are joined: the mark joins each row to the next, in the order the table holds them.