custom `geom_` with two different styles for plotting - r

My goal is to write a custom geom_ method that calculates and plots, e.g., confidence intervals and these should be plotted either as polygons or as lines. The question now is, where to check which "style" should be plotted?
So far I have tried out three different approaches,
(i) write two different geom_/stat_ for line and polygon style plots,
(ii) write a single geom_/stat_ which uses a custom GeomMethod,
(iii) write a single geom_/stat_ which uses either GeomPolygon or GeomLine.
In my opinion, to sum up
(i) is more or less straightforward but only bypasses the problem,
(ii) works when you use either GeomPath$draw_panel() or GeomPolygon$draw_panel() depending on an extra parameter style. But here I can't work it out to set default_aes depending also on the extra argument style. Compare also the answer here.
(iii) works when calling geom_ but fails for calling stat_ as the name matching within ggplot2 fails. See minimal example below.
Setting up the methods of approach (iii):
geom_my_confint <- function(mapping = NULL, data = NULL, stat = "my_confint",
position = "identity", na.rm = FALSE,
show.legend = NA, inherit.aes = TRUE,
style = c("polygon", "line"), ...) {
style <- match.arg(style)
ggplot2::layer(
geom = if (style == "line") GeomPath else GeomPolygon,
mapping = mapping,
data = data,
stat = stat,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
na.rm = na.rm,
style = style,
...
)
)
}
stat_my_confint <- function(mapping = NULL, data = NULL, geom = "my_confint",
position = "identity", na.rm = FALSE,
show.legend = NA, inherit.aes = TRUE,
style = c("polygon", "line"), ...) {
style <- match.arg(style)
ggplot2::layer(
geom = geom,
stat = StatMyConfint,
data = data,
mapping = mapping,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
na.rm = na.rm,
style = style,
...
)
)
}
StatMyConfint <- ggplot2::ggproto("StatMyConfint", ggplot2::Stat,
compute_group = function(data, scales, style) {
if (style == "polygon") {
nd <- data.frame(
x = c(data$x, rev(data$x)),
y = c(data$y - 1, rev(data$y) + 1)
)
nd
} else {
nd <- data.frame(
x = rep(data$x, 2),
y = c(data$y - 1, data$y + 1),
group = c(rep(1, 5), rep(2, 5))
)
nd
}
},
required_aes = c("x", "y")
)
Trying out the methods of approach (iii):
library("ggplot2")
d <- data.frame(
x = seq(1, 5),
y = seq(1, 5)
)
ggplot(d, aes(x = x, y = y)) + geom_line() + geom_my_confint(style = "polygon", alpha = 0.2)
ggplot(d, aes(x = x, y = y)) + geom_line() + geom_my_confint(style = "line", linetype = 2)
This works well so far. However when calling the stat_ there is an error in ggplot2:::check_subclass because there is no GeomMyConfint method.
ggplot(d, aes(x = x, y = y)) + geom_line() + stat_my_confint()
# Error: Can't find `geom` called 'my_confint'
Any solutions or suggestions for alternative approaches?

The following isn't very elegant but seems to work. Let's define the following constructor, wherein the geom is set to GeomMyConfint, which we'll define further down.
geom_my_confint <- function(mapping = NULL, data = NULL, stat = "my_confint",
position = "identity", na.rm = FALSE,
show.legend = NA, inherit.aes = TRUE,
style = c("polygon", "line"), ...) {
style <- match.arg(style)
ggplot2::layer(
geom = GeomMyConfint,
mapping = mapping,
data = data,
stat = stat,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
na.rm = na.rm,
style = style,
...
)
)
}
Below is the paired ggproto class. I've amended the use_defaults method to replace a defaulted colour by some text. Then later, the draw_panel() method chooses the actual default to replace the text we've inserted earlier, depending on the style argument.
GeomMyConfint <- ggproto(
"GeomMyConfint", GeomPolygon,
# Tag colour if it has been defaulted
use_defaults = function(self, data, params = list(), modifiers = aes()) {
has_colour <- "colour" %in% names(data) || "colour" %in% names(params)
data <- ggproto_parent(GeomPolygon, self)$use_defaults(
data, params, modifiers
)
if (!has_colour) {
data$colour <- "default_colour"
}
data
},
# Resolve colour defaults here
draw_panel = function(
data, panel_params, coord,
# Polygon arguments
rule = "evenodd",
# Line arguments
lineend = "butt", linejoin = "round", linemitre = 10,
na.rm = FALSE, arrow = NULL,
# Switch argument
style = "polygon")
{
if (style == "polygon") {
data$colour[data$colour == "default_colour"] <- NA
GeomPolygon$draw_panel(data, panel_params, coord, rule)
} else {
data$colour[data$colour == "default_colour"] <- "black"
GeomPath$draw_panel(data, panel_params, coord,
arrow, lineend, linejoin, linemitre, na.rm)
}
}
)
Then then works with the rest of the functions from your example.
A more elegant method might be to use the vctrs package to define a custom S3 class for defaulted values that is easy to recognise, but I haven't seen people trying to use aes(colour = I("default_colour")) before, so you're probably safe aside from this one single edge case.

