8  Point

How does one measurement change as another changes? point draws one glyph, a small symbol, at the (x, y) position of each row. The glyph is a dot unless you choose another.

The plot it makes is the scatterplot, and it was invented later than the other basic charts. Playfair published the line chart and the bar chart in 1786 (Playfair, 1786), and the pie chart in 1801 (Playfair, 1801). The scatterplot came about thirty years after the pie chart. A picture that plots one measured column against another is usually dated to John Herschel. In 1833 he plotted the position of twin stars against the year, then drew a smooth curve through the dots so the trend could be seen (Friendly & Denis, 2005). Other names for the same picture are scatter diagram and scattergram. When size maps a third column, the same plot is called the bubble chart.

None of those chart names is a word in gog, and that is deliberate. point is a mark, not a chart. point + x(gdp) + y(life) is a scatterplot, and point + x(continent) + y(life) is a strip plot. The mark is the same in both. The chart name belongs to the whole sentence, not to any word in the vocabulary. That is why the names live in an appendix, Chart names, instead of in the kernel.

8.1 Basic usage

Everything in this chapter is added to one sentence, the smallest that draws a point: a source, the mark, and its two positions.

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
data(gapminder_2007) + point + x(gdp) + y(life)
data(gapminder_2007) + point + x(col.gdp) + y(col.life)
data(gapminder_2007) + point + x(:gdp) + y(:life)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

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

Each dot is one row, one country, placed by its gdp and its life. Nothing else about the row is shown yet. Both columns hold numbers, so each axis is a linear scale. Most of the dots sit on the left, because most countries have far lower incomes than the richest few. The Scales chapter shows the log scale that spreads them out.

8.2 The dot as a default

Write point and you get a dot, and treating it as the dot mark is right almost every time. But the dot is only a default, and you can change it. A point is a glyph at a position, and shape replaces the dot with another of the five glyphs gog draws, one per category. Shape below names the five, says which category gets which, and what to do with more than five.

data(gapminder_2007) + point + x(gdp) + y(life) + shape(continent) +
  title("The glyph is not always a dot")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + shape(col.continent) +
  title("The glyph is not always a dot"))
data(gapminder_2007) + point + x(:gdp) + y(:life) + shape(:continent) +
  title("The glyph is not always a dot")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  shape(col.continent), title("The glyph is not always a dot"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 The glyph is not always a dot Life Gdp Continent Asia Europe Africa Americas Oceania

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

So “a dot” means “the first of the five shapes, drawn when you name no shape.” That is what makes point and text siblings: both draw one glyph per row at (x, y), differing only in where the glyph comes from. A point picks its glyph from the fixed set of five, with shape. A text mark takes its glyph from a column, with label, so what you see is the value in that column. Put both on one plot and each row is located by a dot and named by its label:

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) + point + style(size = 5, color = "#9e9e9e") +
  text + x(gold) + y(silver) + label(country) + style(nudge = "up") +
  x_label("Gold") + y_label("Silver") +
  title("point and text, siblings on one 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("point and text, siblings on one plot"))
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("point and text, siblings on one plot")
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("point and text, siblings on one plot"))
USA China Great Britain Russia Germany 20 30 40 10 20 30 point and text, siblings on one plot Silver Gold

“Given the medals: points and also text, x is gold, y is silver, label by country.”

8.3 A categorical x: the strip plot

x accepts a categorical column as readily as a continuous one. Each category gets a position on the axis, and every row is drawn there. You see the whole distribution rather than a summary of it:

data(gapminder_2007) + point + x(continent) + y(life) +
  y_label("Life expectancy") + title("One dot per country")
(data(gapminder_2007) + point + x(col.continent) + y(col.life) +
  y_label("Life expectancy") + title("One dot per country"))
data(gapminder_2007) + point + x(:continent) + y(:life) +
  y_label("Life expectancy") + title("One dot per country")
