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, but where a point’s glyph is a dot, a text mark’s glyph is a string, and the 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, but which is which?")
(data(medals) + point + x(col.gold) + y(col.silver) +
  x_label("Gold") + y_label("Silver") +
  title("Five countries, but which is which?"))
data(medals) + point + x(:gold) + y(:silver) + x_label("Gold") +
  y_label("Silver") + title("Five countries, but which is which?")
plot(data(medals), point, x(col.gold), y(col.silver), x_label("Gold"),
  y_label("Silver"), title("Five countries, but which is which?"))
20 30 40 10 20 30 Five countries, but which is which? Silver Gold

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

Bind label and draw the names themselves at those positions, and the anonymity is gone; the mark is the datum:

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

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

They compose. Superpose a point and a text over one (x, y) and you get a dot located precisely and its name attached: a labeled scatterplot built from two marks, not a special geom. A label would otherwise land right on the dot, so nudge it clear with style(nudge = "up"):

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

There is no “labeled point” mark to learn: a labeled scatter is just point + text, superposition (the + operator) doing the work.

16.2.1 Nudging the label off its point

style(nudge = ) shifts a text label a little off its (x, y), one of "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

The distance is derived from the font size, just enough to clear a default dot, so there is nothing to specify; nudge takes only the direction. It is a setting, not a jitter/dodge-style adjustment, and the difference is exact: those two compute data-derived offsets (a seeded spread, a per-group slot) to separate a mark’s own overlapping elements, whereas a nudge is one fixed displacement that derives nothing from the data. It is text-only: nudging a point or bar would move the data itself, so the engine refuses it there with direction. (When many labels collide with each other, a different overlap, a data-derived repel is the answer; that one is not built yet.)

16.3 Numeric labels: the value on each mark

A label need not be a name. Bind a numeric column and its value is drawn, formatted so an integer keeps no stray decimals. 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("Bars that state their value")
(data(medals) + bar + style(color = "steelblue") +
  text + x(col.country) + y(col.gold) + label(col.gold) +
  y_label("Gold medals") +
  title("Bars that state their value"))
data(medals) + bar + style(color = "steelblue") + text + x(:country) +
  y(:gold) + label(:gold) + y_label("Gold medals") +
  title("Bars that state their value")
plot(data(medals), bar, style({ color: "steelblue" }), text,
  x(col.country), y(col.gold), label(col.gold), y_label("Gold medals"),
  title("Bars that state their value"))
46 38 29 19 17 USA China Great Britain Russia Germany 0 10 20 30 40 Bars that state their value Gold medals Country

16.4 Color

color splits the labels by group and earns a legend, the same discrete split line and bar make. 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"))
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

16.5 Styling: one color, one size

style(color = ) sets the ink and style(size = ) the font, in pixels, for 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"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 Bigger, greener labels Silver Gold

A per-row font size (a word cloud, where the biggest word is the most frequent) is a parked follow-up; for now size is a layer setting, not a mapped channel.

16.6 Why label is a channel

text requires a label: it is the mark’s minimum syllable, the way x and y are. Ask for a text with nothing to say and the engine refuses with direction rather than drawing blank:

data(medals) + text + x(gold) + y(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.

The reason label is a channel and not, say, a text("USA") argument is the grammar’s own rule: a string is data, and data reaches a mark only through a channel, never as an argument to the mark. So a fixed annotation comes from a one-row table (data), not a setting; and label rides text alone: every other mark refuses it, which is why a labeled scatter is point + text and not a label hung on point. text is the shape; label is the column it draws.

16.7 A note is a layer

That one-row table is worth seeing drawn, because it is the whole of what other libraries spell as an annotate() function. 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:

notes <- data.frame(gdp = 35000, life = 66, what = "rich, and dying young")
data(gapminder_2007) + point + x(gdp) + y(life) +
  data(notes) + text + label(what) +
  title("A note is a layer, not a function")
notes = {"gdp": [35000], "life": [66], "what": ["rich, and dying young"]}
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  data(notes) + text + label(col.what) +
  title("A note is a layer, not a function"))
notes = (gdp = [35000], life = [66], what = ["rich, and dying young"],)
data(gapminder_2007) + point + x(:gdp) + y(:life) + data(notes) + text +
  label(:what) + title("A note is a layer, not a function")
const notes = { gdp: [35000], life: [66], what: ["rich, and dying young"] };
plot(data(gapminder_2007), point, x(col.gdp), y(col.life), data(notes),
  text, label(col.what), title("A note is a layer, not a function"))
rich, and dying young 0K 10K 20K 30K 40K 50K 40 50 60 70 80 A note is a layer, not a function Life Gdp

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. Because the note is data like any other, it is scaled like any other: move it outside the cloud and the axes stretch to include it, which is the honest behavior and occasionally a surprise.

One thing to know while writing one. x and y are shared by the whole plot, so the note’s table has to carry the base table’s position column names (gdp and life here), and a note that names its own columns is waiting on per-layer positions. Everything else an annotate() call does in the data’s own units is already sayable: a marker is point over the same table, and several notes at once are simply several rows.

16.8 What you can set

Setting Value
style(color = ) any CSS color name or hex
style(opacity = ) 0 to 1
style(size = ) pixels
style(nudge = ) up, down, left, right

And these vary per row if you map them to a column instead: color() (categories), label() (either), play() (either).

nudge is the text mark’s own, and it exists so a superposed 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 clear of 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 clear of 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 clear of 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 clear of their points"))
46 38 29 19 17 USA China Great Britain Russia Germany 20 30 40 Labels nudged clear of their points Gold medals Country

There is no distance to give it. The offset is derived from the font size, about one line, so a label clears its point at every plot size and in every facet; a pixel count would mean something different in each. nudge shifts every label the same way, which is why it is a setting and not a collision modifier: it derives nothing from the data, so it cannot help when many labels collide with each other.

A mapped size on text is valid grammar the engine does not draw yet, so size(population) is refused as unsupported rather than ignored. Set it instead, and the per-row font size (a word cloud) stays on the list of things not built.

16.9 What it refuses

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)
Error:
! gog: `shape` cannot be bound to `text` — a text has no shape feature. Remove `shape(country)`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Form comes from label here, which is the argument of the section above. 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")
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`) 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.