Based on #teunbrand's answer and how geom_sf() is implemented, I came up with the following solution supporting approach (ii):
geom_my_confint <- function(mapping = NULL, data = NULL, stat = "my_confint",
position = "identity", na.rm = FALSE,
show.legend = NA, inherit.aes = TRUE,
type = c("polygon", "line"), ...) {
type <- match.arg(type)
ggplot2::layer(
geom = GeomMyConfint,
mapping = mapping,
data = data,
stat = stat,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
na.rm = na.rm,
type = type,
...
)
)
}
GeomMyConfint <- ggplot2::ggproto("GeomMyConfint", ggplot2::Geom,
## Setting up all defaults needed for `GeomPolygon` and `GeomPath`
default_aes = ggplot2::aes(
colour = NA,
fill = NA,
size = NA,
linetype = NA,
alpha = NA,
subgroup = NULL
),
draw_panel = function(data, panel_params, coord,
rule = "evenodd", # polygon arguments
lineend = "butt", linejoin = "round", # line arguments
linemitre = 10, na.rm = FALSE, arrow = NULL, # line arguments
type = c("polygon", "line")) {
type <- match.arg(type)
## Swap NAs in `default_aes` with own defaults
data <- my_modify_list(data, my_default_aesthetics(type), force = FALSE)
if (type == "polygon") {
GeomPolygon$draw_panel(data, panel_params, coord, rule)
} else {
GeomPath$draw_panel(data, panel_params, coord,
arrow, lineend, linejoin, linemitre, na.rm)
}
},
draw_key = function(data, params, size) {
## Swap NAs in `default_aes` with own defaults
data <- my_modify_list(data, my_default_aesthetics(params$type), force = FALSE)
if (params$type == "polygon") {
draw_key_polygon(data, params, size)
} else {
draw_key_path(data, params, size)
}
}
)
## Helper function inspired by internal from `ggplot2` defined in `performance.R`
my_modify_list <- function(old, new, force = FALSE) {
if (force) {
for (i in names(new)) old[[i]] <- new[[i]]
} else {
for (i in names(new)) old[[i]] <- if (all(is.na(old[[i]]))) new[[i]] else old[[i]]
}
old
}
## Helper function inspired by internal from `ggplot2` defined in `geom-sf.R`
my_default_aesthetics <- function(type) {
if (type == "line") {
my_modify_list(GeomPath$default_aes, list(colour = "red", linetype = 2), force = TRUE)
} else {
my_modify_list(GeomPolygon$default_aes, list(fill = "red", alpha = 0.2), force = TRUE)
}
}
I've kept the stat_my_confint() and StatMyConfint() from above unchanged (only the argument style is now called type according to the naming w/i geom_sf()):
stat_my_confint <- function(mapping = NULL, data = NULL, geom = "my_confint",
position = "identity", na.rm = FALSE,
show.legend = NA, inherit.aes = TRUE,
type = c("polygon", "line"), ...) {
type <- match.arg(type)
ggplot2::layer(
geom = geom,
stat = StatMyConfint,
data = data,
mapping = mapping,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
na.rm = na.rm,
type = type,
...
)
)
}
StatMyConfint <- ggplot2::ggproto("StatMyConfint", ggplot2::Stat,
compute_group = function(data, scales, type) {
if (type == "polygon") {
nd <- data.frame(
x = c(data$x, rev(data$x)),
y = c(data$y - 1, rev(data$y) + 1)
)
nd
} else {
nd <- data.frame(
x = rep(data$x, 2),
y = c(data$y - 1, data$y + 1),
group = c(rep(1, 5), rep(2, 5))
)
nd
}
},
required_aes = c("x", "y")
)
Now the examples from above work fine:
library("ggplot2")
d1 <- data.frame(
x = seq(1, 5),
y = seq(1, 5)
)
ggplot(d1, aes(x = x, y = y)) + geom_line() + geom_my_confint()
ggplot(d1, aes(x = x, y = y)) + geom_line() + geom_my_confint(type = "line")
ggplot(d1, aes(x = x, y = y)) + geom_line() + geom_my_confint(type = "polygon", alpha = 0.8)
ggplot(d1, aes(x = x, y = y)) + geom_line() + geom_my_confint(type = "line", linetype = 4, colour = "red")
ggplot(d1, aes(x = x, y = y)) + geom_line() + stat_my_confint()
ggplot(d1, aes(x = x, y = y)) + geom_line() + stat_my_confint(type = "line")
ggplot(d1, aes(x = x, y = y)) + geom_line() + stat_my_confint(type = "polygon", alpha = 0.8)
ggplot(d1, aes(x = x, y = y)) + geom_line() + stat_my_confint(type = "line", linetype = 4, colour = "red")
However, the solution still fails if you want additionally, e.g., set the fill colour of the polygon by an external grouping variable:
d2 <- data.frame(
x = rep(seq(1, 5), 2),
y = rep(seq(1, 5), 2),
z = factor(c(rep(1, 5), rep(2, 5)))
)
ggplot(d2, aes(x = x, y = y)) + geom_line() + geom_my_confint() + facet_wrap(.~z)
# no error
ggplot(d2, aes(x = x, y = y, fill = z)) + geom_line() + geom_my_confint() + facet_wrap(.~z)
# Error in grid.Call.graphics(C_setviewport, vp, TRUE) :
# non-finite location and/or size for viewport
So still no perfect answer. Help/extensions appreciated!
EDIT:
The error no longer occurs if the size argument is set to 0.5 within GeomMyConfint$default_aes():
Not clear to me why - anyone?!
Here, this works as I don't change the default size for GeomPolygon or GeomPath, but would be problematic otherwise.
I do not find any more errors (for now).
The adapted code:
GeomMyConfint <- ggplot2::ggproto("GeomMyConfint", ggplot2::Geom,
## Setting up all defaults needed for `GeomPolygon` and `GeomPath`
default_aes = ggplot2::aes(
colour = NA,
fill = NA,
size = 0.5,
linetype = NA,
alpha = NA,
subgroup = NULL
),
draw_panel = function(data, panel_params, coord,
rule = "evenodd", # polygon arguments
lineend = "butt", linejoin = "round", # line arguments
linemitre = 10, na.rm = FALSE, arrow = NULL, # line arguments
type = c("polygon", "line")) {
type <- match.arg(type)
## Swap NAs in `default_aes` with own defaults
data <- my_modify_list(data, my_default_aesthetics(type), force = FALSE)
if (type == "polygon") {
GeomPolygon$draw_panel(data, panel_params, coord, rule)
} else {
GeomPath$draw_panel(data, panel_params, coord,
arrow, lineend, linejoin, linemitre, na.rm)
}
},
draw_key = function(data, params, size) {
## Swap NAs in `default_aes` with own defaults
data <- my_modify_list(data, my_default_aesthetics(params$type), force = FALSE)
if (params$type == "polygon") {
draw_key_polygon(data, params, size)
} else {
draw_key_path(data, params, size)
}
}
)
The plot:
ggplot(d2, aes(x = x, y = y, fill = z)) + geom_line() + geom_my_confint() + facet_wrap(.~z)

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")

