| year | sales |
|---|---|
| 2019 | 120 |
| 2020 | 135 |
| 2021 | 128 |
| 2022 | 152 |
| 2023 | 168 |
27 Encoding scope
If you write one x() and then two marks, which marks use it? Both do. Each atom binds to a scope: the whole plot, or one layer. This chapter gives the rule that decides which.
27.1 The rule
| Atom | Scope | Position-sensitive? |
|---|---|---|
x(), y(), z() before any mark |
Plot, every layer | Yes |
| the same positions after a mark | Layer, that mark’s column for the shared axis | Yes |
data() before any mark |
Plot, default table | No |
data() after a mark |
Next layer only | Yes |
color(), size(), shape(), pattern(), opacity(), group(), label(), play() before any mark |
Plot, every layer that can accept it | Yes |
| the same channels after a mark | Layer, that mark alone | Yes |
style() |
Layer, nearest preceding mark; refused when no mark precedes it | Yes |
title(), x_label(), y_label(), z_label(), palette(), theme(), brush() |
Plot | No |
One rule covers the position-sensitive rows: a mark binds forward, so it takes every atom written after it. The rows marked No (the labels, palette(), and a data() before any mark) belong to the plot wherever they are written. Write a channel before any mark and it binds to the plot; write it after a mark and it binds to that mark.
Where two bindings could apply, the nearest one wins: the same rule that lets a mid-expression data() give the next layer its own table.
27.2 One axis, and which column each layer reads
Every layer of a plot shares the same axes. That is what makes it one plot rather than two drawn on top of each other. gog allows no second axis: one x scale, one set of ticks, one label. A table that does not belong on those axes does not belong in this plot. Composition covers the choice between layering, faceting, and a page of separate plots.
Sharing an axis is not the same as sharing a column name, and the two are easy to confuse. Bind the positions once before the marks and every layer reads them, which is what you want almost always:
# x and y defined once: both marks use them
data(actuals) + x(year) + y(sales) +
bar +
line(data(actuals) + x(col.year) + y(col.sales) +
bar +
line)data(actuals) + x(:year) + y(:sales) + bar + lineplot(data(actuals), x(col.year), y(col.sales), bar, line)“Given the actuals: x is year, y is sales, bars and also a line.”
Write a position after a mark and it belongs to that mark, exactly as color() does. The axis is unchanged. The layer is only saying which of its columns supplies the values. That rule is what lets milestones in Data keep its own column names:
| 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 |
| at | value | note |
|---|---|---|
| 10000 | 72 | income takes off |
| 40000 | 80 | the long plateau |
# `milestones` calls its columns `at` and `value`, not `gdp` and `life`
data(gapminder_2007) + point + x(gdp) + y(life) +
data(milestones) + text + x(at) + y(value) + label(note)(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
data(milestones) + text + x(col.at) + y(col.value) + label(col.note))data(gapminder_2007) + point + x(:gdp) + y(:life) + data(milestones) +
text + x(:at) + y(:value) + label(:note)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
data(milestones), text, x(col.at), y(col.value), label(col.note))“Given gapminder 2007: points, x is gdp, y is life; then given the milestones: text, x is at, y is value, label by note.”
Look at the axis labels: they still read Gdp and Life. The milestones column names never reach the axis labels, because the axis belongs to the plot. Only the column choice is local. Without this rule, milestones would have to rename at and value to gdp and life. That is easy when you are inventing a table, and tedious when the table already exists.
The same rule covers two columns of one table on one axis:
| session | open | close | high | low |
|---|---|---|---|---|
| 01 | 100.39 | 100.45 | 100.66 | 99.98 |
| 02 | 99.41 | 99.72 | 99.93 | 98.66 |
| 03 | 101.02 | 103.11 | 104.19 | 100.53 |
| 04 | 102.09 | 102.01 | 102.98 | 101.60 |
| 05 | 105.03 | 104.85 | 105.71 | 103.86 |
data(sessions) + x(session) +
line + y(open) + style(color = "steelblue") +
line + y(close) + style(color = "tomato", pattern = "dashed")(data(sessions) + x(col.session) +
line + y(col.open) + style(color = "steelblue") +
line + y(col.close) + style(color = "tomato", pattern = "dashed"))data(sessions) + x(:session) + line + y(:open) +
style(color = "steelblue") + line + y(:close) +
style(color = "tomato", pattern = "dashed")plot(data(sessions), x(col.session), line, y(col.open),
style({ color: "steelblue" }), line, y(col.close),
style({ color: "tomato", pattern: "dashed" }))“Given the sessions: x is session, a line, y is open, colored steelblue, and also a line, y is close, colored tomato.”
Reshaping to tidy data is still usually better: one value column plus a type column, mapped with color(type). That form earns a legend and handles more than two series. Name the columns per layer when you want the two lines styled differently. Do the same when the second set of values comes from another table.
What a layer may not do is bring its own scale:
data(gapminder_2007) + point + x(gdp) + y(life) +
data(milestones) + text + x(at, scale = "log") + y(value) + label(note)(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
data(milestones) + text + x(col.at, scale = "log") + y(col.value) + label(col.note))data(gapminder_2007) + point + x(:gdp) + y(:life) + data(milestones) +
text + x(:at, scale = "log") + y(:value) + label(:note)plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
data(milestones), text, x(col.at, { scale: "log" }), y(col.value),
label(col.note))Error:
! gog: this layer's `x(at, …)` gives it its own scale, and the layers of one plot share one x axis — two scales on it would be two coordinate spaces in one panel, where every mark reads against an axis the expression never named. Set the scale once for the plot, before the marks: `x(<column>, scale = "log")`. A layer may name its own *column* for the shared axis — `x(at)` on its own is fine — but not its own scale. If the two series are genuinely in different units, facet on the measure instead.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A second scale on one axis is a second axis. Every mark would then be measured against a scale you never wrote. This refusal and the plot before it differ by one word: a layer names its own column, never its own scale.
27.3 Channels: plot-scoped or layer-scoped, by position
Positions were the hard case, because every layer shares one axis. The other channels share nothing, and they follow the same position rule.
A channel is often meant for one layer alone: colored points under a single plain line, for example. Written after a mark, a channel binds to that mark alone:
data(gapminder_2007) + x(gdp) + y(life) +
point + color(continent) + # color applies to point only
line # line uses a single default color(data(gapminder_2007) + x(col.gdp) + y(col.life) +
point + color(col.continent) +
line)data(gapminder_2007) + x(:gdp) + y(:life) + point + color(:continent) +
lineplot(data(gapminder_2007), x(col.gdp), y(col.life), point,
color(col.continent), line)“Given gapminder 2007: x is gdp, y is life, points colored by continent, and also a line.”
Written before any mark, a channel has no mark to bind to, so it binds to the plot and covers every layer:
data(gapminder_2007) + x(gdp) + y(life) + color(continent) +
point +
line(data(gapminder_2007) + x(col.gdp) + y(col.life) + color(col.continent) +
point +
line)data(gapminder_2007) + x(:gdp) + y(:life) + color(:continent) + point +
lineplot(data(gapminder_2007), x(col.gdp), y(col.life), color(col.continent),
point, line)“Given gapminder 2007: x is gdp, y is life, color by continent, points and also a line.”
That is the same position rule x and y follow. Written before the marks, they apply to every mark. Written after one mark, they belong to that mark. No channel has a scoping rule of its own. For x, y and z, a layer-scoped binding names a column, never a new axis.
27.3.1 Why binding only goes forward
Suppose you want the lines colored by country and only the points sized by population. Because a mark takes only the atoms written after it, the same atoms always mean the same thing:
| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| China | Asia | 1952 | 44.00000 | 556263527 | 400.4486 |
| China | Asia | 1957 | 50.54896 | 637408000 | 575.9870 |
| China | Asia | 1962 | 44.50136 | 665770000 | 487.6740 |
| China | Asia | 1967 | 58.38112 | 754550000 | 612.7057 |
| China | Asia | 1972 | 63.11888 | 862030000 | 676.9001 |
# `size` is written after `point`, so it belongs to the points;
# the line beside them does not change, and keeps its own color
data(gapminder_asia) + x(year) + y(life) +
line + color(country) +
point + size(population)(data(gapminder_asia) + x(col.year) + y(col.life) +
line + color(col.country) +
point + size(col.population))data(gapminder_asia) + x(:year) + y(:life) + line + color(:country) +
point + size(:population)plot(data(gapminder_asia), x(col.year), y(col.life), line,
color(col.country), point, size(col.population))“Given gapminder Asia: x is year, y is life, lines colored by country, and also points sized by population.”
A channel that bound backward could not draw this plot. It would put size on the line as well. A line has no size, so gog would refuse the whole plot, and you could not say what you meant.
27.3.2 A plot-scoped channel and the marks it skips
size written for the whole plot applies to the points, because a line has no size. gog applies it to every mark that has the feature and prints a message naming the marks it skipped. That message is an assumption: it reports a choice rather than refusing one, so the plot still renders:
data(gapminder_asia) + x(year) + y(life) + size(population) +
line + group(country) +
point(data(gapminder_asia) + x(col.year) + y(col.life) + size(col.population) +
line + group(col.country) +
point)data(gapminder_asia) + x(:year) + y(:life) + size(:population) + line +
group(:country) + pointplot(data(gapminder_asia), x(col.year), y(col.life), size(col.population),
line, group(col.country), point)gog: `size(population)` is written for the whole plot, so it applies to `point` — `line` has no size feature and is left unchanged. Move `size(population)` after a mark to bind it to that mark alone.
“Given gapminder Asia: x is year, y is life, size by population, lines grouped by country, and also points.”
If no mark in the plot can accept the channel, the binding does nothing at all, so gog refuses it:
data(gapminder_asia) + x(year) + y(life) + size(population) +
line + group(country)(data(gapminder_asia) + x(col.year) + y(col.life) + size(col.population) +
line + group(col.country))data(gapminder_asia) + x(:year) + y(:life) + size(:population) + line +
group(:country)plot(data(gapminder_asia), x(col.year), y(col.life), size(col.population),
line, group(col.country))Error:
! gog: `size(population)` is written for the whole plot, but no mark here has a size feature (`line`). Remove it, or add a mark that does.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
27.4 data() mid-expression
A forecast, a threshold or a set of labels usually arrives in a table of its own, so one plot often reads two tables. Data states this rule, and it is a scope rule. The first data() is the plot’s table. Each later data() applies to the mark written directly after it.
| year | sales |
|---|---|
| 2024 | 180 |
| 2025 | 195 |
| 2026 | 210 |
data(actuals) + x(year) + y(sales) +
line +
data(forecast) + point # ← forecast applies to point, not to line(data(actuals) + x(col.year) + y(col.sales) +
line +
data(forecast) + point)data(actuals) + x(:year) + y(:sales) + line + data(forecast) + pointplot(data(actuals), x(col.year), y(col.sales), line, data(forecast),
point)Position matters here because each data() marks where one layer’s table ends and the next layer’s begins.