Boxed geom_text with ggplot2 - r

I am developing a graphic with ggplot2 wherein I need to superimpose text over other graphical elements. Depending on the color of the elements underlying the text, it can be difficult to read the text. Is there a way to draw geom_text in a bounding box with a semi-transparent background?
I can do this with plotrix:
library(plotrix)
Labels <- c("Alabama", "Alaska", "Arizona", "Arkansas")
SampleFrame <- data.frame(X = 1:10, Y = 1:10)
TextFrame <- data.frame(X = 4:7, Y = 4:7, LAB = Labels)
### plotrix ###
plot(SampleFrame, pch = 20, cex = 20)
boxed.labels(TextFrame$X, TextFrame$Y, TextFrame$LAB,
bg = "#ffffff99", border = FALSE,
xpad = 3/2, ypad = 3/2)
But I do not know of a way to achieve similar results with ggplot2:
### ggplot2 ###
library(ggplot2)
Plot <- ggplot(data = SampleFrame,
aes(x = X, y = Y)) + geom_point(size = 20)
Plot <- Plot + geom_text(data = TextFrame,
aes(x = X, y = Y, label = LAB))
print(Plot)
As you can see, the black text labels are impossible to perceive where they overlap the black geom_points in the background.

Try this geom, which is slightly modified from GeomText.
GeomText2 <- proto(GeomText, {
objname <- "text2"
draw <- function(., data, scales, coordinates, ..., parse = FALSE,
expand = 1.2, bgcol = "grey50", bgfill = NA, bgalpha = 1) {
lab <- data$label
if (parse) {
lab <- parse(text = lab)
}
with(coordinates$transform(data, scales), {
tg <- do.call("mapply",
c(function(...) {
tg <- with(list(...), textGrob(lab, default.units="native", rot=angle, gp=gpar(fontsize=size * .pt)))
list(w = grobWidth(tg), h = grobHeight(tg))
}, data))
gList(rectGrob(x, y,
width = do.call(unit.c, tg["w",]) * expand,
height = do.call(unit.c, tg["h",]) * expand,
gp = gpar(col = alpha(bgcol, bgalpha), fill = alpha(bgfill, bgalpha))),
.super$draw(., data, scales, coordinates, ..., parse))
})
}
})
geom_text2 <- GeomText2$build_accessor()
Labels <- c("Alabama", "Alaska", "Arizona", "Arkansas")
SampleFrame <- data.frame(X = 1:10, Y = 1:10)
TextFrame <- data.frame(X = 4:7, Y = 4:7, LAB = Labels)
Plot <- ggplot(data = SampleFrame, aes(x = X, y = Y)) + geom_point(size = 20)
Plot <- Plot + geom_text2(data = TextFrame, aes(x = X, y = Y, label = LAB),
size = 5, expand = 1.5, bgcol = "green", bgfill = "skyblue", bgalpha = 0.8)
print(Plot)
BUG FIXED AND CODE IMPROVED
GeomText2 <- proto(GeomText, {
objname <- "text2"
draw <- function(., data, scales, coordinates, ..., parse = FALSE,
expand = 1.2, bgcol = "grey50", bgfill = NA, bgalpha = 1) {
lab <- data$label
if (parse) {
lab <- parse(text = lab)
}
with(coordinates$transform(data, scales), {
sizes <- llply(1:nrow(data),
function(i) with(data[i, ], {
grobs <- textGrob(lab[i], default.units="native", rot=angle, gp=gpar(fontsize=size * .pt))
list(w = grobWidth(grobs), h = grobHeight(grobs))
}))
gList(rectGrob(x, y,
width = do.call(unit.c, lapply(sizes, "[[", "w")) * expand,
height = do.call(unit.c, lapply(sizes, "[[", "h")) * expand,
gp = gpar(col = alpha(bgcol, bgalpha), fill = alpha(bgfill, bgalpha))),
.super$draw(., data, scales, coordinates, ..., parse))
})
}
})
geom_text2 <- GeomText2$build_accessor()

In the development version of ggplot2 package there is a new geom called geom_label() that implements this directly. Transperency can be atchieved with alpha= parameter.
ggplot(data = SampleFrame,
aes(x = X, y = Y)) + geom_point(size = 20)+
geom_label(data = TextFrame,
aes(x = X, y = Y, label = LAB),alpha=0.5)

Instead of adding a bounding box, I would suggest changing the text color to white which can be done by doing
Plot <- Plot +
geom_text(data = TextFrame, aes(x = X, y = Y, label = LAB), colour = 'white')
The other approach would be to add an alpha to geom_point to make it more transparent
Plot <- Plot + geom_point(size = 20, alpha = 0.5)
EDIT. Here is a way to generalize Chase's solution to automatically compute the bounding box. The trick is to add the width and height of text directly to the text data frame.
Here is an example
Labels <- c("Alabama", "Alaska", "Arizona", "Arkansas",
"Pennsylvania + California")
TextFrame <- data.frame(X = 4:8, Y = 4:8, LAB = Labels)
TextFrame <- transform(TextFrame,
w = strwidth(LAB, 'inches') + 0.25,
h = strheight(LAB, 'inches') + 0.25
)
ggplot(data = SampleFrame,aes(x = X, y = Y)) +
geom_point(size = 20) +
geom_rect(data = TextFrame, aes(xmin = X - w/2, xmax = X + w/2,
ymin = Y - h/2, ymax = Y + h/2), fill = "grey80") +
geom_text(data = TextFrame,aes(x = X, y = Y, label = LAB), size = 4)