Subset data in custom made `ggplot2` extension based on type

I'm working on a ggplot2 extension that works on data.frames looking a bit like:
data <- data.frame(
type = c("text", "text", "line", "line"),
label = c("some label", "another one", NA, NA),
x = c(0,10,2,4),
y = c(0,10,3,7),
xend = c(NA, NA, 8, 10),
yend = c(NA, NA, 3, 4)
)
This means we have objects (rows) with different type. Now I want to subset the data inside my Geoms and Stats based on type.
Consider the following example (using ggplot2 standard functions):
library(ggplot2)
ggplot(data, aes(x, y)) +
geom_text(aes(label = label)) +
geom_segment(aes(xend = xend, yend = yend))
This plots what one expects (text as text and lines as segments).
Now I have my own version of geom_text called geom_var:
GeomVar <- ggproto("GeomVar", ggplot2::GeomText,
default_aes = aes(x = x, y = y, label = label, colour = "black",
size = 4, angle = 0, hjust = 0.5, vjust = 0.5,
alpha = NA, family = "Arial", fontface = 1,
lineheight = 1.2, length = 10)
)
geom_var <- function(mapping = aes(label = label), data = NULL, position = "identity",
..., parse = FALSE, nudge_x = 0, nudge_y = 0, check_overlap = FALSE,
na.rm = FALSE, show.legend = NA, inherit.aes = TRUE)
{
ggplot2::layer(data = data, mapping = mapping, stat = StatVar, geom = GeomVar,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(parse = parse, check_overlap = check_overlap,
na.rm = na.rm, ...))
}
StatVar <- ggproto("StatVar", ggplot2::Stat,
compute_group = function(data, scales, length = 5) {
data$label <- sapply(data$label,
function(x) {paste0(strwrap(x, width = length),
collapse = "\n")})
data
}
)
stat_var <- function(mapping = NULL, data = NULL, geom = "var",
position = "identity", na.rm = FALSE, show.legend = NA,
inherit.aes = TRUE, ...) {
ggplot2::layer(
stat = StatVar, data = data, mapping = mapping, geom = geom,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...)
)
}
When I use my own version the plot looks like:
ggplot(data, aes(x, y)) +
geom_var(aes(label = label)) +
geom_segment(aes(xend = xend, yend = yend))
TL;DR:
How can I change my GeomVar and/or StatVar in order the NAs don't get plotted anymore?
Or: How can I subset my data based on type in my GeomVar and StatVar functions?
(I tried data <- data[data$type == "text", ] in basically every place data occurs in the GeomVar, geom_var, StatVar and stat_var functions..)
You can pass type as an aesthetic in geom_var(aes(...)), and specify the setup_data function for your StatVar to look out for it:
# define setup_data in StatVar
StatVar <- ggproto("StatVar",
ggplot2::Stat,
setup_data = function(data, params){
# print(data) # I like doing this while debugging code to see what data
# actually looks like at this point
data <- data[data$type == "text", ]
},
compute_group = function(data, scales, length = 5) {
data$label <- sapply(data$label,
function(x) {paste0(strwrap(x, width = length),
collapse = "\n")})
data
}
)
# include type as a required aesthetic mapping in GeomVar
GeomVar <- ggproto("GeomVar", ggplot2::GeomText,
required_aes = c("x", "y", "label", "type"),
default_aes = aes(x = x, y = y, label = label, colour = "black",
size = 4, angle = 0, hjust = 0.5, vjust = 0.5,
alpha = NA, family = "Arial", fontface = 1,
lineheight = 1.2, length = 10))
# map type in geom_var
ggplot(data, aes(x, y)) +
geom_var(aes(label = label, type = type)) +
geom_segment(aes(xend = xend, yend = yend))

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

