Подтвердить что ты не робот

Как динамически обернуть метку метки, используя ggplot2

Я ищу способ динамически обернуть текст метки полосы в вызове facet_wrap или facet_grid. Я нашел способ выполнить это с помощью strwrap, но мне нужно указать width, чтобы выход работал по желанию. Часто количество фасетов неизвестно заранее, поэтому этот метод требует от меня итеративного изменения параметра width на основе набора данных и размера графика. Можно ли динамически указать ширину для функции обертки, или есть еще один вариант для маркировки граней, которые будут работать лучше?

library(ggplot2)
df = expand.grid(group=paste(c("Very Very Very Long Group Name "), 1:9),
                 x=rnorm(5), y=rnorm(5), stringsAsFactors=FALSE)

df$groupwrap = unlist(lapply(strwrap(df$group, width=30, simplify=FALSE), paste, 
                             collapse="\n"))
p = ggplot(df) +
  geom_point(aes(x=x, y=y)) +
  facet_wrap(~groupwrap)

ОБНОВЛЕНИЕ. Основываясь на руководстве, представленном @baptiste и @thunk, я придумал вариант ниже. В настоящее время он работает только для определенного семейства шрифтов и размера, но в идеале нужно также использовать настройки по умолчанию theme. Возможно, у кого-то, у кого больше опыта ggplot2, есть предложения по улучшению.

library('grid')
grobs <- ggplotGrob(p)

sum = sum(sapply(grobs$width, function(x) convertWidth(x, "in")))
panels_width = par("din")[1] - sum  # inches

df$group = as.factor(df$group)
npanels = nlevels(df$group)
if (class(p$facet)[1] == "wrap") {
  cols = n2mfrow(npanels)[1]
} else {
  cols = npanels
}

ps = 12
family = "sans"
pad = 0.01  # inches
panel_width = panels_width / cols
char_width = strwidth(levels(df$group)[
  which.max(nchar(levels(df$group)))], units="inches", cex=ps / par("ps"), 
                      family=family) / max(nchar(levels(df$group)))
width = floor((panel_width - pad)/ char_width)  # characters

df$groupwrap = unlist(lapply(strwrap(df$group, width=width, simplify=FALSE), 
                             paste, collapse="\n"))
ggplot(df) +
  geom_point(aes(x=x, y=y)) +
  facet_wrap(~groupwrap) +
  theme(strip.text.x=element_text(size=ps, family=family))
4b9b3361

Ответ 1

Благодаря руководству от @baptiste и @thunk, я создал функцию ниже, которая, кажется, делает довольно хорошую работу по автоматической обмотке фасетных меток. Однако предложения по улучшению всегда приветствуются.

strwrap_strip_text = function(p, pad=0.05) { 
  # get facet font attributes
  th = theme_get()
  if (length(p$theme) > 0L)
    th = th + p$theme

  require("grid")
  grobs <- ggplotGrob(p)

  # wrap strip x text
  if ((class(p$facet)[1] == "grid" && !is.null(names(p$facet$cols))) ||
        class(p$facet)[1] == "wrap")
  {
    ps = calc_element("strip.text.x", th)[["size"]]
    family = calc_element("strip.text.x", th)[["family"]]
    face = calc_element("strip.text.x", th)[["face"]]

    if (class(p$facet)[1] == "wrap") {
      nm = names(p$facet$facets)
    } else {
      nm = names(p$facet$cols)
    }

    # get number of facet columns
    levs = levels(factor(p$data[[nm]]))
    npanels = length(levs)
    if (class(p$facet)[1] == "wrap") {
      cols = n2mfrow(npanels)[1]
    } else {
      cols = npanels
    }

    # get plot width
    sum = sum(sapply(grobs$width, function(x) convertWidth(x, "in")))
    panels_width = par("din")[1] - sum  # inches
    # determine strwrap width
    panel_width = panels_width / cols
    mx_ind = which.max(nchar(levs))
    char_width = strwidth(levs[mx_ind], units="inches", cex=ps / par("ps"), 
                          family=family, font=gpar(fontface=face)$font) / 
      nchar(levs[mx_ind])
    width = floor((panel_width - pad)/ char_width)  # characters

    # wrap facet text
    p$data[[nm]] = unlist(lapply(strwrap(p$data[[nm]], width=width, 
                                         simplify=FALSE), paste, collapse="\n"))
  }

  if (class(p$facet)[1] == "grid" && !is.null(names(p$facet$rows))) {  
    ps = calc_element("strip.text.y", th)[["size"]]
    family = calc_element("strip.text.y", th)[["family"]]
    face = calc_element("strip.text.y", th)[["face"]]

    nm = names(p$facet$rows)

    # get number of facet columns
    levs = levels(factor(p$data[[nm]]))
    rows = length(levs)

    # get plot height
    sum = sum(sapply(grobs$height, function(x) convertWidth(x, "in")))
    panels_height = par("din")[2] - sum  # inches
    # determine strwrap width
    panels_height = panels_height / rows
    mx_ind = which.max(nchar(levs))
    char_height = strwidth(levs[mx_ind], units="inches", cex=ps / par("ps"), 
                           family=family, font=gpar(fontface=face)$font) / 
      nchar(levs[mx_ind])
    width = floor((panels_height - pad)/ char_height)  # characters

    # wrap facet text
    p$data[[nm]] = unlist(lapply(strwrap(p$data[[nm]], width=width, 
                                         simplify=FALSE), paste, collapse="\n"))
  }

  invisible(p)
}