Update for ggplot2 v0.9
library(ggplot2)
library(proto)
btextGrob <- function (label,x = unit(0.5, "npc"), y = unit(0.5, "npc"),
just = "centre", hjust = NULL, vjust = NULL, rot = 0, check.overlap = FALSE,
default.units = "npc", name = NULL, gp = gpar(), vp = NULL, f=1.5) {
if (!is.unit(x))
x <- unit(x, default.units)
if (!is.unit(y))
y <- unit(y, default.units)
grob(label = label, x = x, y = y, just = just, hjust = hjust,
vjust = vjust, rot = rot, check.overlap = check.overlap,
name = name, gp = gp, vp = vp, cl = "text")
tg <- textGrob(label = label, x = x, y = y, just = just, hjust = hjust,
vjust = vjust, rot = rot, check.overlap = check.overlap)
w <- unit(rep(1, length(label)), "strwidth", as.list(label))
h <- unit(rep(1, length(label)), "strheight", as.list(label))
rg <- rectGrob(x=x, y=y, width=f*w, height=f*h,
gp=gpar(fill="white", alpha=0.3, col=NA))
gTree(children=gList(rg, tg), vp=vp, gp=gp, name=name)
}
GeomText2 <- proto(ggplot2:::GeomText, {
objname <- "text2"
draw <- function(., data, scales, coordinates, ..., parse = FALSE, na.rm = FALSE) {
data <- remove_missing(data, na.rm,
c("x", "y", "label"), name = "geom_text2")
lab <- data$label
if (parse) {
lab <- parse(text = lab)
}
with(coord_transform(coordinates, data, scales),
btextGrob(lab, x, y, default.units="native",
hjust=hjust, vjust=vjust, rot=angle,
gp = gpar(col = alpha(colour, alpha), fontsize = size * .pt,
fontfamily = family, fontface = fontface, lineheight = lineheight))
)
}
})
geom_text2 <- function (mapping = NULL, data = NULL, stat = "identity", position = "identity",
parse = FALSE, ...) {
GeomText2$new(mapping = mapping, data = data, stat = stat,position = position,
parse = parse, ...)
}
qplot(wt, mpg, data = mtcars, label = rownames(mtcars), size = wt) +
geom_text2(colour = "red")

One option is to add another layer that corresponds to the text layer. Since ggplot adds layers sequentially, place a geom_rect under the call to geom_text and it will create the illusion you're after. This is admittedly a bit of a manual process trying to figure out the appropriate size for the box, but it's the best I can come up with for now.
library(ggplot2)
ggplot(data = SampleFrame,aes(x = X, y = Y)) +
geom_point(size = 20) +
geom_rect(data = TextFrame, aes(xmin = X -.4, xmax = X + .4, ymin = Y - .4, ymax = Y + .4), fill = "grey80") +
geom_text(data = TextFrame,aes(x = X, y = Y, label = LAB), size = 4)

following baptiste v0.9 answer, here's an update with rudimentary control of the box appearance (bgfill, bgalpha, bgcol, expand_w, expand_h):
btextGrob <- function (label,x = unit(0.5, "npc"), y = unit(0.5, "npc"),
just = "centre", hjust = NULL, vjust = NULL, rot = 0, check.overlap = FALSE,
default.units = "npc", name = NULL, gp = gpar(), vp = NULL, expand_w, expand_h, box_gp = gpar()) {
if (!is.unit(x))
x <- unit(x, default.units)
if (!is.unit(y))
y <- unit(y, default.units)
grob(label = label, x = x, y = y, just = just, hjust = hjust,
vjust = vjust, rot = rot, check.overlap = check.overlap,
name = name, gp = gp, vp = vp, cl = "text")
tg <- textGrob(label = label, x = x, y = y, just = just, hjust = hjust,
vjust = vjust, rot = rot, check.overlap = check.overlap)
w <- unit(rep(1, length(label)), "strwidth", as.list(label))
h <- unit(rep(1, length(label)), "strheight", as.list(label))
rg <- rectGrob(x=x, y=y, width=expand_w*w, height=expand_h*h,
gp=box_gp)
gTree(children=gList(rg, tg), vp=vp, gp=gp, name=name)
}
GeomTextbox <- proto(ggplot2:::GeomText, {
objname <- "textbox"
draw <- function(., data, scales, coordinates, ..., parse = FALSE, na.rm = FALSE,
expand_w = 1.2, expand_h = 2, bgcol = "grey50", bgfill = "white", bgalpha = 1) {
data <- remove_missing(data, na.rm,
c("x", "y", "label"), name = "geom_textbox")
lab <- data$label
if (parse) {
lab <- parse(text = lab)
}
with(coord_transform(coordinates, data, scales),
btextGrob(lab, x, y, default.units="native",
hjust=hjust, vjust=vjust, rot=angle,
gp = gpar(col = alpha(colour, alpha), fontsize = size * .pt,
fontfamily = family, fontface = fontface, lineheight = lineheight),
box_gp = gpar(fill = bgfill, alpha = bgalpha, col = bgcol),
expand_w = expand_w, expand_h = expand_h)
)
}
})
geom_textbox <- function (mapping = NULL, data = NULL, stat = "identity", position = "identity",
parse = FALSE, ...) {
GeomTextbox$new(mapping = mapping, data = data, stat = stat,position = position,
parse = parse, ...)
}
qplot(wt, mpg, data = mtcars, label = rownames(mtcars), size = wt) +
theme_bw() +
geom_textbox()