In ggproto, coord$transform did not transform some columns to [0, 1]

I want to create a new Geom type: geom_ohlc(), which is something like Candlestick Charts, to plot the stock open-high-low-close data.
After learning this Hadley's article: I tried this:
GeomOHLC <- ggproto(`_class` = "GeomOHLC", `_inherit` = Geom,
required_aes = c("x", "op", "hi", "lo", "cl"),
draw_panel = function(data, panel_scales, coord){
coords <- coord$transform(data, panel_scales)
browser() # <<-- here is where I found the problem
grid::gList(
grid::rectGrob(
x = coords$x,
y = pmin(coords$op, coords$cl),
vjust = 0,
width = 0.01,
height = abs(coords$op - coords$cl),
gp = grid::gpar(col = coords$color, fill = "yellow")
),
grid::segmentsGrob(
x0 = coords$x,
y0 = coords$lo,
x1 = coords$x,
y1 = coords$hi
)
)
})
geom_ohlc <- function(data = NULL, mapping = NULL, stat = "identity", position = "identity", na.rm = FALSE, show.legend = NA, inherit.aes = TRUE, ...)
{
layer(
geom = GeomOHLC, mapping = mapping, data = data,
stat = stat, position = position, show.legend = show.legend,
inherit.aes = inherit.aes, params = list(na.rm = na.rm, ...)
)
}
dt <- data.table(x = 1:10, open = 1:10, high = 3:12, low = 0:9, close = 2:11)
p <- ggplot(dt, aes(x = x, op = open, hi = high, lo = low, cl = close)) +
geom_ohlc()
p
for simplicity, i just do not consider the color of bar.
The result plot is like this:
I add a browser() inside the ggproto function, and I found that the coord$transform did not transform the op, hi, lo, cl aesthetics into interverl [0,1]. How to fix this problem ?
Moreover, is there any other documents about how to create your own Geom type except that Hadley's article ?
As mentioned in the comments under the OP's question the problem is aes_to_scale() function inside transform_position(), which in turn is called by coord$transform. Transformations are limited to variables named x, xmin, xmax, xend, xintercept and the equivalents for y axis. This is mentioned in the help for transform_position:
Description
Convenience function to transform all position variables.
Usage
transform_position(df, trans_x = NULL, trans_y = NULL, ...) Arguments
trans_x, trans_y Transformation functions for x and y aesthetics.
(will transform x, xmin, xmax, xend etc) ... Additional arguments
passed to trans_x and trans_y.
A workaround would be to use those variable names instead of the variable names used by the OP. The following code works in transforming the variables but it fails at somewhere else (see at the end). I do not know the details of the intended plot, so didn't try to fix this error.
GeomOHLC <- ggproto(
`_class` = "GeomOHLC",
`_inherit` = Geom,
required_aes = c("x", "yintercept", "ymin", "ymax", "yend"),
draw_panel = function(data, panel_scales, coord) {
coords <- coord$transform(data, panel_scales)
#browser() # <<-- here is where I found the problem
grid::gList(
grid::rectGrob(
x = coords$x,
y = pmin(coords$yintercept, coords$yend),
vjust = 0,
width = 0.01,
height = abs(coords$op - coords$cl),
gp = grid::gpar(col = coords$color, fill = "yellow")
),
grid::segmentsGrob(
x0 = coords$x,
y0 = coords$ymin,
x1 = coords$x,
y1 = coords$ymax
)
)
}
)
geom_ohlc <-
function(data = NULL,
mapping = NULL,
stat = "identity",
position = "identity",
na.rm = FALSE,
show.legend = NA,
inherit.aes = TRUE,
...)
{
layer(
geom = GeomOHLC,
mapping = mapping,
data = data,
stat = stat,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...)
)
}
dt <-
data.table(
x = 1:10,
open = 1:10,
high = 3:12,
low = 0:9,
close = 2:11
)
p <-
ggplot(dt, aes(
x = x,
yintercept = open,
ymin = high,
ymax = low,
yend = close
)) +
geom_ohlc()
p
This transforms the variables but produces the following error:
Error in unit(height, default.units) :
'x' and 'units' must have length > 0
But hopefully from here it can be made to work.
NOTE: I chose the mapping between the original variable names (op, hi, lo, cl) rather arbitrarily. Specially yintercept does not seem to fit well. Maybe there is need to support arbitrary scale variable names in ggplot2?

