| 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 at each (x, y) position. It is point’s sibling: both place one glyph per row. A point’s glyph is a dot. A text mark’s glyph is a string, and that string comes from a column, through the label channel.
16.1 The mark and its content
A point tells you where a row sits; it cannot tell you which row. Five medal counts drawn as dots are five anonymous points:
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.”
Bind label and draw the names themselves at those positions, and each mark now shows which row it is:
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 a glyph at the row, text draws the row’s own value as the glyph.
16.2 Both at once: the labeled scatter
A word is wide and a position is a point, so a name alone marks its row only roughly. The two marks compose. Layer a point and a text over one (x, y). 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 the gray below belongs to the dots alone. A label would otherwise sit on top of its dot, so style(nudge = "up") moves it off:
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.”
There is no “labeled point” mark to learn: a labeled scatter is just point + text, layered with the + operator.
Five names fit here. Fifty would not, and choosing which few to keep is the hardest part of a labeled scatter. In the web edition you can try them first. Clicking a point prints its values on the plot, which Selection calls a stamp. That shows how a few labels look before you write any. A stamp disappears when the page reloads; a text label is part of the specification, so it is drawn every time.
16.2.1 Nudging the label off its point
Up is one of four directions, and it is not always the empty side. A dot near the top of the panel, or one with a neighbor just above it, wants its name somewhere else. 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"))The distance comes from the font size, just enough to sit outside a default dot, so there is nothing to specify; nudge takes only the direction. It is a setting, not a collision modifier, and the two differ in where the offset comes from. jitter and dodge compute their offsets from the data, so every mark moves by its own amount. A nudge is one fixed shift, the same for every label, derived from nothing in the data. It is text-only. Nudging a point or bar would move the data itself, so the engine refuses it there and says why.
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 capita") + 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 capita") + 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 capita") +
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 capita"),
y_label("Life expectancy"), title("Thirty countries, named"))repel solves that, and it is a collision modifier rather than a setting. 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 capita") + 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 capita") + 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 capita") +
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 capita"), 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.”
The modifier does three things in that picture. Almost every label comes to rest just outside its dot, so the word does not cover the point it names. A label that moved far keeps a thin line back to its point, so you can still see which point it names. And each label rests beside its dot, on whichever side has room. repel pulls a label toward its dot and pushes it away from every word and every other dot. A label with no neighbors comes to rest just above its dot.
The placement is deterministic. It uses no random-number generator and no system time, so the same specification draws the same picture every time.
When no arrangement fits, repel still draws every label and reports how many still overlap. It never drops a label in silence. A plot missing the names it could not fit would look like a plot of fewer countries, so gog reports the count instead. A larger plot, a smaller style(size = ), or fewer rows separate the labels.
The setting and the collision modifier compose, and each does one thing. style(nudge = ) says which side a label prefers, and repel resolves the overlaps that remain:
data(gm_europe) + point + style(color = "#9e9e9e") +
text * repel + x(gdp) + y(life) + label(country) + style(nudge = "right") +
x_label("GDP per capita") + 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 capita") + 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 capita") + 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 capita"),
y_label("Life expectancy"),
title("Nudged right first, then moved apart"))16.3 Numeric labels: the value on each mark
A bar shows which country won more, and the axis only lets a reader estimate by how much. Sometimes the number itself is the answer. A label need not be a name. Bind a column that holds numbers and each row’s value is drawn. A whole number is printed without a decimal point. Layer text over bar to print each bar’s height at its top:
data(medals) + bar + style(color = "steelblue") +
text + x(country) + y(gold) + label(gold) +
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) +
y_label("Gold medals") +
title("Each bar labeled with its height"))data(medals) + bar + style(color = "steelblue") + text + x(:country) +
y(:gold) + label(:gold) + 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), 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.”
16.4 Color
Every label is printed in one color until a column separates them. A reader sees a hue before reading a word, so the group arrives before the name. color splits the labels by group and earns a legend, the same categorical split bar makes. Here each country’s name takes its own hue:
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.”
16.5 Styling: one color, one size
When the labels are the plot rather than notes beside it, the default font can be small for the job. A larger size for all of them, and a color of your own, are settings, because no column decides them. style(color = ) sets the color of the words. style(size = ) sets the font size in pixels. Both cover the whole 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.”
On text, size is a layer setting, not a mapped channel, so one font size covers every label. A word cloud sizes each word by its frequency, which needs a size per row.
16.6 Why label is a channel
You might expect to type the words into the sentence, the way a title is typed. A label is not a title: it is one value per row, and that makes it data. text requires a label: it is the mark’s minimum syllable, the way 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.
The grammar’s own rule decides this. A label’s string is data, and data reaches a mark only through a channel. So label is a channel, and a mark takes no text("USA") argument. So a fixed note comes from a one-row table (data), not a setting. 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 is the mark; label is the column it draws.
16.7 A note is a layer
A plot often needs one remark written on it, at a place you choose. It may say what a region of the scatter means, or why a point sits where it does. That one-row table is worth seeing drawn. In ggplot2 a note is a separate function, annotate(). In gog it is a table with one row. A second data() opens a second layer over its own table (the Data chapter’s rule), and a table with one row in it puts one string on the plot:
| 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 |
| 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 notes, label by what.”
Nothing was added to the grammar to reach that. data() already binds the nearest table, text already draws one string per row, and a note is a table that happens to have one row. The note is data like any other, so it is scaled like any other. Place it far from the points and the axes widen to include it.
One thing to know while writing a note. x and y belong to the whole plot, so the note’s table needs a column for each. The note above reuses the base names, gdp and life. A note with its own names writes its positions after text: + text + x(at) + y(value) + label(what). Everything else annotate() does in the data’s own units is already sayable. A dot on the plot is a point over the same one-row table, and several notes are several rows.
16.8 What you can set
Once the words are on the plot, the next question is how they look. Which size, which color, and which side of the dot? 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(nudge = ) |
up, down, left, right |
nudge is the one setting only text has. It 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 = "gray", 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 = "gray", 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 = "gray", 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: "gray", size: 6 }), text, label(col.gold),
style({ nudge: "up", size: 11, color: "black" }),
y_label("Gold medals"), title("Labels nudged off their points"))The four directions and the fixed offset are under Nudging the label off its point, with repel for labels that collide with each other.
A mapped size on text is valid grammar this engine does not draw, so size(population) is refused as unsupported rather than ignored. Set the font size with style(size = ) instead.
The grid of every mark and every setting shows which other marks share this list. What a mark maps rather than sets is its row on the companion grid.
16.9 What it refuses
A text with no label is refused first. The label is the mark’s minimum syllable, the way x and y are, for the reason Why label is a channel gave. 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, and one of them has no work to do on a string. A text mark’s glyph is its content, so it has nothing left for shape to vary:
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 has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A text mark’s form comes from label, for the reason that section gave. For the same reason a string takes no texture, being neither a stroke to dash nor a region to hatch:
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.