Как динамически обернуть метку метки, используя 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))
Ответы
Ответ 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
Извините, что это по-прежнему неоднородно по частям. Но это насколько я понял, поэтому я надеюсь, что эти идеи могут помочь на этом пути...