This is a follow-up to this question (part 2): Custom shape in ggplot (geom_point) (and this is part 1)
Which provided a solution to produce custom ggplot2 shapes in the form of skulls 💀 (or hearts ❤):
library(ggplot2)
df <- read.table(text="x y
1 3
2 4
3 6
4 7", header=TRUE)
ggplot(data = df, aes(x =x, y=y)) +
geom_point(shape="\u2764",
colour = "red",
fill = "white",
size = 6)
Created on 2023-12-25 with reprex v2.0.2
But a problem arises because I actually want to plot a brain 🧠...
ggplot(data = df, aes(x =x, y=y)) +
geom_point(shape="\u1F9E0",
colour = "red",
fill = "white",
size = 6)
#> Error in `geom_point()`:
#> ! Problem while converting geom to grob.
#> ℹ Error occurred in the 1st layer.
#> Caused by error in `translate_shape_string()`:
#> ! Shape aesthetic contains invalid value: "ᾞ0"
#> Backtrace:
#> ▆
#> 1. ├─base::tryCatch(...)
#> 2. │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#> 3. │ ├─base (local) tryCatchOne(...)
#> 4. │ │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#> 5. │ └─base (local) tryCatchList(expr, names[-nh], parentenv, handlers[-nh])
#> 6. │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 7. │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#> 8. ├─base::withCallingHandlers(...)
#> 9. ├─base::saveRDS(...)
#> 10. ├─base::do.call(...)
#> 11. ├─base (local) `<fn>`(...)
#> 12. └─global `<fn>`(input = base::quote("super-sable_reprex.R"))
#> 13. └─rmarkdown::render(input, quiet = TRUE, envir = globalenv(), encoding = "UTF-8")
#> 14. └─knitr::knit(knit_input, knit_output, envir = envir, quiet = quiet)
#> 15. └─knitr:::process_file(text, output)
#> 16. ├─knitr:::handle_error(...)
#> 17. │ └─base::withCallingHandlers(...)
#> 18. ├─base::withCallingHandlers(...)
#> 19. ├─knitr:::process_group(group)
#> 20. └─knitr:::process_group.block(group)
#> 21. └─knitr:::call_block(x)
#> 22. └─knitr:::block_exec(params)
#> 23. └─knitr:::eng_r(options)
#> 24. ├─knitr:::in_input_dir(...)
#> 25. │ └─knitr:::in_dir(input_dir(), expr)
#> 26. └─knitr (local) evaluate(...)
#> 27. └─evaluate::evaluate(...)
#> 28. └─evaluate:::evaluate_call(...)
#> 29. ├─evaluate (local) handle(...)
#> 30. │ └─base::try(f, silent = TRUE)
#> 31. │ └─base::tryCatch(...)
#> 32. │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#> 33. │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 34. │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#> 35. ├─base::withCallingHandlers(...)
#> 36. ├─base::withVisible(value_fun(ev$value, ev$visible))
#> 37. └─knitr (local) value_fun(ev$value, ev$visible)
#> 38. └─knitr (local) fun(x, options = options)
#> 39. ├─base::withVisible(knit_print(x, ...))
#> 40. ├─knitr::knit_print(x, ...)
#> 41. └─knitr:::knit_print.default(x, ...)
#> 42. └─evaluate (local) normal_print(x)
#> 43. ├─base::print(x)
#> 44. └─ggplot2:::print.ggplot(x)
#> 45. ├─ggplot2::ggplot_gtable(data)
#> 46. └─ggplot2:::ggplot_gtable.ggplot_built(data)
#> 47. └─ggplot2:::by_layer(...)
#> 48. ├─rlang::try_fetch(...)
#> 49. │ ├─base::tryCatch(...)
#> 50. │ │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#> 51. │ │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 52. │ │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#> 53. │ └─base::withCallingHandlers(...)
#> 54. └─ggplot2 (local) f(l = layers[[i]], d = data[[i]])
#> 55. └─l$draw_geom(d, layout)
#> 56. └─ggplot2 (local) draw_geom(..., self = self)
#> 57. └─self$geom$draw_layer(...)
#> 58. └─ggplot2 (local) draw_layer(..., self = self)
#> 59. └─base::lapply(...)
#> 60. └─ggplot2 (local) FUN(X[[i]], ...)
#> 61. ├─rlang::inject(self$draw_panel(data, panel_params, coord, !!!params))
#> 62. └─self$draw_panel(data, panel_params, coord, na.rm = FALSE)
#> 63. └─ggplot2 (local) draw_panel(..., self = self)
#> 64. └─ggplot2:::translate_shape_string(data$shape)
#> 65. └─cli::cli_abort("Shape aesthetic contains invalid value{?s}: {.val {bad_string}}")
#> 66. └─rlang::abort(...)
Created on 2023-12-25 with reprex v2.0.2
Is it possible at all? Could someone explain the issue with this particular emoji or why it isn't apparently supported?
\U
rather than \u
for codepoints longer than four hex digitsThis is to do with how R interprets "\u1f9e0"
. When you pass a value to the shape
parameter in geom_point()
, this is handled by ggplot2:::translate_shape_string()
. This function basically does the following:
"\u2764"
or "❤"
, it returns that value to be plotted.pch
symbols, e.g. "triangle down filled" = 25
.The problem is that while R understands "\u2764"
is ❤, "\u1f9e0"
is not interpreted as 🧠 but actually the string "ᾞ0"
:
nchar("\u2764", type = "chars") # heart
# [1] 1
nchar("\u1f9e0", type = "chars") # expecting brain (but it isn't)
# [1] 2
ggplot2
therefore treats it as a pch
name, which cannot be matched to a value and throws an error.
R's behaviour with utf-8 codepoint literals is documented (arguably a little obscurely) in Quotes
:
\unnnn
Unicode character with given code (1--4 hex digits)
\Unnnnnnnn
Unicode character with given code (1--8 hex digits)
There is a similar passage in Section 10.3.1 (Constants) in the R Language Definition.
As "\u2764"
(❤) is four digits, a lower case and upper case \u
have the same result. However, "\u1f9e0"
(🧠) is five hex digits, so using "\u"
R interprets it as two characters.
cat("\u1f9e0") # ᾞ0
identical("🧠", "\u1f9e0") # FALSE
as.hexmode(utf8ToInt("\u1f9e0"))
# [1] "1f9e" "0030"
cat("\u1f9e", "\u0030") # ᾞ 0
However, if we use "\U"
, R correctly understands that this is one character:
as.hexmode(utf8ToInt("\U1f9e0"))
# [1] "1f9e0"
nchar("\U{01f9e0}")
# [1] 1
identical("🧠", "\U1f9e0") # TRUE
cat("\U1f9e0") # 🧠
If we use this representation we will get the desired result:
ggplot(data = df, aes(x = x, y = y)) +
geom_point(
shape = "\U1f9e0",
colour = "red",
size = 10
) +
theme_bw()