Update for ggplot2 1.0.1
GeomText2 <- proto(ggplot2:::GeomText, {
objname <- "text2"
draw <- function(., data, scales, coordinates, ..., parse = FALSE, na.rm = FALSE
,hjust = 0.5, vjust = 0.5
,expand = c(1.1,1.2), bgcol = "black", bgfill = "white", bgalpha = 1) {
data <- remove_missing(data, na.rm, c("x", "y", "label"), name = "geom_text")
lab <- data$label
if (parse) {
lab <- parse(text = lab)
}
with(coord_transform(coordinates, data, scales),{
sizes <- llply(1:nrow(data),
function(i) with(data[i, ], {
grobs <- textGrob(lab[i], default.units="native", rot=angle, gp=gpar(fontsize=size * .pt))
list(w = grobWidth(grobs), h = grobHeight(grobs))
})
)
w <- do.call(unit.c, lapply(sizes, "[[", "w"))
h <- do.call(unit.c, lapply(sizes, "[[", "h"))
gList(rectGrob(x, y,
width = w * expand[1],
height = h * expand[length(expand)],
just = c(hjust,vjust),
gp = gpar(col = alpha(bgcol, bgalpha), fill = alpha(bgfill, bgalpha))),
.super$draw(., data, scales, coordinates, ..., parse))
})
}
})
geom_text2 <- function (mapping = NULL, data = NULL, stat = "identity", position = "identity",parse = FALSE, ...) {
GeomText2$new(mapping = mapping, data = data, stat = stat, position = position, parse = parse, ...)
}

Related

Adapt `draw_key()` according to own `draw_panel()` for new `ggproto`

Based on the example in Master Software Development in R, I wrote a new geom_my_point(), adapting the alpha depending on the number of data points.
This works fine, but the alpha value of the label is not correct if alpha is explicitly set.
Here the code for the figures:
d <- data.frame(x = runif(200))
d$y <- 1 * d$x + rnorm(200, 0, 0.2)
d$z <- factor(sample(c("group1", "group2"), size = 200, replace = TRUE))
require("ggplot2")
gg1 <- ggplot(d) + geom_my_point(aes(x, y, colour = z)) + ggtitle("gg1")
gg2 <- ggplot(d) + geom_my_point(aes(x, y, colour = z), alpha = 1) + ggtitle("gg2")
gg3 <- ggplot(d) + geom_my_point(aes(x, y, colour = z, alpha = z)) + ggtitle("gg3")
Here the code for the geom_*():
geom_my_point <- function(mapping = NULL, data = NULL, stat = "identity",
position = "identity", na.rm = FALSE,
show.legend = NA, inherit.aes = TRUE, ...) {
ggplot2::layer(
geom = GeomMyPoint, mapping = mapping,
data = data, stat = stat, position = position,
show.legend = show.legend, inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...)
)
}
GeomMyPoint <- ggplot2::ggproto("GeomMyPoint", ggplot2::Geom,
required_aes = c("x", "y"),
non_missing_aes = c("size", "shape", "colour"),
default_aes = ggplot2::aes(
shape = 19, colour = "black", size = 2,
fill = NA, alpha = NA, stroke = 0.5
),
setup_params = function(data, params) {
n <- nrow(data)
if (n > 100 && n <= 200) {
params$alpha <- 0.3
} else if (n > 200) {
params$alpha <- 0.15
} else {
params$alpha <- 1
}
params
},
draw_panel = function(data, panel_scales, coord, alpha) {
if (is.character(data$shape)) {
data$shape <- translate_shape_string(data$shape)
}
## Transform the data first
coords <- coord$transform(data, panel_scales)
## Get alpha conditional on number of data points
if (any(is.na(coords$alpha))) {
coords$alpha <- alpha
}
## Construct a grid grob
grid::pointsGrob(
x = coords$x,
y = coords$y,
pch = coords$shape,
gp = grid::gpar(
col = alpha(coords$colour, coords$alpha),
fill = alpha(coords$fill, coords$alpha),
fontsize = coords$size * ggplot2::.pt + coords$stroke * ggplot2::.stroke / 2,
lwd = coords$stroke * ggplot2::.stroke / 2
)
)
},
draw_key = function(data, params, size) {
data$alpha <- params$alpha
ggplot2::draw_key_point(data, params, size)
}
)
EDIT:
According to the comment of #teunbrand, the problem for the plot qq2 can be solved by the following adaptions to the draw_key() function:
draw_key = function(data, params, size) {
if (is.na(data$alpha)) {
data$alpha <- params$alpha
}
ggplot2::draw_key_point(data, params, size)
}
But this still does not solve the problem with the graph qq3 - so the underlying question is why alpha is not correctly represented by the data argument of the draw_key() function. Compare also the following plot qq4, in which the size is correctly displayed in the legend (set a browser() w/i draw_key()):
gg4 <- ggplot(d) + geom_my_point(aes(x, y, colour = z, alpha = z, size = z)) + ggtitle("gg4")

