8  Point

How do two measurements move together? point draws one glyph per row of data at the (x, y) position.

The plot it makes is the scatterplot, which is the youngest of the plain chart types by half a century. Playfair had the line and the bar in 1786, and the pie by 1801. A picture that sets two measured columns against each other 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 to read a trend off them (Friendly & Denis, 2005). Other names for it are the same picture: scatter diagram, scattergram, and, once size carries a third column, the bubble chart.

None of those is an atom here, and the reason is the whole design. point names the mark, not the chart. point + x(gdp) + y(life) is a scatterplot and point + x(continent) + y(life) is a strip plot, and nothing about the mark changed between them: the chart name is a property of the sentence, not a word in the vocabulary. That is why the names live in an appendix, Chart names, instead of in the kernel.

8.1 Basic usage

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

8.2 The dot is a default, not the mark

Reach for point and you get a dot, and treating it as the dot mark will serve you well almost every time. But the dot is a default, not the mark’s whole identity. A point is a glyph at a position, and the glyph is yours to change: shape swaps the dot for one of 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

So “a dot” really means “the first of five shapes, drawn when you name none.” 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 closed set of five (shape); a text mark takes it from a column: the string itself (label). Put both on one plot and each row is located by a dot and named by its label:

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

8.3 A categorical x: the strip plot

x accepts a text column as readily as a numeric one. Each category gets a position on the axis and every row is drawn at it, showing 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, no per-mark 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

The strip plot shows Africa’s spread and its overlap with Asia; the bar chart shows neither. Its one weakness: points at the same category sit on one line, so heavy overlap can hide density. opacity helps, and point * jitter spreads the pile sideways to reveal it outright, nudging each point along the category axis only, never off its measured value.

The other way to resolve that overlap is to stop hiding the coincident points and count them: point * bin * stack piles one dot per observation up the measure axis, which is the dot plot. One mark, two overlap answers: a nudge along a category, a pile along a measure.

8.3.1 Either axis: a horizontal strip

Because it is the same rule, the category can sit on y instead of x. The strip then lies down, the same countries and the same distributions, read left to right along each row:

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

This is the orientation freedom bar already has, and gog reads it the same way: whichever axis carries the category decides which way the plot faces; there is no flip atom to set. Reach for the horizontal form when the category names are long enough to collide across the bottom, or when there are enough of them that rows stacked down the page scan more easily than columns across it.

8.4 Color

Bind color to a categorical column to color points by group. gog assigns a distinct color from a 10-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

A color legend is generated automatically whenever color is active.

8.5 Shape

shape maps a categorical column to one of five glyphs: circle, square, triangle, diamond, or cross.

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

Combine color and shape on the same column to make groups distinguishable for colorblind readers and in black-and-white print:

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

size maps a numeric column to the point radius (3 – 12 px by default).

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

A size legend showing the min, midpoint, and max values is generated automatically.

8.7 Opacity

opacity maps a numeric column (0 – 1) to transparency. Useful for dense scatterplots where overplotting hides structure.

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

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

That example maps opacity to a column. For plain overplotting relief you want the same opacity for every point, which is a setting rather than a mapping. style(opacity = ), and no legend:

data(gm_all) + point + x(gdp) + y(life) +
  style(opacity = 0.2, size = 3) +
  title("Every point at one opacity: see Setting vs mapping")
(data(gm_all) + point + x(col.gdp) + y(col.life) +
  style(opacity = 0.2, size = 3) +
  title("Every point at one opacity: see Setting vs mapping"))
data(gm_all) + point + x(:gdp) + y(:life) +
  style(opacity = 0.2, size = 3) +
  title("Every point at one opacity: see Setting vs mapping")
plot(data(gm_all), point, x(col.gdp), y(col.life),
  style({ opacity: 0.2, size: 3 }),
  title("Every point at one opacity: see Setting vs mapping"))
0K 50K 100K 40 60 80 Every point at one opacity: see Setting vs mapping Life Gdp

8.8 All channels at once

All decorative channels can be combined freely:

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 Available shapes

Index Shape
0 Circle (default)
1 Square
2 Triangle
3 Diamond
4 Cross

Shape assignment is automatic and consistent: the first unique category always gets circle, the second gets square, and so on.

8.10 What you can set

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

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

A point is a closed glyph, and its row is the widest in the book because of it: it is the only mark that picks a shape, and one of five that carry a rim. size is the radius in pixels, and border_* draws that rim distinct from the fill.

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

The white rim is doing real work there: it separates points that overlap, which is what a rim is for on a glyph dense enough to pile up. A cross has no fill to outline, so a border on that shape has nothing to draw on.

There is no pattern. A texture needs an area to fill or a stroke to dash, and a 5-pixel glyph is neither; a point’s form is shape. The other way to answer overplotting is the collision modifier, which takes a parameter of its own: point * jitter(0.4) spreads coincident points along a categorical axis.

8.11 What it refuses

A point is a glyph, and both refusals worth meeting follow from that.

size measures, so it needs something to measure:

data(gapminder_2007) + point + x(gdp) + y(life) + size(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 the three channels that do separate categories.

Texture is the other. A pattern is a stroke’s dash or a fill’s hatch, and a glyph five pixels across is neither:

data(gapminder_2007) + point + x(gdp) + y(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`) 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 refusals point the same way, at shape, which is a point’s own form.