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:

actuals: all 5 rows
year sales
2019 120
2020 135
2021 128
2022 152
2023 168
# 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 + line
plot(data(actuals), x(col.year), y(col.sales), bar, line)
2019 2020 2021 2022 2023 0 50 100 150 Sales Year

“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:

gapminder_2007: first 5 of 142 rows
country continent year life population gdp
Afghanistan Asia 2007 43.828 31889923 974.5803
Albania Europe 2007 76.423 3600523 5937.0295
Algeria Africa 2007 72.301 33333216 6223.3675
Angola Africa 2007 42.731 12420476 4797.2313
Argentina Americas 2007 75.320 40301927 12779.3796
milestones: all 2 rows
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))
income takes off the long plateau 0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“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:

sessions: first 5 of 14 rows
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" }))
01 02 03 04 05 06 07 08 09 10 11 12 13 14 100 105 110 Open Session

“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) +
  line
plot(data(gapminder_2007), x(col.gdp), y(col.life), point,
  color(col.continent), line)
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

“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 +
  line
plot(data(gapminder_2007), x(col.gdp), y(col.life), color(col.continent),
  point, line)
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

“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:

gapminder_asia: first 5 of 60 rows
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))
1960 1980 2000 40 50 60 70 80 Life Year Country China India Indonesia Japan Korea, Rep. Population 20.9M 669.8M 1.3B

“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) + point
plot(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.
1960 1980 2000 40 50 60 70 80 Life Year Population 20.9M 669.8M 1.3B

“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.

forecast: all 3 rows
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) + point
plot(data(actuals), x(col.year), y(col.sales), line, data(forecast),
  point)
2020 2022 2024 2026 120 140 160 180 200 Sales Year

Position matters here because each data() marks where one layer’s table ends and the next layer’s begins.