R ggplot2 Bar Chart with Round Corners on Top of Bar

I would like to create a ggplot2 bar chart with round corners at the top of the bars. Consider the following example data:
data <- data.frame(x = letters[1:3],
y = c(5, 1, 4))
Based on the ggchicklet package, I can draw a ggplot2 bar chart with rounded corners:
library("ggplot2")
library("ggchicklet")
ggplot(data, aes(x, y)) +
geom_chicklet(radius = grid::unit(10, 'mm'))
However, as you can see in the image, the corners are round on both sides of the bars. How could I create a ggplot2 bar chart, where only the top of the bars are round?
As #GregorThomas suggests, you probably need a bit of a hacky fix. Here's my effort:
ggplot(data, aes(x, y + 2)) +
geom_chicklet(radius = grid::unit(10, 'mm')) +
scale_y_continuous(breaks = 0:8, labels = (-2):6) +
coord_cartesian(ylim = c(2, 8)) +
geom_rect(aes(xmin = 0.5, xmax = 3.5, ymin = 0, ymax = 1.95), fill = "gray95") +
labs(y = "y")
This allows fills and outlines to be preserved:
ggplot(data, aes(x, y + 2, fill = x)) +
geom_chicklet(radius = grid::unit(10, 'mm'), colour = "black") +
scale_y_continuous(breaks = 0:8, labels = (-2):6) +
coord_cartesian(ylim = c(2, 8)) +
geom_rect(aes(xmin = 0.5, xmax = 3.5, ymin = 0, ymax = 1.95), fill = "gray95") +
labs(y = "y")
Another Hack (also described by #Gregor Thomas in the comments)
Here is another hack that avoids manipulating the scale by painting a rect over the bottom rounded corners. It works with fill (but fails on outlines):
ggplot(data, aes(x, y, fill = x)) +
geom_chicklet(radius = grid::unit(10, 'mm'), colour = NA) +
geom_col(aes(y = y / 2))
polyclip solution
Based on the hack, I cobbled together a solution that will also work with outlines. The following code creates two new geoms, geom_top_rounded_rect and geom_top_rounded_col. A top rounded rect is created by uniting the polygons from a roundrectGrob and a rectGrob with half the size (like in the hack) using gridGeometry::polyclipGrob().
geom_top_rounded_rect <- function(mapping = NULL, data = NULL,
stat = "identity", position = "identity",
radius = grid::unit(6, "pt"),
...,
na.rm = FALSE,
show.legend = NA,
inherit.aes = TRUE) {
layer(
data = data,
mapping = mapping,
stat = stat,
geom = GeomTopRoundedRect,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
radius = radius,
na.rm = na.rm,
...
)
)
}
GeomTopRoundedRect <- ggplot2::ggproto(
"GeomTopRoundedRect", ggplot2::Geom,
default_aes = ggplot2::aes(
colour = NA, fill = "grey35", size = 0.5, linetype = 1, alpha = NA
),
required_aes = c("xmin", "xmax", "ymin", "ymax"),
draw_panel = function(self, data, panel_params, coord,
radius = grid::unit(6, "pt")) {
coords <- coord$transform(data, panel_params)
grobs <- lapply(1:length(coords$xmin), function(i) {
gridGeometry::polyclipGrob(
grid::roundrectGrob(
coords$xmin[i], coords$ymax[i],
width = (coords$xmax[i] - coords$xmin[i]),
height = (coords$ymax[i] - coords$ymin[i]),
r = radius,
default.units = "native",
just = c("left", "top")
),
grid::rectGrob(
coords$xmin[i], coords$ymax[i] - (coords$ymax[i] - coords$ymin[i]) / 2,
width = (coords$xmax[i] - coords$xmin[i]),
height = (coords$ymax[i] - coords$ymin[i]) / 2,
default.units = "native",
just = c("left", "top")
),
op = "union",
gp = grid::gpar(
col = coords$colour[i],
fill = alpha(coords$fill[i], coords$alpha[i]),
lwd = coords$size[i] * .pt,
lty = coords$linetype[i],
lineend = "butt"
)
)
})
grobs <- do.call(grid::gList, grobs)
ggplot2:::ggname("geom_top_rounded_rect", grid::grobTree(children = grobs))
},
draw_key = ggplot2::draw_key_polygon
)
geom_top_rounded_col <- function(mapping = NULL, data = NULL,
position = ggplot2::position_stack(reverse = TRUE),
radius = grid::unit(3, "pt"), ..., width = NULL,
na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) {
layer(
data = data, mapping = mapping, stat = "identity",
geom = GeomTopRoundedCol, position = position, show.legend = show.legend,
inherit.aes = inherit.aes, params = list(
width = width, radius = radius, na.rm = na.rm, ...
)
)
}
GeomTopRoundedCol <- ggproto(
"GeomTopRoundedCol", GeomTopRoundedRect,
required_aes = c("x", "y"),
setup_params = function(data, params) {
params$flipped_aes <- has_flipped_aes(data, params)
params
},
non_missing_aes = c("xmin", "xmax", "ymin", "ymax"),
setup_data = function(data, params) {
data$width <- data$width %||%
params$width %||% (resolution(data$x, FALSE) * 0.9)
transform(data,
ymin = pmin(y, 0), ymax = pmax(y, 0),
xmin = x - width / 2, xmax = x + width / 2, width = NULL
)
},
draw_panel = function(self, data, panel_params, coord, width = NULL, radius = grid::unit(3, "pt")) {
ggproto_parent(GeomTopRoundedRect, self)$draw_panel(data, panel_params, coord, radius = radius)
}
)
Usage:
ggplot(data, aes(x, y, fill = x)) +
geom_top_rounded_col(radius = grid::unit(10, 'mm'))
With outlines:
ggplot(data, aes(x, y, fill = x)) +
geom_top_rounded_col(radius = grid::unit(10, 'mm'), color = "black")