plot(data(gapminder_2007), point, x(col.continent), y(col.life),
  y_label("Life expectancy"), title("One dot per country"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 One dot per country Life expectancy Continent

This is the same rule bar uses for a categorical axis, and no mark gets a special case. Compare it with bar * mean, which collapses each group to one number:

data(gapminder_2007) + bar * mean + x(continent) + y(life) +
  y_label("Mean life expectancy") + title("The same data, summarized")
(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life) +
  y_label("Mean life expectancy") + title("The same data, summarized"))
data(gapminder_2007) + bar * mean + x(:continent) + y(:life) +
  y_label("Mean life expectancy") + title("The same data, summarized")
plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
  y(col.life), y_label("Mean life expectancy"),
  title("The same data, summarized"))
Asia Europe Africa Americas Oceania 0 20 40 60 80 The same data, summarized Mean life expectancy Continent

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

The strip plot shows Africa’s spread and its overlap with Asia; the bar chart shows neither. The strip plot has one weakness: every point in a category sits on the same vertical line, so heavy overlap can hide how many there are. opacity makes each point partly transparent, so overlap reads as a darker patch. point * jitter spreads overlapping points along the category axis, never off their measured value.

A second answer is to count the overlapping points instead of separating them. point * bin * stack stacks one dot per row along the measure axis, which is the dot plot. The cookbook draws it with thirty rows, where every row still shows. Which to write is your choice, by what you want the reader to see. jitter keeps the spread: every value sits where it was measured. bin * stack gives the count: how many rows fall in each bin, which is close to a bar chart, with the rows still visible instead of summarized into one length.

8.3.1 Either axis: a horizontal strip

gog reads the direction of a plot from its bindings: whichever axis carries the category decides it, for a point as for a bar. So the category can sit on y instead of x, and the strips lie down. The countries and the distributions are the same, and each row now reads left to right:

data(gapminder_2007) + point + x(life) + y(continent) +
  style(opacity = 0.6) +
  x_label("Life expectancy") +
  title("The same distributions, lying down")
(data(gapminder_2007) + point + x(col.life) + y(col.continent) +
  style(opacity = 0.6) +
  x_label("Life expectancy") +
  title("The same distributions, lying down"))
data(gapminder_2007) + point + x(:life) + y(:continent) +
  style(opacity = 0.6) + x_label("Life expectancy") +
  title("The same distributions, lying down")
plot(data(gapminder_2007), point, x(col.life), y(col.continent),
  style({ opacity: 0.6 }), x_label("Life expectancy"),
  title("The same distributions, lying down"))
40 50 60 70 80 Oceania Americas Africa Europe Asia The same distributions, lying down Continent Life expectancy

If you know ggplot2’s coord_flip(), there is no such word here: swapping the two bindings was the whole change, as swapping the two arguments of matplotlib’s scatter() is. Use the horizontal form when the category names are too long for the bottom axis. It also helps when there are many categories, since a list of rows is easier to read than a list of columns.

8.4 Color

Which continent is which? Every dot looks the same until a column tells them apart. Bind color to a categorical column to color points by group. gog assigns a distinct color from a 20-color palette automatically.

data(gapminder_2007) + point + x(gdp) + y(life) +
  color(continent)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  color(col.continent))
data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

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

A color legend is generated automatically whenever a column is mapped to color.

8.5 Shape

shape maps a categorical column to one of five glyphs. In order, they are the circle, the square, the triangle, the diamond and the cross, one per category. Categories are taken as they first appear in the table, or in the order you declare, so the first category gets the circle, the second the square, and so on. Asia comes first in the gapminder table, so Asia is the circle, and the legend below reads the same way. Five is the whole set, and about as many as a reader can separate on one plot. Do not map shape to a column with more than five categories: the sixth starts over at the circle, two categories then share a glyph and look the same, and gog does not warn. The book shows four ways to draw more groups than that:

  • color, whose default palette has twenty colors, as Color above does with the continents.
  • One panel per category with facet, folded into rows so the panels stay wide: Folding a long line of panels.
  • The name on the point instead of a glyph: a text layer with label, the labeled scatter, and repel when the names overlap. This is the one route that reaches a hundred categories, and in the web edition a brush leaves only the selected names readable.
  • Every category in one pale color, with the few that matter drawn over it in a second layer: What settings are for.

