Powered by AppSignal & Oban Pro

Overlapping points: geom_count

notebooks/geom_count.livemd

Overlapping points: geom_count

Mix.install([
  {:giggly, path: Path.join(__DIR__, "..")},
  {:kino, "~> 0.19"}
  ])

alias Giggly.Plot

mpg = Giggly.Datasets.mpg()
diamonds = Giggly.Datasets.diamonds()

render = fn plot ->
  plot
  |> Giggly.to_svg()
  |> IO.iodata_to_binary()
  |> Kino.Image.new(:svg)
end

About this notebook

This notebook follows the examples on ggplot2's reference page for geom_count(), which covers stat_sum() too, in the order the page gives them. Each example shows ggplot2's code first and then Giggly's. An example Giggly cannot draw yet keeps its ggplot2 code and names what it is waiting for, so the page is also a list of what remains.

geom_count() is a variant of geom_point() for discrete data whose points overlap: stat_sum() counts the rows at each position, and the count is mapped to the point's area. Giggly has both, Giggly.Plot.geom_count/3 and Giggly.Plot.stat_sum/3, the count n and its proportion of the group, prop, both read with {:after_stat, [...]}, and the size scales and legends that read them back. A plot is described with Giggly.Plot and drawn with Giggly.to_svg/2, which returns an SVG document. The setup cell aliases Giggly.Plot, so the cells write Plot.new/2, and defines render, which draws a plot and shows it below the cell with Kino.Image. The first examples use Giggly.Datasets.mpg/0, ggplot2's mpg: fuel economy for 234 cars of 38 popular models from 1999 to 2008, which the setup cell binds to mpg, ggplot2's name for it. The rest use ggplot2's diamonds, Giggly.Datasets.diamonds/0, which the setup cell binds to diamonds, and whose cut and clarity are ordered factors, as R's are, so each axis follows its levels.

Points that overlap

ggplot(mpg, aes(cty, hwy)) +
 geom_point()

cty and hwy are whole numbers of miles per gallon, so many cars share a point, and one point drawn over another looks like one.

mpg
|> Plot.new(%{x: :cty, y: :hwy})
|> Plot.geom_point()
|> render.()

Counting the points

ggplot(mpg, aes(cty, hwy)) +
 geom_count()

Each point's area grows with the number of cars at its place, and the legend reads the counts back.

p =
  mpg
  |> Plot.new(%{x: :cty, y: :hwy})
  |> Plot.geom_count()

render.(p)

Sizes by area

# Best used in conjunction with scale_size_area which ensures that
# counts of zero would be given size 0. Doesn't make much different
# here because the smallest count is already close to 0.
ggplot(mpg, aes(cty, hwy)) +
 geom_count() +
 scale_size_area()
p
|> Plot.scale_size_area()
|> render.()

Proportions of the whole panel

# Display proportions instead of counts -------------------------------------
# By default, all categorical variables in the plot form the groups.
# Specifying geom_count without a group identifier leads to a plot which is
# not useful:
d <- ggplot(diamonds, aes(x = cut, y = clarity))
d + geom_count(aes(size = after_stat(prop)))
# To correct this problem and achieve a more desirable plot, we need
# to specify which group the proportion is to be calculated over.
d + geom_count(aes(size = after_stat(prop), group = 1)) +
  scale_size_area(max_size = 10)

With each combination of cut and clarity a group of its own, every proportion is 1, so every point is the same size; group = 1 makes the whole panel one group, a constant in the mapping.

plot = Plot.new(diamonds, %{x: :cut, y: :clarity})

plot
|> Plot.geom_count(%{size: {:after_stat, [:prop]}})
|> render.()
plot
|> Plot.geom_count(%{size: {:after_stat, [:prop]}, group: 1})
|> Plot.scale_size_area(max_size: 10)
|> render.()

Proportions of each row or column

# Or group by x/y variables to have rows/columns sum to 1.
d + geom_count(aes(size = after_stat(prop), group = cut)) +
  scale_size_area(max_size = 10)
d + geom_count(aes(size = after_stat(prop), group = clarity)) +
  scale_size_area(max_size = 10)

Grouping by a column is a mapping of group.

plot
|> Plot.geom_count(%{size: {:after_stat, [:prop]}, group: :cut})
|> Plot.scale_size_area(max_size: 10)
|> render.()
plot
|> Plot.geom_count(%{size: {:after_stat, [:prop]}, group: :clarity})
|> Plot.scale_size_area(max_size: 10)
|> render.()