How to tell ggplot2 to use an user created scale for a new aesthetic.

I'm writing a package that extends ggplot2. One of those extensions is a geom_arrow() that takes aesthetics mag and angle to plot vector fields by magnitude and direction. I also created a scale_mag() to manipulate the length of the arrows with the prospect of creating also a new guide. Right now both geom and scale work as expected when added together.
ggplot(geo, aes(lon, lat)) +
geom_arrow(aes(mag = mag, angle = angle)) +
scale_mag()
But if I don't add scale_mag(), it doesn't work at all. What I want is for this scale to work like scale_color(), which is added by default when the color aesthetic is present.
Here is the code as it is right now:
geom_arrow <- function(mapping = NULL, data = NULL,
stat = "arrow",
position = "identity", ...,
start = 0,
direction = 1,
# scale = 1,
min.mag = 0,
skip = 0,
skip.x = skip,
skip.y = skip,
arrow.angle = 15,
arrow.length = 0.5,
arrow.ends = "last",
arrow.type = "closed",
arrow = grid::arrow(arrow.angle, unit(arrow.length, "lines"),
ends = arrow.ends, type = arrow.type),
lineend = "butt",
na.rm = FALSE,
show.legend = NA,
inherit.aes = TRUE) {
layer(geom = GeomArrow,
mapping = mapping,
data = data,
stat = stat,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
start = start,
direction = direction,
arrow = arrow,
lineend = lineend,
na.rm = na.rm,
# scale = scale,
skip.x = skip.x,
skip.y = skip.y,
min.mag = min.mag,
...)
)
}
GeomArrow <- ggplot2::ggproto("GeomArrow", Geom,
required_aes = c("x", "y"),
default_aes = ggplot2::aes(color = "black", size = 0.5, min.mag = 0,
linetype = 1, alpha = NA),
draw_key = ggplot2::draw_key_path,
draw_panel = function(data, panel_scales, coord,
arrow = arrow, lineend = lineend,
start = start, direction = direction,
preserve.dir = TRUE) {
coords <- coord$transform(data, panel_scales)
unit.delta <- "snpc"
if (preserve.dir == FALSE) {
coords$angle <- with(coords, atan2(yend - y, xend - x)*180/pi)
unit.delta <- "npc"
}
coords$dx <- with(coords, mag*cos(angle*pi/180))
coords$dy <- with(coords, mag*sin(angle*pi/180))
# from https://stackoverflow.com/questions/47814998/how-to-make-segments-that-preserve-angles-in-different-aspect-ratios-in-ggplot2
xx <- grid::unit.c(grid::unit(coords$x, "npc"),
grid::unit(coords$x, "npc") + grid::unit(coords$dx, unit.delta))
yy <- grid::unit.c(grid::unit(coords$y, "npc"),
grid::unit(coords$y, "npc") + grid::unit(coords$dy, unit.delta))
mag <- with(coords, mag/max(mag, na.rm = T))
arrow$length <- unit(as.numeric(arrow$length)*mag, attr(arrow$length, "unit"))
pol <- grid::polylineGrob(x = xx, y = yy,
default.units = "npc",
arrow = arrow,
gp = grid::gpar(col = coords$colour,
fill = scales::alpha(coords$colour, coords$alpha),
alpha = ifelse(is.na(coords$alpha), 1, coords$alpha),
lwd = coords$size*.pt,
lty = coords$linetype,
lineend = lineend),
id = rep(seq(nrow(coords)), 2))
pol
})
StatArrow <- ggplot2::ggproto("StatArrow", ggplot2::Stat,
required_aes = c("x", "y"),
default_aes = ggplot2::aes(min.mag = 0, dx = NULL, dy = NULL,
mag = NULL, angle = NULL),
compute_group = function(data, scales,
skip.x = skip.x, skip.y = skip.y,
min.mag = min.mag) {
min.mag <- data$min.mag %||% min.mag
if (is.null(data$mag) | is.null(data$angle)) {
if (is.null(data$dx) | is.null(data$dy)) stop("stat_arrow need dx, dy or mag angle (improve mesage!!)")
data$mag <- with(data, Mag(dx, dy))
data$angle <- with(data, atan2(dy, dx)*180/pi)
} else {
data$dx <- with(data, mag*cos(angle*pi/180))
data$dy <- with(data, mag*sin(angle*pi/180))
}
data <- subset(data, x %in% JumpBy(unique(x), skip.x + 1) &
y %in% JumpBy(unique(y), skip.y + 1) &
mag >= min.mag)
data$xend = with(data, x + dx)
data$yend = with(data, y + dy)
data
}
)
scale_mag <- function(length = 0.1,
max = waiver(),
default_unit = "lines") {
# if (!is.unit(length)) length <- ggplot2::unit(length, default_unit)
continuous_scale("mag",
"mag",
identity,
rescaler = rescale_mag(length, max),
guide = "none")
}
# scale_type.mag <- function(x) "vector"
rescale_mag <- function(length, max) {
function(x, from) {
if (is.waive(max)) max <- max(x, na.rm = T)
scales::rescale(x, c(0, length), c(0, max))
}
}
Finally, I find the answer!
Based on the code in ggplot2/R/scale-type.R, there should be a scale named scale_mag_continuous in the parent environment of find_scale function. Then, this scale can be find automatically.
geo <- tibble(lon = 1:10, lat = 1:10, mag = 1:10, angle = 1:10)
scale_mag_continuous <- scale_mag
ggplot(geo, aes(lon, lat)) +
geom_arrow(aes(mag = mag, angle = angle))
I added a default theme to ggplot for a work package by overloading the ggplot function, basically like this:
ggplot <- function(...) {ggplot2::ggplot(...) + your_added_thing()}
If you want it to be less obtrusive, rename your version of ggplot:
jjplot <- function (...) {ggplot2::ggplot(...) + my_added_thing()}
this page will be helpful for you.
https://gist.github.com/wch/3250485
especially, the code below:
#This tells ggplot2 what scale to look for, for yearmon
scale_type.yearmon <- function(x) "yearmon"

