Nothing
#' @title Axes through or offset from the origin
#'
#' @description `geom_axis()` renders lines through or orthogonally translated
#' from the origin and the position of each case or variable.
#' @details Axes are lines that track the values of linear variables across a
#' plot. Multivariate scatterplots may include more axes than plotting
#' dimensions, in which case the plot may display only a fraction of the total
#' variation in the data.
#'
#' Gower & Hand (1996) recommend using axes to represent numerical variables
#' in biplots. Consequently, Gardner & le Roux (2002) refer to these as Gower
#' biplots.
#'
#' Axes positioned orthogonally at the origin are a ubiquitous feature of
#' scatterplots and used both to recover variable values from case markers
#' (prediction) and to position new case markers from variables
#' (interpolation). When they are not orthogonal, these two uses conflict, so
#' interpolative versus predictive axes must be used appropriately.
#'
#' @template ref-gower1996
#' @template ref-gardner2002
#' @section Aesthetics:
#' `geom_axis()` understands the following aesthetics (required aesthetics are
#' in bold):
#' - **`x`**
#' - **`y`**
#' - `lower`
#' - `upper`
#' - `yintercept` _or_ `xintercept` _or_ `xend` and `yend`
#' - `linetype`
#' - `linewidth`
#' - `size`
#' - `hjust`
#' - `vjust`
#' - `colour`
#' - `alpha`
#' - `label`
#' - `family`
#' - `fontface`
#' - `center`, `scale`
#' - `group`
#'
#' @import ggplot2
#' @inheritParams ggplot2::layer
#' @inheritParams ggplot2::geom_text
#' @inheritParams geom_isoline
#' @template param-geom
#' @param axis_labels,axis_ticks,axis_text Logical; whether to include labels,
#' tick marks, and text value marks along the axes.
#' @param label_dodge Numeric; the orthogonal distance of the axis label from
#' the axis, as a proportion of the minimum of the plot width and height.
#' @param label_placement Character; how to place the labels along the axes.
#' Matched to `"positive"` (the default; at the increasing end of the axis),
#' `"negative"` (at the decreasing end), `"peripheral"` (at the end farther
#' from the origin).
#' @param tick_length Numeric; the length of the tick marks, as a proportion of
#' the minimum of the plot width and height.
#' @param text_dodge Numeric; the orthogonal distance of tick mark text from the
#' axis, as a proportion of the minimum of the plot width and height.
#' @param axis.linewidth,axis.linetype,axis.colour,axis.color,axis.alpha Default
#' aesthetics for axes. Set to NULL to inherit from the data's aesthetics.
#' @param label.size,label.angle,label.hjust,label.vjust,label.family,label.fontface,label.colour,label.color,label.alpha
#' Default aesthetics for labels. Set to NULL to inherit from the data's
#' aesthetics.
#' @param tick.linewidth,tick.linetype,tick.colour,tick.color,tick.alpha Default
#' aesthetics for tick marks. Set to NULL to inherit from the data's
#' aesthetics.
#' @param text.size,text.angle,text.hjust,text.vjust,text.family,text.fontface,text.colour,text.color,text.alpha
#' Default aesthetics for tick mark labels. Set to NULL to inherit from the
#' data's aesthetics.
#' @template return-layer
#' @family geom layers
#' @example inst/examples/ex-geom-axis.r
#' @export
geom_axis <- function(
mapping = NULL, data = NULL, stat = "identity", position = "identity",
axis_labels = TRUE, axis_ticks = TRUE, axis_text = TRUE,
by = NULL, num = NULL,
tick_length = .025,
text_dodge = .03,
label_dodge = .03,
label_placement = c("positive", "negative", "peripheral"),
...,
# NB: Fallbacks declared here will be missed by `layer()` and `stat_*()`;
# they must be coordinated with the internal `*_fallback`s.
# axis_fallback
axis.linewidth = sync(), axis.linetype = sync(),
axis.colour = sync(), axis.color = NULL, axis.alpha = sync(),
# label_fallback
label.size = sync(), label.angle = 0,
label.hjust = sync(), label.vjust = sync(),
label.family = sync(), label.fontface = sync(),
label.colour = sync(), label.color = NULL, label.alpha = sync(),
# tick_fallback
# TODO: Inherit from theme.
tick.linewidth = 0.25, tick.linetype = "solid",
tick.colour = sync(), tick.color = NULL, tick.alpha = sync(),
# text_fallback
# TODO: Inherit from theme.
text.size = 2.6, text.angle = 0,
text.hjust = sync(), text.vjust = sync(),
# TODO: Inherit from theme.
text.family = sync(), text.fontface = sync(),
text.colour = sync(), text.color = NULL, text.alpha = sync(),
parse = FALSE, check_overlap = FALSE,
na.rm = FALSE,
show.legend = NA, inherit.aes = TRUE
) {
axis_gp <- list(
linewidth = axis.linewidth,
linetype = axis.linetype,
colour = axis.color %||% axis.colour,
alpha = axis.alpha
)
label_gp <- list(
size = label.size,
angle = label.angle,
hjust = label.hjust,
vjust = label.vjust,
family = label.family,
fontface = label.fontface,
colour = label.color %||% label.colour,
alpha = label.alpha
)
tick_gp <- list(
linewidth = tick.linewidth,
colour = tick.color %||% tick.colour,
alpha = tick.alpha
)
text_gp <- list(
size = text.size,
angle = text.angle,
hjust = text.hjust,
vjust = text.vjust,
family = text.family,
fontface = text.fontface,
colour = text.color %||% text.colour,
alpha = text.alpha
)
layer(
data = data,
mapping = mapping,
stat = stat,
geom = GeomAxis,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
axis_labels = axis_labels, axis_ticks = axis_ticks, axis_text = axis_text,
by = by, num = num,
tick_length = tick_length,
text_dodge = text_dodge,
label_dodge = label_dodge,
label_placement = label_placement,
axis_gp = axis_gp,
label_gp = label_gp,
tick_gp = tick_gp,
text_gp = text_gp,
parse = parse,
check_overlap = check_overlap,
na.rm = na.rm,
...
)
)
}
#' @rdname gggda-ggproto
#' @format NULL
#' @usage NULL
#' @export
GeomAxis <- ggproto(
"GeomAxis", Geom,
required_aes = c("x", "y"),
non_missing_aes = c("x", "y", "angle", "radius"),
optional_aes = c(
"lower", "upper",
"yintercept", "xintercept", "xend", "yend"
),
default_aes = aes(
# axis & label
linetype = "solid", linewidth = .25, size = 3.88,
angle = 0, hjust = 0.5, vjust = 0.5,
colour = "black", alpha = NA,
label = "", family = "", fontface = 1L,
# mark needs
center = 0, scale = 1
),
setup_params = function(data, params) {
# allow only `by` or `num`, not both
if (! is.null(params[["by"]]) && ! is.null(params[["num"]])) {
warning("Both `by` and `num` provided; ignoring `num`.")
params$num <- NULL
} else if (is.null(params[["by"]]) && is.null(params[["num"]])) {
params$num <- 6L
}
# fill in angles if necessary
if (is.null(params$label_gp$angle)) params$label_gp$angle <- 0
if (is.null(params$text_gp$angle)) params$text_gp$angle <- 0
params
},
setup_data = function(data, params) {
data <- ensure_cartesian_polar(data)
# limits?
use_limits <- ! is.null(data[["lower"]]) && ! is.null(data[["upper"]])
# offset?
use_offset <-
! is.null(data[["yintercept"]]) || ! is.null(data[["xintercept"]]) ||
(! is.null(data[["xend"]]) && ! is.null(data[["yend"]]))
# compute endpoints
if (use_limits) {
data <- transform(
data,
xmin = lower * cos(angle), ymin = lower * sin(angle),
xmax = upper * cos(angle), ymax = upper * sin(angle)
)
}
# recover and offset endpoints
if (use_offset) {
if (is.null(data[["xend"]]) || is.null(data[["yend"]]))
data <- recover_offset_endpoints(data)
if (use_limits) {
data <- transform(
data,
xmin = xmin + xend, ymin = ymin + yend,
xmax = xmax + xend, ymax = ymax + yend
)
}
}
# drop position coordinates
data$x <- data$y <- NULL
data
},
draw_panel = function(
data, panel_params, coord,
axis_labels = TRUE, axis_ticks = TRUE, axis_text = TRUE,
by = NULL, num = NULL,
tick_length = .025,
text_dodge = .03,
label_dodge = .03,
label_placement = c("positive", "negative", "peripheral"),
axis_gp = NULL, label_gp = NULL, tick_gp = NULL, text_gp = NULL,
parse = FALSE, check_overlap = FALSE,
na.rm = FALSE
) {
# save(data, panel_params, coord,
# axis_labels, axis_ticks, axis_text,
# by, num,
# tick_length,
# text_dodge,
# label_dodge,
# label_placement,
# axis_gp, label_gp, tick_gp, text_gp,
# parse, check_overlap,
# na.rm,
# file = "geom-axis-draw-panel.rda")
# load(file = "geom-axis-draw-panel.rda")
data <- ensure_cartesian_polar(data)
if (! coord$is_linear()) {
rlang::warn(
"Axes are not yet tailored to non-linear coordinates.",
.frequency = "regularly",
.frequency_id = "GeomAxis$draw_panel-is_linear"
)
}
# extract value ranges
ranges <- coord$range(panel_params)
# remove lengthless vectors
data <- subset(data, x^2 + y^2 > 0)
# offset?
use_offset <- ! is.null(data[["xend"]]) && ! is.null(data[["yend"]])
# placement
label_placement <- match.arg(
label_placement,
c("positive", "negative", "peripheral")
)
# initialize grob list
grobs <- list()
# minimum of the plot width and height
plot_whmin <- min(diff(ranges$x), diff(ranges$y))
# recover slope and (if offset) intercepts
if (is.null(data[["slope"]])) data$slope <- data$y / data$x
if (use_offset) {
if (is.null(data[["yintercept"]]) || is.null(data[["xintercept"]]))
data <- recover_offset_intercepts(data)
}
# text dodge vector
if (axis_labels || axis_text) {
data <- transform(
data,
dodge_angle = if (use_offset)
atan2(yend, xend)
else
(atan(slope) + pi/2)
)
}
# compute marks (`x_t` and `y_t`):
# if no segments then first bound outside window
if (axis_ticks || axis_text) {
mark_data <- data
# compute rule bounds in axis units, just beyond window borders
mark_data <- delimit_rules(mark_data, ranges$x, ranges$y)
# calculate rule values and positions
mark_data <- calibrate_rules(mark_data, by, num, loose = FALSE)
}
# axis grobs: if `xend` & `yend` then segment else abline & vline
axis_data <- unique(data)
# specify independent aesthetics
axis_fallback <- list()
axis_aes <- GeomAbline$aesthetics()
for (aes_name in axis_aes) {
axis_data[[aes_name]] <-
(if (is.sync(axis_gp[[aes_name]]))
axis_data[[aes_name]]) %||%
axis_gp[[aes_name]] %||%
axis_fallback[[aes_name]] %||%
axis_data[[aes_name]]
}
# NB: This step redefines positional aesthetics for a specific grob.
# diagonal versus vertical lines
axis_data <- transform(
axis_data,
vline = (x == 0 & y != 0) | (angle %% pi == pi/2),
slope = tan(angle)
)
axis_data <- transform(
axis_data,
intercept = ifelse(vline, Inf, axis_data$yintercept %||% 0),
xintercept = ifelse(slope == 0, Inf, axis_data$xintercept %||% 0)
)
if (any(! axis_data$vline)) {
grobs <- c(grobs, list(GeomAbline$draw_panel(
data = axis_data[! axis_data$vline, , drop = FALSE],
panel_params = panel_params, coord = coord
)))
}
if (any(axis_data$vline)) {
grobs <- c(grobs, list(GeomVline$draw_panel(
data = axis_data[axis_data$vline, , drop = FALSE],
panel_params = panel_params, coord = coord
)))
}
if (axis_labels) {
label_data <- data
# specify independent aesthetics
label_fallback <- list()
label_aes <- setdiff(GeomText$aesthetics(), "angle")
for (aes_name in label_aes) {
label_data[[aes_name]] <-
(if (is.sync(label_gp[[aes_name]]))
label_data[[aes_name]]) %||%
label_gp[[aes_name]] %||%
label_fallback[[aes_name]] %||%
label_data[[aes_name]]
}
# NB: This step redefines positional aesthetics for a specific grob.
# compute positions: if `xend` & `yend` then mid/endpoint else border
label_data <- border_points(
label_data,
panel_params$x.range, panel_params$y.range,
use_offset = use_offset, label_placement = label_placement
)
# adjust labels inward from borders
label_data <- transform(
label_data,
hjust = "inward"
)
# dodge axis
label_data <- transform(
label_data,
x = x + cos(dodge_angle) * plot_whmin * label_dodge,
y = y + sin(dodge_angle) * plot_whmin * label_dodge
)
# update text angle
label_data <- transform(
label_data,
angle = atan(tan(angle)) + label_gp$angle * pi / 180
)
# put total angle in degrees
label_data$angle <- label_data$angle * 180 / pi
# axis label grobs
grobs <- c(grobs, list(GeomText$draw_panel(
data = label_data,
panel_params = panel_params, coord = coord
)))
}
if (axis_ticks) {
tick_data <- mark_data
# specify independent aesthetics
tick_fallback <- list(linewidth = 0.25, linetype = "solid")
tick_aes <- GeomSegment$aesthetics()
for (aes_name in tick_aes) {
tick_data[[aes_name]] <-
(if (is.sync(tick_gp[[aes_name]]))
tick_data[[aes_name]]) %||%
tick_gp[[aes_name]] %||%
tick_fallback[[aes_name]] %||%
tick_data[[aes_name]]
}
# tick mark radius
rtick <- plot_whmin * tick_length / 2
# tick mark vector
tick_data <- transform(
tick_data,
xtick = - y / radius * rtick,
ytick = x / radius * rtick
)
# NB: This step redefines positional aesthetics for a specific grob.
# endpoints of tick marks
tick_data <- transform(
tick_data,
xend = x_t - xtick, x = x_t + xtick,
yend = y_t - ytick, y = y_t + ytick
)
# tick mark grobs
grobs <- c(grobs, list(GeomSegment$draw_panel(
data = offset_xy(tick_data),
panel_params = panel_params, coord = coord
)))
}
if (axis_text) {
text_data <- mark_data
# specify independent aesthetics
text_fallback <- list(size = 2.6)
text_aes <- setdiff(GeomText$aesthetics(), "angle")
for (aes_name in text_aes) {
text_data[[aes_name]] <-
(if (is.sync(text_gp[[aes_name]]))
text_data[[aes_name]]) %||%
text_gp[[aes_name]] %||%
text_fallback[[aes_name]] %||%
text_data[[aes_name]]
}
# omit labels at origin
if (! use_offset) {
text_data <-
text_data[text_data$x_t != 0 | text_data$y_t != 0, , drop = FALSE]
}
# NB: This step redefines positional aesthetics for a specific grob.
# dodge axis
text_data <- transform(
text_data,
x = x_t - cos(dodge_angle) * plot_whmin * text_dodge,
y = y_t - sin(dodge_angle) * plot_whmin * text_dodge
)
# update text angle and put in degrees
text_data <- transform(
text_data,
angle = atan(tan(angle)) * 180 / pi + text_gp$angle
)
if (nrow(text_data) > 0L) {
# mark text grobs
grobs <- c(grobs, list(GeomText$draw_panel(
data = offset_xy(text_data),
panel_params = panel_params, coord = coord,
parse = parse,
check_overlap = check_overlap,
na.rm = na.rm
)))
}
}
grob <- do.call(grid::grobTree, grobs)
grob$name <- grid::grobName(grob, "geom_axis")
grob
},
# update this to include segment and letter in key squares
draw_key = draw_key_abline
)
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.