Skip to content

Commit 22691ab

Browse files
daniel-barnetthadley
authored andcommitted
Shapes provided via strings instead of integers (#2338)
* Add shape string translate function * Call translate_shape_string from geom_point if shape is a character * Call translate_shape_string from scale_shape_manual if character * Tidy-up/changing pch strings * Updated NEWS.md * Sryle fixes * Style fixes * Remove paste() from stop() * Update translate_shape_string() function * Remove shape string translation from manual scales (is superfluous) * Add shape string translation to draw_key_point() * Strip names from translated shape integer vector * translate_shape_string() tests * Rename shape translate test file * Fix style * Adhere to error message guidelines * Remove superfluous tests and merge two tests for invalidity * Adjust geom-point tests * Remove merge remnants * Add "triangle square" as an alias for "square triangle" shape * Add shape name example in aesthetic vignette * Quick formatting adjustments * Replace startsWith for <= 3.2 compatibility
1 parent 9cefab7 commit 22691ab

File tree

5 files changed

+152
-1
lines changed

5 files changed

+152
-1
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ up correct aspect ratio, and draws a graticule.
172172
* `guide_train()`, `guide_merge()`, `guide_geom()`, and `guide_gengrob()`
173173
are now exported as they are needed if you want to design your own guide.
174174
They are not currently documented; use at your own risk (#2528).
175+
176+
* Shapes can now be provided using strings instead of integers (i.e.
177+
`geom_point(shape = "diamond")`) (@daniel-barnett, #2075).
175178

176179
## Breaking changes
177180

R/geom-point.r

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ GeomPoint <- ggproto("GeomPoint", Geom,
126126
),
127127

128128
draw_panel = function(data, panel_params, coord, na.rm = FALSE) {
129+
if (is.character(data$shape)) {
130+
data$shape <- translate_shape_string(data$shape)
131+
}
132+
129133
coords <- coord$transform(data, panel_params)
130134
ggname("geom_point",
131135
pointsGrob(
@@ -144,3 +148,91 @@ GeomPoint <- ggproto("GeomPoint", Geom,
144148

145149
draw_key = draw_key_point
146150
)
151+
152+
translate_shape_string <- function(shape_string) {
153+
if (nchar(shape_string[1]) == 1) {
154+
return(shape_string)
155+
}
156+
157+
pch_table <- c(
158+
"square open" = 0,
159+
"circle open" = 1,
160+
"triangle open" = 2,
161+
"plus" = 3,
162+
"cross" = 4,
163+
"diamond open" = 5,
164+
"triangle down open" = 6,
165+
"square cross" = 7,
166+
"asterisk" = 8,
167+
"diamond plus" = 9,
168+
"circle plus" = 10,
169+
"star" = 11,
170+
"square plus" = 12,
171+
"circle cross" = 13,
172+
"square triangle" = 14,
173+
"triangle square" = 14,
174+
"square" = 15,
175+
"circle small" = 16,
176+
"triangle" = 17,
177+
"diamond" = 18,
178+
"circle" = 19,
179+
"bullet" = 20,
180+
"circle filled" = 21,
181+
"square filled" = 22,
182+
"diamond filled" = 23,
183+
"triangle filled" = 24,
184+
"triangle down filled" = 25
185+
)
186+
187+
shape_match <- charmatch(shape_string, names(pch_table))
188+
189+
invalid_strings <- is.na(shape_match)
190+
nonunique_strings <- shape_match == 0
191+
192+
if (any(invalid_strings)) {
193+
bad_string <- unique(shape_string[invalid_strings])
194+
n_bad <- length(bad_string)
195+
196+
collapsed_names <- sprintf("\n* '%s'", bad_string[1:min(5, n_bad)])
197+
198+
more_problems <- if (n_bad > 5) {
199+
sprintf("\n* ... and %d more problem%s", n_bad - 5, ifelse(n_bad > 6, "s", ""))
200+
}
201+
202+
stop(
203+
"Can't find shape name:",
204+
collapsed_names,
205+
more_problems,
206+
call. = FALSE
207+
)
208+
}
209+
210+
if (any(nonunique_strings)) {
211+
bad_string <- unique(shape_string[nonunique_strings])
212+
n_bad <- length(bad_string)
213+
214+
n_matches <- vapply(
215+
bad_string[1:min(5, n_bad)],
216+
function(shape_string) sum(grepl(paste0("^", shape_string), names(pch_table))),
217+
integer(1)
218+
)
219+
220+
collapsed_names <- sprintf(
221+
"\n* '%s' partially matches %d shape names",
222+
bad_string[1:min(5, n_bad)], n_matches
223+
)
224+
225+
more_problems <- if (n_bad > 5) {
226+
sprintf("\n* ... and %d more problem%s", n_bad - 5, ifelse(n_bad > 6, "s", ""))
227+
}
228+
229+
stop(
230+
"Shape names must be unambiguous:",
231+
collapsed_names,
232+
more_problems,
233+
call. = FALSE
234+
)
235+
}
236+
237+
unname(pch_table[shape_match])
238+
}

R/legend-draw.r

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ NULL
1515
#' @export
1616
#' @rdname draw_key
1717
draw_key_point <- function(data, params, size) {
18+
if (is.character(data$shape)) {
19+
data$shape <- translate_shape_string(data$shape)
20+
}
21+
1822
pointsGrob(0.5, 0.5,
1923
pch = data$shape,
2024
gp = gpar(

tests/testthat/test-geom-point.R

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
context("geom-point")
2+
3+
test_that("single strings translate to their corresponding integers", {
4+
expect_equal(translate_shape_string("square open"), 0)
5+
})
6+
7+
test_that("vectors of strings translate to corresponding integers", {
8+
shape_strings <- c(
9+
"square open",
10+
"circle open",
11+
"square open",
12+
"triangle open"
13+
)
14+
15+
expect_equal(translate_shape_string(shape_strings), c(0, 1, 0, 2))
16+
})
17+
18+
test_that("single characters are not translated to integers", {
19+
expect_equal(translate_shape_string(letters), letters)
20+
expect_equal(translate_shape_string(as.character(0:9)), as.character(0:9))
21+
})
22+
23+
test_that("invalid shape names raise an error", {
24+
expect_error(translate_shape_string("void"), "Can't find shape name")
25+
expect_error(translate_shape_string("tri"), "Shape names must be unambiguous")
26+
})

vignettes/ggplot2-specs.Rmd

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ The border of the polygon is controlled by the `colour`, `linetype`, and `size`
143143
144144
### Shape {#sec:shape-spec}
145145
146-
Shapes take four types of values:
146+
Shapes take five types of values:
147147
148148
* An __integer__ in $[0, 25]$:
149149
@@ -161,6 +161,32 @@ Shapes take four types of values:
161161
scale_x_continuous(NULL, breaks = NULL) +
162162
scale_y_continuous(NULL, breaks = NULL)
163163
```
164+
165+
* The __name__ of the shape:
166+
167+
```{r out.width = "90%", fig.asp = 0.4, fig.width = 8}
168+
shape_names <- c(
169+
"circle", paste("circle", c("open", "filled", "cross", "plus", "small")), "bullet",
170+
"square", paste("square", c("open", "filled", "cross", "plus", "triangle")),
171+
"diamond", paste("diamond", c("open", "filled", "plus")),
172+
"triangle", paste("triangle", c("open", "filled", "square")),
173+
paste("triangle down", c("open", "filled")),
174+
"plus", "cross", "asterisk"
175+
)
176+
177+
shapes <- data.frame(
178+
shape_names = shape_names,
179+
x = c(1:7, 1:6, 1:3, 5, 1:3, 6, 2:3, 1:3),
180+
y = -rep(1:6, c(7, 6, 4, 4, 2, 3))
181+
)
182+
183+
ggplot(shapes, aes(x, y)) +
184+
geom_point(aes(shape = shape_names), fill = "red", size = 5) +
185+
geom_text(aes(label = shape_names), nudge_y = -0.3, size = 3.5) +
186+
scale_shape_identity() +
187+
scale_x_continuous(NULL, breaks = NULL) +
188+
scale_y_continuous(NULL, breaks = NULL)
189+
```
164190

165191
* A __single character__, to use that character as a plotting symbol.
166192

0 commit comments

Comments
 (0)