Skip to content

Commit 63b263b

Browse files
authored
New stat: stat_connect() (#6329)
* draft `stat_connect()` * document * broaden required aes * try to detect orientation better * add tests * add news bullet * add to pkgdown
1 parent 7e67db3 commit 63b263b

File tree

9 files changed

+436
-5
lines changed

9 files changed

+436
-5
lines changed

DESCRIPTION

+1
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ Collate:
247247
'stat-bindot.R'
248248
'stat-binhex.R'
249249
'stat-boxplot.R'
250+
'stat-connect.R'
250251
'stat-contour.R'
251252
'stat-count.R'
252253
'stat-density-2d.R'

NAMESPACE

+2
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ export(StatBin2d)
257257
export(StatBindot)
258258
export(StatBinhex)
259259
export(StatBoxplot)
260+
export(StatConnect)
260261
export(StatContour)
261262
export(StatContourFilled)
262263
export(StatCount)
@@ -684,6 +685,7 @@ export(stat_bin_2d)
684685
export(stat_bin_hex)
685686
export(stat_binhex)
686687
export(stat_boxplot)
688+
export(stat_connect)
687689
export(stat_contour)
688690
export(stat_contour_filled)
689691
export(stat_count)

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ggplot2 (development version)
22

3+
* New `stat_connect()` to connect points via steps or other shapes
4+
(@teunbrand, #6228)
35
* Fixed regression with incorrectly drawn gridlines when using `coord_flip()`
46
(@teunbrand, #6293).
57
* Deprecated functions and arguments prior to ggplot2 3.0.0 throw errors instead

R/stat-connect.R

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#' Connect observations
2+
#'
3+
#' Connect successive points with lines of different shapes.
4+
#'
5+
#' @inheritParams layer
6+
#' @inheritParams geom_point
7+
#' @param connection A specification of how two points are connected. Can be one
8+
#' of the folloing:
9+
#' * A string giving a named connection. These options are:
10+
#' * `"hv"` to first jump horizontally, then vertically.
11+
#' * `"vh"` to first jump vertically, then horizontally.
12+
#' * `"mid"` to step half-way between adjacent x-values.
13+
#' * `"linear"` to use a straight segment.
14+
#' * A numeric matrix with two columns giving x and y coordinates respectively.
15+
#' The coordinates should describe points on a path that connect point A
16+
#' at location (0, 0) and point B at location (1, 1). At least one of these
17+
#' two points is expected to be included in the coordinates.
18+
#'
19+
#' @eval rd_aesthetics("stat", "connect")
20+
#' @export
21+
#'
22+
#' @examples
23+
#' ggplot(head(economics, 20), aes(date, unemploy)) +
24+
#' stat_connect(connection = "hv")
25+
#'
26+
#' # Setup custom connections
27+
#' x <- seq(0, 1, length.out = 20)[-1]
28+
#' smooth <- cbind(x, scales::rescale(1 / (1 + exp(-(x * 10 - 5)))))
29+
#' zigzag <- cbind(c(0.4, 0.6, 1), c(0.75, 0.25, 1))
30+
#'
31+
#' ggplot(head(economics, 10), aes(date, unemploy)) +
32+
#' geom_point() +
33+
#' stat_connect(aes(colour = "zigzag"), connection = zigzag) +
34+
#' stat_connect(aes(colour = "smooth"), connection = smooth)
35+
stat_connect <- function(
36+
mapping = NULL,
37+
data = NULL,
38+
geom = "path",
39+
position = "identity",
40+
...,
41+
connection = "hv",
42+
na.rm = FALSE,
43+
show.legend = NA,
44+
inherit.aes = TRUE) {
45+
layer(
46+
data = data,
47+
mapping = mapping,
48+
stat = StatConnect,
49+
geom = geom,
50+
position = position,
51+
show.legend = show.legend,
52+
inherit.aes = inherit.aes,
53+
params = list2(
54+
na.rm = na.rm,
55+
connection = connection,
56+
...
57+
)
58+
)
59+
}
60+
61+
#' @rdname ggplot2-ggproto
62+
#' @format NULL
63+
#' @usage NULL
64+
#' @export
65+
StatConnect <- ggproto(
66+
"StatConnect", Stat,
67+
68+
required_aes = c("x|xmin|xmax", "y|ymin|ymax"),
69+
70+
setup_params = function(data, params) {
71+
params$flipped_aes <- has_flipped_aes(
72+
data, params,
73+
range_is_orthogonal = TRUE, ambiguous = TRUE
74+
)
75+
76+
connection <- params$connection %||% "hv"
77+
78+
if (is.character(connection)) {
79+
check_string(connection)
80+
connection <- switch(
81+
arg_match0(connection, c("hv", "vh", "mid", "linear")),
82+
hv = matrix(c(1, 1, 0, 1), 2, 2),
83+
vh = matrix(c(0, 0, 0, 1), 2, 2),
84+
mid = matrix(c(0.5, 0.5, 0, 1), 2, 2),
85+
linear = matrix(c(0, 1, 0, 1), 2, 2)
86+
)
87+
}
88+
89+
if (!is.matrix(connection) ||
90+
!typeof(connection) %in% c("integer", "double") ||
91+
!identical(dim(connection)[2], 2L)) {
92+
extra <- ""
93+
if (!is.null(dim(connection)[2])) {
94+
extra <- paste0(" with ", dim(connection)[2], " column(s)")
95+
}
96+
cli::cli_abort(
97+
"{.arg connection} must be a numeric {.cls matrix} with 2 columns, \\
98+
not {.obj_type_friendly {connection}}{extra}."
99+
)
100+
}
101+
102+
if (any(!is.finite(connection))) {
103+
cli::cli_abort(
104+
"{.arg connection} cannot contain missing or other non-finite values."
105+
)
106+
}
107+
108+
if (nrow(connection) < 1) {
109+
connection <- NULL
110+
}
111+
112+
params$connection <- connection
113+
params
114+
},
115+
116+
compute_group = function(data, scales, connection = "hv", flipped_aes = FALSE) {
117+
118+
data <- flip_data(data, flipped_aes)
119+
120+
n <- nrow(data)
121+
if (n <= 1) {
122+
return(vec_slice(data, 0))
123+
}
124+
125+
if (!is.matrix(connection)) {
126+
return(data)
127+
}
128+
m <- nrow(connection)
129+
130+
before <- rep(seq_len(n - 1), each = m)
131+
after <- rep(seq_len(n)[-1], each = m)
132+
133+
data <- vec_slice(data, order(data$x %||% data$xmin))
134+
135+
# Interpolate x
136+
# Note that `length(x) != length(xjust)`, but these are kept in sync due to
137+
# the matrix recycling rules (effectively `rep(xjust, ncol(x))`)
138+
x <- as.matrix(data[intersect(names(data), ggplot_global$x_aes)])
139+
xjust <- rep(connection[, 1], n - 1L)
140+
x <- vec_slice(x, before) * (1 - xjust) + vec_slice(x, after) * xjust
141+
142+
# Interpolate y
143+
y <- as.matrix(data[intersect(names(data), ggplot_global$y_aes)])
144+
yjust <- rep(connection[, 2], n - 1L)
145+
y <- vec_slice(y, before) * (1 - yjust) + vec_slice(y, after) * yjust
146+
147+
# Reconstitute data
148+
new_data <- vec_slice(data, before)
149+
new_data[colnames(x)] <- split_matrix(x)
150+
new_data[colnames(y)] <- split_matrix(y)
151+
152+
# Esnure data starts and ends are intact
153+
if (!all(connection[1, ] == c(0, 0))) {
154+
new_data <- vec_c(vec_slice(data, 1), new_data)
155+
}
156+
if (!all(connection[m, ] == c(1, 1))) {
157+
new_data <- vec_c(new_data, vec_slice(data, n))
158+
}
159+
flip_data(new_data, flipped_aes)
160+
}
161+
162+
)

_pkgdown.yml

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ reference:
6969
- stat_unique
7070
- stat_sf_coordinates
7171
- stat_manual
72+
- stat_connect
7273
- after_stat
7374

7475
- subtitle: Position adjustment

man/ggplot2-ggproto.Rd

+6-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/stat_connect.Rd

+153
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)