A column with fewer categories often carries the encoding: continent groups the countries and fits the five glyphs, so country goes on the label or the facet.

Combine color and shape on the same column so that colorblind readers, and black-and-white printing, can still separate the groups:

data(gapminder_2007) + point + x(gdp) + y(life) +
  color(continent) + shape(continent)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  color(col.continent) + shape(col.continent))
data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
  shape(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent), shape(col.continent))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania Continent Asia Europe Africa Americas Oceania

8.6 Size

A third measurement has nowhere to go once both positions are taken. size gives it one: it maps a continuous column to the point radius, from 3 pixels at the smallest value to 12 at the largest.

data(gapminder_2007) + point + x(gdp) + y(life) +
  color(continent) + size(population)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  color(col.continent) + size(col.population))
data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
  size(:population)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent), size(col.population))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania Population 199.6K 659.4M 1.3B

“Given gapminder 2007: points, x is gdp, y is life, color by continent, size by population.”

A size legend showing the minimum, midpoint, and maximum of the column is generated automatically.

8.7 Opacity

opacity maps a continuous column to transparency. It helps in a dense scatterplot, where marks cover one another and hide the shape of the data. Population is skewed, so on a linear scale two countries are dark and the rest are faint. The log scale spreads them across the range:

data(gapminder_2007) + point + x(gdp) + y(life) +
  opacity(population, scale = "log") +
  title("Opacity: population, on a log scale")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  opacity(col.population, scale = "log") +
  title("Opacity: population, on a log scale"))
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  opacity(:population, scale = "log") +
  title("Opacity: population, on a log scale")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  opacity(col.population, { scale: "log" }),
  title("Opacity: population, on a log scale"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Opacity: population, on a log scale Life Gdp Population 199.6K 16.2M 1.3B

“Given gapminder 2007: points, x is gdp, y is life, opacity by population on a log scale.”

See Channels and legends for the opacity range and why line does not accept this channel.

That example maps opacity to a column. Often there is no column to show: the only problem is that many points overlap. Then you want one opacity on every point instead. That is a setting rather than a mapping, so you write style(opacity = ) and no legend appears; see Setting vs mapping. The table below, gm_all, holds every gapminder country in every year, so the points overlap heavily:

gm_all: first 5 of 1704 rows
country continent year life population gdp
Afghanistan Asia 1952 28.801 8425333 779.4453
Afghanistan Asia 1957 30.332 9240934 820.8530
Afghanistan Asia 1962 31.997 10267083 853.1007
Afghanistan Asia 1967 34.020 11537966 836.1971
Afghanistan Asia 1972 36.088 13079460 739.9811
data(gm_all) + point + x(gdp) + y(life) +
  style(opacity = 0.2, size = 3) +
  title("Every point at one opacity")
(data(gm_all) + point + x(col.gdp) + y(col.life) +
  style(opacity = 0.2, size = 3) +
  title("Every point at one opacity"))
data(gm_all) + point + x(:gdp) + y(:life) +
  style(opacity = 0.2, size = 3) + title("Every point at one opacity")
plot(data(gm_all), point, x(col.gdp), y(col.life),
  style({ opacity: 0.2, size: 3 }), title("Every point at one opacity"))
0K 50K 100K 40 60 80 Every point at one opacity Life Gdp

“Given all the gapminder years: points, x is gdp, y is life, with opacity 0.2.”

8.8 All channels at once

A table often has more to show than two positions can hold. The refining channels carry the rest, and they can all be on one plot at once. Here continent goes to color and shape together, so the groups survive black-and-white printing, and population goes to size:

data(gapminder_2007) + point + x(gdp) + y(life) +
  color(continent) +
  shape(continent) +
  size(population) +
  title("GDP, life expectancy, and population")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  color(col.continent) +
  shape(col.continent) +
  size(col.population) +
  title("GDP, life expectancy, and population"))
