41  Two variables

You have two columns. What can they tell you together? Their types decide the question. Two measurements ask about relationship: do high values of one go with high values of the other? A category and a measurement ask about comparison: is the measurement larger in some groups than in others?

41.2 What is the trend through the cloud?

The scatter above is a cloud of 142 countries. The direction of that cloud is hard to judge from the dots alone. When the trend is the answer and every country still has to be seen, layer a derived line over the points. + puts both marks in one panel, and * smooth fits a curve through the middle of the cloud:

data(gapminder_2007) + x(gdp) + y(life) +
  point + color(continent) +
  line * smooth
(data(gapminder_2007) + x(col.gdp) + y(col.life) +
  point + color(col.continent) +
  line * smooth)
data(gapminder_2007) + x(:gdp) + y(:life) + point + color(:continent) +
  line * smooth
plot(data(gapminder_2007), x(col.gdp), y(col.life), point,
  color(col.continent), layer(line, smooth))
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 derived by smooth.”

The positions are written before any mark, so both layers share them; color is written after point, so only the points split. Transforms owns smooth; Encoding scope owns that rule.

41.3 How much was there at each time?

The actuals table holds one sales figure per year. Sometimes the question is not whether sales rose but how large they were in each year. A line says where the value went; an area says how much there was. It fills to zero, so the filled area is the amount:

actuals: all 5 rows
year sales
2019 120
2020 135
2021 128
2022 152
2023 168
data(actuals) + area + x(year) + y(sales)
data(actuals) + area + x(col.year) + y(col.sales)
data(actuals) + area + x(:year) + y(:sales)
plot(data(actuals), area, x(col.year), y(col.sales))
2019 2020 2021 2022 2023 0 50 100 150 Sales Year

“Given the actuals: an area, x is year, y is sales.”

One atom away. If your column has no meaningful zero, you want line, not area. Owning chapters: Line, Area.

41.4 What is the typical value per group?

Each continent holds many countries, and each country has its own life expectancy. To compare the continents you need one number for each, so you summarize the measurement within each category. The aggregation family is sum, mean, median, max, min and quantile. Each one reads a bound y and reduces every group to a single number. quantile also needs a probability, written quantile(0.9):

data(gapminder_2007) + bar * mean + x(continent) + y(life)
data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life)
data(gapminder_2007) + bar * mean + x(:continent) + y(:life)
plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
  y(col.life))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Life Continent

“Given gapminder 2007: bars derived by mean, x is continent, y is life.”

One atom away. The median is robust to outliers, so swap mean for median:

data(gapminder_2007) + bar * median + x(continent) + y(life)
data(gapminder_2007) + bar * median + x(col.continent) + y(col.life)
data(gapminder_2007) + bar * median + x(:continent) + y(:life)
plot(data(gapminder_2007), layer(bar, median), x(col.continent),
  y(col.life))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Life Continent

“Given gapminder 2007: bars derived by median, x is continent, y is life.” Owning chapters: Bar, Transforms.

41.5 What does every value per group look like?

The bars above gave each continent one number, and that number hides how far its countries spread. Sometimes you want to see every country, not only the mean. The strip plot drops the derivation and changes the mark: point where bar * mean stood. Each row is one dot, at its own height in its category’s slot:

data(gapminder_2007) + point + x(continent) + y(life)
data(gapminder_2007) + point + x(col.continent) + y(col.life)
data(gapminder_2007) + point + x(:continent) + y(:life)
plot(data(gapminder_2007), point, x(col.continent), y(col.life))
Asia Europe Africa Americas Oceania 40 50 60 70 80 Life Continent

“Given gapminder 2007: points, x is continent, y is life.”

When the dots pile onto one line, * jitter spreads them sideways, along the category axis only, so every dot keeps its exact height:

data(gapminder_2007) + point * jitter + x(continent) + y(life) + style(opacity = 0.6)
data(gapminder_2007) + point * jitter + x(col.continent) + y(col.life) + style(opacity = 0.6)
data(gapminder_2007) + point * jitter + x(:continent) + y(:life) +
  style(opacity = 0.6)
plot(data(gapminder_2007), layer(point, jitter), x(col.continent),
  y(col.life), style({ opacity: 0.6 }))
Asia Europe Africa Americas Oceania 40 50 60 70 80 Life Continent

“Given gapminder 2007: points derived by jitter, x is continent, y is life.”

One atom away. Swap the positions and the strips lie down, useful when the category names are long. See Point and jitter.

41.6 What if the table already has the answer?

The medals table holds one row per country, and the question is how many bronze medals each one won. That count is already in the table, so no transform is involved; bar draws the numbers as they arrive:

medals: all 5 rows
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
data(medals) + bar + x(country) + y(bronze)
data(medals) + bar + x(col.country) + y(col.bronze)
data(medals) + bar + x(:country) + y(:bronze)
plot(data(medals), bar, x(col.country), y(col.bronze))
USA China Great Britain Russia Germany 0 10 20 30 Bronze Country

“Given the medals: bars, x is country, y is bronze.”

One atom away. Ranked, because a comparison is easiest to read when it is sorted:

data(medals) + bar + x(country) + y(bronze) + order(bronze, desc = TRUE)
data(medals) + bar + x(col.country) + y(col.bronze) + order(col.bronze, desc = True)
data(medals) + bar + x(:country) + y(:bronze) +
  order(:bronze, desc = true)
plot(data(medals), bar, x(col.country), y(col.bronze),
  order(col.bronze, { desc: true }))
USA China Great Britain Germany Russia 0 10 20 30 Bronze Country

“Given the medals: bars, x is country, y is bronze, ordered by bronze, largest first.”

There is no hidden counting here to surprise you: if you want a computation, you write its name. That is why this recipe and How many of each? are different sentences.

41.7 What this section refuses

Suppose both columns are categorical, a country and its continent, and you write bar with one on each axis. A bar needs at least one of its axes to be a number: that number is the length of the bar. Two categories leave it nothing to measure, so the refusal names count:

data(gapminder_2007) + bar + x(country) + y(continent)
data(gapminder_2007) + bar + x(col.country) + y(col.continent)
data(gapminder_2007) + bar + x(:country) + y(:continent)
plot(data(gapminder_2007), bar, x(col.country), y(col.continent))
Error:
! gog: `bar` has categorical columns on both axes — `x(country)` and `y(continent)` — so there is nothing for it to measure. One axis must be a number: that is the length of the bar. To count rows per category instead, use `bar * count`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.