ggplot with bty="n", or how to add grid coordinates to plot coordinates

I have a question that expands on this one. Basically I want to add bty = "n" to a ggplot2 graph in a proper way. Emphasis on proper here because the solution in the other question almost what I want, except for this detail: I would like it if the axis line would continue until the end of the tick, not until the middle of it. First, code for the graph:
library(ggplot2)
library(grid)
graph = ggplot(faithful, aes(x=eruptions, y=waiting)) +
geom_point(shape=21) +
theme(
# tick width, a bit exaggerated as example
axis.ticks = element_line(size = 5, color = "gray")
)
graph # graph with no axis lines
# get axis limits
gb = ggplot_build(graph)
xLim = range(gb$layout$panel_ranges[[1]]$x.major_source)
yLim = range(gb$layout$panel_ranges[[1]]$y.major_source)
# add lines
graph +
geom_segment(y = -Inf, yend = -Inf, x = xLim[1], xend = xLim[2]) +
geom_segment(x = -Inf, xend = -Inf, y = yLim[1], yend = yLim[2])
So the problem is: I draw on the x-axis from 50 till 90. But, the tickmarks are centered on 50 and 90, therefore they extend by half of size = 5 on each side. ?element_line tells me that line/ border size is by default in mm. Thus I want to draw the line from 50 - 5mm / 2 until 90 + 5mm / 2. I tried (many variations of) the following:
xLim = range(gb$layout$panel_ranges[[1]]$x.major_source)
yLim = range(gb$layout$panel_ranges[[1]]$y.major_source)
uType = "npc"
uType2 = "mm"
# attempt conversion of units
xLim[1] = xLim[1] - convertWidth(unit(2.5, units = uType2),
unitTo = uType, valueOnly = TRUE)
xLim[2] = xLim[2] + convertWidth(unit(2.5, units = uType2),
unitTo = uType, valueOnly = TRUE)
yLim[1] = yLim[1] - convertHeight(unit(2.5, units = uType2),
unitTo = uType, valueOnly = TRUE)
yLim[2] = yLim[2] - convertHeight(unit(2.5, units = uType2),
unitTo = uType, valueOnly = TRUE)
# redraw graph
cairo_pdf("Rplot.pdf")
graph +
geom_segment(y = -Inf, yend = -Inf, x = xLim[1], xend = xLim[2]) +
geom_segment(x = -Inf, xend = -Inf, y = yLim[1], yend = yLim[2])
dev.off()
But no luck whatsoever. Any ideas?
I believe you'd have to write a drawDetails method or similar to do the unit calculation at drawing time for this to work.
Alternatively (and perhaps easier), you could write a custom tick grob that extends to cover the axis line.
(Note that the two axes have different line widths because of their z-order IIRC; I thought that bug had been fixed).
library(ggplot2)
library(grid)
element_grob.element_custom_x <- function (element, x = 0:1, y = 0:1, colour = NULL, size = NULL,
linetype = NULL, lineend = "butt", default.units = "npc", id.lengths = NULL,
...)
{
gp <- gpar(lwd = ggplot2:::len0_null(size * .pt), col = colour, lty = linetype,
lineend = lineend)
element_gp <- gpar(lwd = ggplot2:::len0_null(element$size * .pt), col = element$colour,
lty = element$linetype, lineend = element$lineend)
arrow <- if (is.logical(element$arrow) && !element$arrow) {
NULL
}
else {
element$arrow
}
g1 <- polylineGrob(x, y, default.units = default.units,
gp = utils::modifyList(element_gp, gp),
id.lengths = id.lengths, arrow = arrow, ...)
vertical <- length(unique(element$x)) == 1 && length(unique(element$y)) >= 1
g2 <- grid::editGrob(g1, y=y + unit(1,"mm"), gp=utils::modifyList(gp, list(col="green")), name="new")
grid::grobTree(g2, g1)
}
element_grob.element_custom_y <- function (element, x = 0:1, y = 0:1, colour = NULL, size = NULL,
linetype = NULL, lineend = "butt", default.units = "npc", id.lengths = NULL,
...)
{
gp <- gpar(lwd = ggplot2:::len0_null(size * .pt), col = colour, lty = linetype,
lineend = lineend)
element_gp <- gpar(lwd = ggplot2:::len0_null(element$size * .pt), col = element$colour,
lty = element$linetype, lineend = element$lineend)
arrow <- if (is.logical(element$arrow) && !element$arrow) {
NULL
}
else {
element$arrow
}
g1 <- polylineGrob(x, y, default.units = default.units,
gp = utils::modifyList(element_gp, gp),
id.lengths = id.lengths, arrow = arrow, ...)
g2 <- grid::editGrob(g1, x=x + unit(1,"mm"), gp=utils::modifyList(gp, list(col="green")), name="new")
grid::grobTree(g2, g1)
}
## silly wrapper to fool ggplot2
x_custom <- function(...){
structure(
list(...), # this ... information is not used, btw
class = c("element_custom_x","element_blank", "element") # inheritance test workaround
)
}
y_custom <- function(...){
structure(
list(...), # this ... information is not used, btw
class = c("element_custom_y","element_blank", "element") # inheritance test workaround
)
}
graph = ggplot(faithful, aes(x=eruptions, y=waiting)) +
geom_point(shape=21) + theme_minimal() +
theme(
axis.ticks.x = x_custom(size = 5, colour = "red") ,
axis.ticks.y = y_custom(size = 5, colour = "red") ,
axis.ticks.length = unit(2,"mm")
)
graph # graph with no axis lines
gb <- ggplot_build(graph)
xLim = range(gb$layout$panel_ranges[[1]]$x.major_source)
yLim = range(gb$layout$panel_ranges[[1]]$y.major_source)
graph +
geom_segment(y = -Inf, yend = -Inf, x = xLim[1], xend = xLim[2],lwd=2) +
geom_segment(x = -Inf, xend = -Inf, y = yLim[1], yend = yLim[2],lwd=2)
Much simpler nowadays: use the geom_rangeframe() from the package ggthemes(). I think it does exactly what you want.

