Search code examples
rggplot2unicodeshapes

Custom shape in ggplot (geom_point) - use brain emoji 🧠


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?


Solution

  • R requires \U rather than \u for codepoints longer than four hex digits

    This 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:

    1. If the argument has one character, e.g. "\u2764" or "❤", it returns that value to be plotted.
    2. Otherwise it matches it to R's 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()
    

    enter image description here