data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
  shape(:continent) + size(:population) +
  title("GDP, life expectancy, and population")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent), shape(col.continent), size(col.population),
  title("GDP, life expectancy, and population"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 GDP, life expectancy, and population Life Gdp Continent Asia Europe Africa Americas Oceania Continent Asia Europe Africa Americas Oceania Population 199.6K 659.4M 1.3B

8.9 What you can set

A setting changes how every point looks without reading a column, and each mark takes its own. These are a point’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(shape = ) circle, square, triangle, diamond, cross
style(border_color = ) any CSS color name or hex
style(border_size = ) pixels

A point is a closed glyph, a shape with an inside to fill, so it takes more settings than most marks. It is the only mark that picks a shape, and one of the five marks that draw a border. size is the radius in pixels, and border_* draws that border distinct from the fill.

A point layer can carry several of them at once:

data(gapminder_2007) + point + x(gdp) + y(life) +
  style(shape = "diamond", size = 7, color = "steelblue",
        border_color = "navy", border_size = 1.5) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("A shape, a size, and a border")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  style(shape = "diamond", size = 7, color = "steelblue",
        border_color = "navy", border_size = 1.5) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("A shape, a size, and a border"))
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  style(shape = "diamond", size = 7, color = "steelblue", border_color = "navy", border_size = 1.5) +
  x_label("GDP per person") + y_label("Life expectancy") +
  title("A shape, a size, and a border")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  style({ shape: "diamond", size: 7, color: "steelblue",
  border_color: "navy", border_size: 1.5 }), x_label("GDP per person"),
  y_label("Life expectancy"), title("A shape, a size, and a border"))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 A shape, a size, and a border Life expectancy GDP per person

“Given gapminder 2007: points, x is gdp, y is life, colored steelblue, with shape diamond, size 7, border color navy and border size 1.5.”

The navy border is what separates the overlapping points, which is what a border is for in a dense cloud. A cross has no fill to outline, so border_color and border_size change nothing on that shape.

pattern is not on the list. A texture needs an area to fill or a stroke to dash, and a small glyph has neither, so a point’s only form is its shape; What it refuses shows the refusal. When points overlap, a border keeps them apart, and so does jitter, one of the four collision modifiers: point * jitter(0.4) moves each point a little along the categorical axis, by that amount, as the strip plot showed.

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.

8.10 What it refuses

A point is a glyph, and the two refusals below both follow from that.

size shows an amount, so it needs a column of amounts:

data(gapminder_2007) + point + x(gdp) + y(life) + size(continent)
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + size(col.continent)
data(gapminder_2007) + point + x(:gdp) + y(:life) + size(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  size(col.continent))
Error:
! gog: `size(continent)` maps a categorical (text) column, but `size` on `point` needs a continuous (numeric) column. Use `color`, `shape`, or `pattern` to distinguish categories.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Continents are names, not amounts, and a larger dot would claim an order the data does not have. The refusal names three channels that separate categories: color, shape and pattern. A point takes the first two, and the next section shows why it refuses pattern.

The second refusal is texture. A pattern is a stroke’s dash or a fill’s hatch, and a small closed glyph is neither:

data(gapminder_2007) + point + x(gdp) + y(life) + style(pattern = "dashed")
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + style(pattern = "dashed")
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  style(pattern = "dashed")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  style({ pattern: "dashed" }))
Error:
! gog: `style(pattern = )` is a stroke's dash or a fill's texture, and a `point` 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.

Both messages send you to shape, which is the one form a point can change.