How to modify the backgroup color of label in the multiple-ggproto using ggplot2

I want to draw a graph which is familiar to the enterotype plot in the research. But my new multiple-ggproto seems terrible as showed in p1, owing to the missing backgroup color of the label. I've tried multiple variations of this, for example modify GeomLabel$draw_panel in order to reset the default arguments of geom in ggplot2::ggproto. However, I could not find the labelGrob() function which is removed in ggplot2 and grid package. Thus, the solution of modification didn't work. How to modify the backgroup color of label in the multiple-ggproto. Any ideas? Thanks in advance. Here is my code and two pictures.
p1: the background color of label should be white or the text color should be black.
P2:displays the wrong point color, line color and legend.
geom_enterotype <- function(mapping = NULL, data = NULL, stat = "identity", position = "identity",
alpha = 0.3, prop = 0.5, ..., lineend = "butt", linejoin = "round",
linemitre = 1, arrow = NULL, na.rm = FALSE, parse = FALSE,
nudge_x = 0, nudge_y = 0, label.padding = unit(0.15, "lines"),
label.r = unit(0.15, "lines"), label.size = 0.1,
show.legend = TRUE, inherit.aes = TRUE) {
library(ggplot2)
# create new stat and geom for PCA scatterplot with ellipses
StatEllipse <- ggproto("StatEllipse", Stat,
required_aes = c("x", "y"),
compute_group = function(., data, scales, level = 0.75, segments = 51, ...) {
library(MASS)
dfn <- 2
dfd <- length(data$x) - 1
if (dfd < 3) {
ellipse <- rbind(c(NA, NA))
} else {
v <- cov.trob(cbind(data$x, data$y))
shape <- v$cov
center <- v$center
radius <- sqrt(dfn * qf(level, dfn, dfd))
angles <- (0:segments) * 2 * pi/segments
unit.circle <- cbind(cos(angles), sin(angles))
ellipse <- t(center + radius * t(unit.circle %*% chol(shape)))
}
ellipse <- as.data.frame(ellipse)
colnames(ellipse) <- c("x", "y")
return(ellipse)
})
# write new ggproto
GeomEllipse <- ggproto("GeomEllipse", Geom,
draw_group = function(data, panel_scales, coord) {
n <- nrow(data)
if (n == 1)
return(zeroGrob())
munched <- coord_munch(coord, data, panel_scales)
munched <- munched[order(munched$group), ]
first_idx <- !duplicated(munched$group)
first_rows <- munched[first_idx, ]
grid::pathGrob(munched$x, munched$y, default.units = "native",
id = munched$group,
gp = grid::gpar(col = first_rows$colour,
fill = alpha(first_rows$fill, first_rows$alpha), lwd = first_rows$size * .pt, lty = first_rows$linetype))
},
default_aes = aes(colour = "NA", fill = "grey20", size = 0.5, linetype = 1, alpha = NA, prop = 0.5),
handle_na = function(data, params) {
data
},
required_aes = c("x", "y"),
draw_key = draw_key_path
)
# create a new stat for PCA scatterplot with lines which totally directs to the center
StatConline <- ggproto("StatConline", Stat,
compute_group = function(data, scales) {
library(miscTools)
library(MASS)
df <- data.frame(data$x,data$y)
mat <- as.matrix(df)
center <- cov.trob(df)$center
names(center)<- NULL
mat_insert <- insertRow(mat, 2, center )
for(i in 1:nrow(mat)) {
mat_insert <- insertRow( mat_insert, 2*i, center )
next
}
mat_insert <- mat_insert[-c(2:3),]
rownames(mat_insert) <- NULL
mat_insert <- as.data.frame(mat_insert,center)
colnames(mat_insert) =c("x","y")
return(mat_insert)
},
required_aes = c("x", "y")
)
# create a new stat for PCA scatterplot with center labels
StatLabel <- ggproto("StatLabel" ,Stat,
compute_group = function(data, scales) {
library(MASS)
df <- data.frame(data$x,data$y)
center <- cov.trob(df)$center
names(center)<- NULL
center <- t(as.data.frame(center))
center <- as.data.frame(cbind(center))
colnames(center) <- c("x","y")
rownames(center) <- NULL
return(center)
},
required_aes = c("x", "y")
)
layer1 <- layer(data = data, mapping = mapping, stat = stat, geom = GeomPoint,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...))
layer2 <- layer(stat = StatEllipse, data = data, mapping = mapping, geom = GeomEllipse, position = position, show.legend = FALSE,
inherit.aes = inherit.aes, params = list(na.rm = na.rm, prop = prop, alpha = alpha, ...))
layer3 <- layer(data = data, mapping = mapping, stat = StatConline, geom = GeomPath,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(lineend = lineend, linejoin = linejoin,
linemitre = linemitre, arrow = arrow, na.rm = na.rm, ...))
if (!missing(nudge_x) || !missing(nudge_y)) {
if (!missing(position)) {
stop("Specify either `position` or `nudge_x`/`nudge_y`",
call. = FALSE)
}
position <- position_nudge(nudge_x, nudge_y)
}
layer4 <- layer(data = data, mapping = mapping, stat = StatLabel, geom = GeomLabel,
position = position, show.legend = FALSE, inherit.aes = inherit.aes,
params = list(parse = parse, label.padding = label.padding,
label.r = label.r, label.size = label.size, na.rm = na.rm, ...))
return(list(layer1,layer2,layer3,layer4))
}
# data
data(Cars93, package = "MASS")
car_df <- Cars93[, c(3, 5, 13:15, 17, 19:25)]
car_df <- subset(car_df, Type == "Large" | Type == "Midsize" | Type == "Small")
x1 <- mean(car_df$Price) + 2 * sd(car_df$Price)
x2 <- mean(car_df$Price) - 2 * sd(car_df$Price)
car_df <- subset(car_df, Price > x2 | Price < x1)
car_df <- na.omit(car_df)
# Principal Component Analysis
car.pca <- prcomp(car_df[, -1], scale = T)
car.pca_pre <- cbind(as.data.frame(predict(car.pca)[, 1:2]), car_df[, 1])
colnames(car.pca_pre) <- c("PC1", "PC2", "Type")
xlab <- paste("PC1(", round(((car.pca$sdev[1])^2/sum((car.pca$sdev)^2)), 2) * 100, "%)", sep = "")
ylab <- paste("PC2(", round(((car.pca$sdev[2])^2/sum((car.pca$sdev)^2)), 2) * 100, "%)", sep = "")
head(car.pca_pre)
#plot
library(ggplot2)
p1 <- ggplot(car.pca_pre, aes(PC1, PC2, fill = Type , color= Type ,label = Type)) +
geom_enterotype()
p2 <- ggplot(car.pca_pre, aes(PC1, PC2, fill = Type , label = Type)) +
geom_enterotype()
You can manually change the colour scale to give it more emphasis against the background fill colour:
p3 <- ggplot(car.pca_pre, aes(PC1, PC2, fill = Type , color = Type, label = Type)) +
geom_enterotype() +
scale_colour_manual(values = c("red4", "green4", "blue4"))
p3
You can additionally adjust your fill colours by changing the alpha values, or assigning different colour values to give better contrast to your labels.
p4 <- ggplot(car.pca_pre, aes(PC1, PC2, label = Type, shape = Type, fill = Type, colour = Type)) +
geom_enterotype() +
scale_fill_manual(values = alpha(c("pink", "lightgreen", "skyblue"), 1)) +
scale_colour_manual(values = c("red4", "green4", "blue4"))
p4
Finally, if you want a background white colour to your labels, you have to remove the fill option. You can also additionally assign a shape value.
As you can observe, the background text colour is associated with the shape fill colour, while the text label colour is associated with the line colour, the the shape border colour.
p5 <- ggplot(car.pca_pre, aes(PC1, PC2, label = Type, shape = Type, colour = Type)) +
geom_enterotype() + scale_colour_manual(values = c("red4", "green4", "blue4"))
p5

Resources