| 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 |
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:
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"))“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"))“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"))“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"))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:
| 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"))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"))“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:
| 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 |
| 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"))“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"))“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"))“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"))“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" }))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" }))“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" }))“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" }))“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" }))“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" }))“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" }))“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"))“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:
| 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" }))“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"))“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"))“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"))“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"))“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"))“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"))“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:
| 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"))“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:
| 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"))“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"))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.