Чтобы использовать эту функцию, вызовите ее вместо print.

library(ggplot2)
df = expand.grid(group=paste(c("Very Very Very Long Group Name "), 1:4),
                 group1=paste(c("Very Very Very Long Group Name "), 5:8),
                 x=rnorm(5), y=rnorm(5), stringsAsFactors=FALSE)

p = ggplot(df) +
  geom_point(aes(x=x, y=y)) +
  facet_grid(group1~group)
strwrap_strip_text(p)

Ответ 2

Поскольку этот вопрос был опубликован, новая функция label_wrap_gen() с ggplot2 ( >= 1.0.0, я думаю) обрабатывает это красиво:

facet_wrap(~groupwrap, labeller = labeller(groupwrap = label_wrap_gen(10)))

Обратите внимание, что вам нужно указать ширину для работы.

Для более старых версий ggplot2:

facet_wrap(~groupwrap, labeller = label_wrap_gen())

Ответ 3

(слишком длинный, как комментарий, но не реальный ответ)

Я не думаю, что общее решение будет существовать непосредственно в ggplot2; это классическая проблема саморекламы для узлов сетки: ggplot2 хочет рассчитать размеры видовых экранов "на лету", в то время как strwrap должен знать твердую ширину, чтобы решить, как разделить текст. (был очень похожий вопрос, но я забыл, когда и где).

Однако вы могли бы написать вспомогательную функцию, чтобы оценить, сколько обертывания вам понадобится перед построением графика. В псевдокоде

# takes the facetting variable and device size
estimate_wrap = function(f, size=8, fudge=1){ 

    n = nlevels(f)
    for (loop over the labels of strwidth wider than (full.size * fudge) / n){
     new_factor_level[ii] = strwrap(label[ii], available width)
    }

  return(new_factor)
}

(требуются некоторые стандартные преобразования единиц)

Конечно, все будет сложнее, если вы хотите использовать space="free".

Ответ 4

Также слишком длинный для комментария, но не полный ответ. Он идет по строкам ответа баптиста, но с несколькими указателями:

p <- ggplot(df) + geom_point(aes(x=x, y=y)) + facet_wrap(~groupwrap)

# get the grobs of the plot and get the widths of the columns
grobs <- ggplotGrob(p)
grobs$width

# here you would have to use convertWidth from gridDebug package
# to convert all the units in the widths to the same unit (say 'pt'),
# including exctraction from the strings they are in -- also, I
# couldn't make it work neither for the unit 'null' nor for 'grobwidth',
# so you'll have to add up all the other ones, neglect grobwidth, and
# subtract all the widths that are not null (which is the width of each
# panel) from the device width
library('grid')
convertWidth(DO FOR EACH ELEMENT OF grobs$width)
sum <- SUM_UP_ALL_THE_NON-PANEL_WIDTHS

# get the width of the graphics device
device <- par('din')[1]

# get width of all panels in a row
panels_width <- device - sum

# get total number of panels in your case
df$group <- as.factor(df$group)
npanels <- nlevels(df$group)

# get number of panels per row (i.e. number of columns in graph) with
# the function that ggplot2 uses internally
cols <- n2mfrow(npanels)

# get estimate of width of single panel
panel_width <- panels_width / cols

Извините, что это по-прежнему неоднородно по частям. Но это насколько я понял, поэтому я надеюсь, что эти идеи могут помочь на этом пути...