geom_density - customize KDE

I would like to use a different KDE method than stats::density which is used by stat_density/geom_density to plot a KDE for a distrubtion. How should I go about this?
I realized that this can be done by extending ggplot2 with ggproto. The ggproto vignette has an example that can be adapted pretty easily:
StatDensityCommon <- ggproto("StatDensityCommon", Stat,
required_aes = "x",
setup_params = function(data, params) {
if (!is.null(params$bandwidth))
return(params)
xs <- split(data$x, data$group)
bws <- vapply(xs, bw.nrd0, numeric(1))
bw <- mean(bws)
message("Picking bandwidth of ", signif(bw, 3))
params$bandwidth <- bw
params
},
compute_group = function(data, scales, bandwidth = 1) {
### CUSTOM FUNCTION HERE ###
d <- locfit::density.lf(data$x) #FOR EXAMPLE
data.frame(x = d$x, y = d$y)
}
)
stat_density_common <- function(mapping = NULL, data = NULL, geom = "line",
position = "identity", na.rm = FALSE, show.legend = NA,
inherit.aes = TRUE, bandwidth = NULL,
...) {
layer(
stat = StatDensityCommon, data = data, mapping = mapping, geom = geom,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(bandwidth = bandwidth, na.rm = na.rm, ...)
)
}
ggplot(mpg, aes(displ, colour = drv)) + stat_density_common()

Resources