ggpointless
is a small extension of the ggplot2
package
that provides additional layers:

geom_pointless()
&stat_pointless()

geom_lexis()
&stat_lexis()

geom_chaikin()
&stat_chaikin()

geom_catenary()
&stat_catenary()
geom_pointless
geom_pointless()
is a layer to easily add minimal
emphasis to your plots. The function takes it’s power from
stat_pointless()
, which does all the work, but is not
usually in the spotlight.
library(ggplot2)
library(ggpointless)
x < seq(pi, pi, length.out = 100)
y < outer(x, 1:5, function(x, y) sin(x * y))
df1 < data.frame(
var1 = x,
var2 = rowSums(y)
)
p < ggplot(df1, aes(x = var1, y = var2))
p + geom_pointless(location = c("first", "last", "minimum", "maximum"))
As you see, just adding geom_pointless()
to
ggplot(...)
is not terribly useful on its own but when it
teams up with geom_line()
and friends, hopefully.
p < p + geom_line()
p + geom_pointless(location = "all", size = 3)
geom_pointless()
behaves like geom_point()
does with the addition of a location
argument. You can set
it to "first"
, "last"
(the default),
"minimum"
, "maximum"
, and "all"
;
where "all"
is just shorthand to select
"first"
, "last"
, "minimum"
and
"maximum"
.
In addition, you can use the computed variable location
and map it to an aesthetic, e.g. color
.
p + geom_pointless(aes(color = after_stat(location)),
location = "all",
size = 3
) +
theme(legend.position = "bottom")
Order and orientation
The locations are determined in the order in which they appear in the
data, like geom_path()
does compared to
geom_line()
. This can be seen in the next example, with
sample data kindly taken from the geomtextpath
package:
x < seq(5, 1, length.out = 1000) * pi
spiral < data.frame(
var1 = sin(x) * 1:1000,
var2 = cos(x) * 1:1000
)
p < ggplot(spiral) +
geom_path() +
coord_equal(xlim = c(1000, 1000), ylim = c(1000, 1000)) +
theme(legend.position = "none")
p + aes(x = var1, y = var2) +
geom_pointless(aes(color = after_stat(location)),
location = "all",
size = 3
) +
labs(subtitle = "orientation = 'x'")
p + aes(y = var1, x = var2) +
geom_pointless(aes(color = after_stat(location)),
location = "all",
size = 3
) +
labs(subtitle = "orientation = 'y'")
As you see from the first of the last two examples
"first"
and "minimum"
overlap, and
"first"
wins over "minimum"
. If
location
is set to "all"
, then the order in
which points are plotted from top to bottom is: "first"
> "last"
> "minimum"
>
"maximum"
.
Otherwise, the order is determined as specified in the
location
argument, which also applies to the order of the
legend key labels.
cols < c(
"first" = "#f8766d",
"last" = "#7cae00",
"minimum" = "#00bfc4",
"maximum" = "#c77cff"
)
df2 < data.frame(
var1 = 1:2,
var2 = 1:2
)
p < ggplot(df2, aes(x = var1, y = var2)) +
geom_path() +
coord_equal() +
scale_color_manual(values = cols)
# same as location = 'all'
p + geom_pointless(aes(color = after_stat(location)),
location = c("first", "last", "minimum", "maximum"),
size = 3
) +
labs(subtitle = "same as location = 'all'")
# reversed order
p + geom_pointless(aes(color = after_stat(location)),
location = c("maximum", "minimum", "last", "first"),
size = 3
) +
labs(subtitle = "custom order")
# same as location = 'all' again
p + geom_pointless(aes(color = after_stat(location)),
location = c("maximum", "minimum", "last", "first", "all"),
size = 3
) +
labs(subtitle = "same as location = 'all' again")
Pick a different geom
Just like all stat_*
functions,
stat_pointless()
has a default geom, which is
"point"
. This means in reverse that you can highlight
e.g. minimum and maximum in another way, for example with a horizontal
line.
set.seed(42)
ggplot(data.frame(x = 1:10, y = sample(1:10)), aes(x, y)) +
geom_line() +
stat_pointless(
aes(yintercept = y, color = after_stat(location)),
location = c("minimum", "maximum"),
geom = "hline"
) +
guides(color = guide_legend(reverse = TRUE))
Plotting lifelines with geom_lexis
geom_lexis()
draws a lifeline for an event from it’s
start to it’s end. The required aesthetics are x
and
xend
. Here is an example:
df1 < data.frame(
key = c("A", "B", "B", "C", "D"),
x = c(0, 1, 6, 5, 6),
y = c(5, 4, 10, 8, 10)
)
p < ggplot(df1, aes(x = x, xend = y, color = key)) +
coord_equal()
p + geom_lexis()
Also, if there is a gap in an event a horizontal line is drawn, which
can be hidden setting gap_filler = FALSE
.
p + geom_lexis(gap_filler = FALSE)
You can further style the appearance of your plot using the
additional arguments. If you e.g. want to make a visual distinction
between the ascending lines and the connecting lines, use
after_stat()
to map the type
variable to the
linetype aesthetic (or any other aesthetic). The variable
type
is created by geom_lexis()
and takes two
values: “solid” and “dotted”; so you might also want to call
scale_linettype_identity
.
p + geom_lexis(
aes(linetype = after_stat(type)),
point_show = FALSE
) +
scale_linetype_identity()
Date and POSIXct classes
You see the coordinates on the vertical yaxis show the difference
between x
and xend
aesthetics. The “magic” of
geom_lexis()
happens in stat_lexis()
when the
input data is transformed and the calculations are performed.
df1 < data.frame(
start = c(2019, 2021),
end = c(2022, 2022),
key = c("A", "B")
)
ggplot(df1, aes(x = start, xend = end, group = key)) +
geom_lexis() +
coord_fixed()
Keeping in mind that dates are internally represented as the number of days, and the POSIXct class in turn represents seconds since some origin, the yscale values in the next plots should come as no surprise.
# Date
fun < function(i, class) as.Date(paste0(i, "0101"))
df1[, c("start", "end")] < lapply(df1[, c("start", "end")], fun)
p1 < ggplot(df1, aes(x = start, xend = end, group = key)) +
geom_lexis() +
labs(y = "days") +
coord_fixed()
# POSIXct
df2 < df1
df2[, c("start", "end")] < lapply(df2[, c("start", "end")], as.POSIXct)
p2 < ggplot(df2, aes(x = start, xend = end, group = key)) +
geom_lexis() +
labs(y = "seconds") +
coord_fixed()
p1
p2
In order to change the breaks and labels of the vertical scale to, say, years, we make the assumption that 1 year has 365.25 days, or 365.25 * 86400 seconds.
# years, roughly
p1 +
scale_y_continuous(
breaks = 0:3 * 365.25, # or for p2: 0:3*365.25*86400
labels = function(i) floor(i / 365.25) # floor(i / 365.25*86400)
) +
labs(y = "years")
geom_chaikin
The algorithm of the geom_chaikin()
function iteratively
cuts off the ragged corners, as you can see in the example below.
set.seed(42)
dat < data.frame(
x = seq.int(10),
y = sample(15:30, 10)
)
p1 < ggplot(dat, aes(x, y)) +
geom_line(linetype = "12")
p1 +
geom_chaikin()
geom_catenary
A catenary is the curve of an hanging chain or cable under its own weight when supported only at its ends (see Wikipedia for more information).
ggplot(data.frame(x = c(0, 1), y = c(1, 1)), aes(x, y)) +
geom_catenary() +
ylim(0, 1)
#> Set chainLength to 2
geom_catenary()
calculates a default value for the
length of the chain that is set to 2 times the Euclidean distance of all
x/y coordinates. Use the `chainLength argument to overwrite the
default.
ggplot(data.frame(x = c(0, 1), y = c(1, 1)), aes(x, y)) +
geom_catenary(chainLength = 1.5) +
ylim(0, 1)
Multiple x/y coordinates are supported too.
ggplot(data.frame(x = c(0, 1, 4), y = c(1, 1, 1)), aes(x, y)) +
geom_catenary()
#> Set chainLength to 8
If you set chainLength
to a value that is too short to
connect two points, a straight line is drawn and a message is shown.
ggplot(data.frame(x = c(0, 1, 4), y = c(1, 1, 1)), aes(x, y)) +
geom_catenary(chainLength = 4)
#> Minimum chain length from '(1, 1)' to '(4, 1)' is 3, drawing a straight line.
If you want to draw a chain where each piece has a different
chainLength
value, remember you can add a list to a ggplot
object:
ggplot(data.frame(x = c(0, 1), y = 1),
aes(x, y)) +
lapply(2:10, function(chainLength) {
geom_catenary(chainLength = chainLength)
}
)
Data
The following data sets are shipped with the ggpointless
package:

co2_ml
: CO_{2} records taken at Mauna Loa 
covid_vac
: COVID19 Cases and Deaths by Vaccination Status 
female_leaders
: Elected and appointed female heads of state and government
See the vignette("examples")
for possible use cases.