coord_3d() is the core of every ggcube plot. It controls
how 3D data is rotated, projected, framed, and decorated. This article
walks through each of its parameters in detail.
We’ll use a common base plot throughout. For clarity, let’s suppress the axis text and add arrows at the positive ends of the axis lines:
p <- ggplot() +
geom_function_3d(
aes(fill = after_stat(z), color = after_stat(z)),
fun = function(x, y) sin(x) * cos(y),
xlim = c(-pi, pi), ylim = c(-pi, pi), n = 50) +
light("direct", contrast = .7) +
scale_fill_viridis_c() +
scale_color_viridis_c() +
guides(fill = guide_colorbar_3d()) +
theme_light() +
theme(legend.position = "none",
axis.text = element_blank(),
axis.line = element_line(color = "black",
arrow = arrow(20, unit(.1, "in"), type = "closed")))Rotation
The plot’s rotation in 3D space can be specified in three different ways: Euler angles, viewing vectors, or viewing angles.
Euler angles
One way to specify the rotation is by providing three angles,
pitch, roll, and yaw, which
rotate the plot around the x-, y-, and z-axes, respectively. All are
specified in degrees. When they’re all zero, the plot resembles a
standard 2D plot (+x = right, +y = up, +z = toward viewer). The defaults
(pitch = 0, roll = -60,
yaw = -30) give an overhead corner view that can work well
for many plots.
Here are a variety of rotations showing the default, the
zero-rotation case, the effect of setting each rotation angle
individually while the others are held at zero, and an example combining
them to achieve an arbitrary rotation (note this uses the
patchwork library to produce a mutlti-panel plot):
(p + coord_3d() + ggtitle("Default")) +
(p + coord_3d(pitch = 0, roll = 0, yaw = 0) + ggtitle("All angles 0")) +
(p + coord_3d(pitch = 20, roll = 40, yaw = 60) + ggtitle("Arbitrary combination")) +
(p + coord_3d(pitch = 30, roll = 0, yaw = 0) + ggtitle("pitch = 30")) +
(p + coord_3d(pitch = 0, roll = 30, yaw = 0) + ggtitle("roll = 30")) +
(p + coord_3d(pitch = 0, roll = 0, yaw = 30) + ggtitle("yaw = 30")) +
plot_layout(nrow = 2)
Viewing direction
Instead of rotation angles, you can give the direction
the plot is viewed from, as a vector c(x, y, z) in
the same orientation as the data axes. Only the direction of the vector
matters, not its length, so c(1, 1, 1) and
c(2, 2, 2) both view the cube from its xmax-ymax-zmax
corner. The plot is turned so that the z axis points up the page, or,
when looking straight down the z axis, so that the y axis points up.
(p + coord_3d(direction = c(0, 0, 1)) + ggtitle("c(0, 0, 1)")) +
(p + coord_3d(direction = c(1, 1, 1)) + ggtitle("c(1, 1, 1)")) +
(p + coord_3d(direction = c(-1, -1, 1)) + ggtitle("c(-1, -1, 1)")) +
(p + coord_3d(direction = c(0, -1, 0)) + ggtitle("c(0, -1, 0)")) +
(p + coord_3d(direction = c(1, 0, 0.5)) + ggtitle("c(1, 0, 0.5)")) +
(p + coord_3d(direction = c(1, -1, -1)) + ggtitle("c(1, -1, -1)")) +
plot_layout(nrow = 2)
Because a direction only says where the viewer is, it can’t express a
view that is tilted sideways; use pitch, roll,
and yaw to achieve that. Supplying direction
together with any of the rotation angles is an error.
Viewing angles
If you’d rather think in angles, you can pass an azimuth and an
elevation to direction(), which builds a vector you can use
for direction. Azimuth is a compass bearing, measured
clockwise from the positive y axis, and elevation is the angle above the
xy plane, both specified in degrees. The default view, for example, is
from an azimuth of 210 and an elevation of 30, so
coord_3d(direction = direction(210, 30)) gives the same
plot as coord_3d():
direction(210, 30)
#> [1] -0.4330127 -0.7500000 0.5000000Finding a view
Choosing angles by trial and error can be slow. Two tools help: the
facet functions facet_wrap_views() and
facet_grid_views() lay out a grid of views side by side for
comparison, and orbit_3d() lets you interactively rotate
the plot read the angles of the current view, ready to copy into
coord_3d(). See the animation and multiple views
article for details on both.
Perspective and distance
By default, coord_3d() uses perspective projection,
where objects farther from the viewer appear smaller. The
dist parameter controls the camera distance from the center
of the data cube — larger values produce less distortion. Set
persp = FALSE to switch to orthographic projection, where
parallel lines in 3D remain parallel in the 2D rendering. Orthographic
projection is equivalent to viewing from infinite distance and can be
useful for technical or schematic plots.
(p + coord_3d(yaw = 45, dist = 1.1) + ggtitle("dist = 1.1")) +
(p + coord_3d(yaw = 45, dist = 2) + ggtitle("dist = 2")) +
(p + coord_3d(yaw = 45, persp = FALSE) + ggtitle("persp = FALSE")) +
plot_layout(ncol = 3)
Perspective projection enlarges nearby objects and shrinks more
distant objects. For graphical objects whose size is defined by spatial
coordinates (like the polygon tiles in the figure above), this resizing
always happens because it is inherent to the data projection. For
properties like line widths and point and text sizes, a separate “depth
scaling” process changes their sizing to apply perspective. You can
adjust or switch off this latter behavior by specifying the
scale_depth parameter in an individual geom/stat function
(to control scaling for the data layer) or in coord_3d()
(to control scaling of the gridlines, ticks, and axis text).
Aspect ratios and scale matching
The scales parameter controls how data ranges map to
visual cube dimensions. With "free" (the default), each
axis is stretched independently to fill its dimension of the cube,
maximizing the visual range for each variable. With
"fixed", the visual proportions match the data proportions,
analogous to coord_fixed() in 2D:
(p + coord_3d(scales = "free") + ggtitle('scales = "free"')) +
(p + coord_3d(scales = "fixed") + ggtitle('scales = "fixed"')) +
plot_layout(ncol = 2) & theme_light()
The ratio parameter sets custom relative lengths for the
three axes. With "free" scales, ratios are applied to the
standardized cube coordinates. With "fixed" scales, they’re
applied to the data coordinates:
(p + coord_3d(scales = "free", ratio = c(1, 3, 1)) +
ggtitle('free, ratio = c(1, 3, 1)')) +
(p + coord_3d(scales = "fixed", ratio = c(1, 3, 1)) +
ggtitle('fixed, ratio = c(1, 3, 1)')) +
plot_layout(ncol = 2) & theme_light()
Zooming, clipping, and expansion
zoom adjusts the overall framing without changing the
rotation or projection. Values less than 1 zoom out (more whitespace),
while values greater than 1 zoom in (tighter framing).
The clip parameter controls whether graphical elements
can be drawn outside the panel area (ggplot2’s rectangular canvas, not
the projected 3D panel). The default "off" allows this,
which is generally recommended for 3D plots where elements may extend
beyond the projected panel area.
(p + coord_3d(zoom = 0.7) + ggtitle("zoom = 0.7")) +
(p + coord_3d(zoom = 1) + ggtitle("zoom = 1\n(default)")) +
(p + coord_3d(zoom = 1.5, clip = "on") + ggtitle('zoom = 1.5\nclip = "on"')) +
plot_layout(ncol = 3)
The expand parameter works like the expand
argument in standard ggplot2 scales — when TRUE (the
default), axis ranges are padded slightly beyond the data range. You can
fine-tune expansion per axis using the standard scale functions.
(p + coord_3d(expand = TRUE) + ggtitle("expand = TRUE (default)")) +
(p + coord_3d(expand = FALSE) + ggtitle("expand = FALSE")) +
plot_layout(ncol = 2)
Panel selection
The panels argument controls which faces of the cube are
drawn. The six faces are named "xmin", "xmax",
"ymin", "ymax", "zmin", and
"zmax". Depending on the rotation, some faces lie behind
the data (“background” panels) and some lie in front (“foreground”
panels). By default, only background panels are drawn. When drawn,
foreground panels default to 20% opacity so they don’t obscure the data;
their styling is controlled via theme elements (see the theming section
below).
You can request specific faces by name, show all faces, or hide them entirely:
(p + coord_3d(panels = "background") + ggtitle('"background" (default)')) +
(p + coord_3d(panels = c("xmin", "xmax", "zmax")) +
ggtitle('c("xmin", "xmax", "zmax")')) +
(p + coord_3d(panels = "all") + ggtitle('"all"')) +
(p + coord_3d(panels = "none") + ggtitle('"none"')) +
plot_layout(ncol = 2)
Axis label placement
Axis furnishings, including lines, ticks, text, and titles, belong to panels. This means they can only be drawn on panels that are themselves visible (see above).
The xlabels, ylabels, and
zlabels parameters control where axis tick labels and
titles are placed. Each axis potentially has multiple possible edges
where labels could appear, depending on which faces are visible. The
default "auto" uses a heuristic that prioritizes edges on
the periphery of the plot for readability.
For manual control, pass a length-2 character vector naming two adjacent faces — the shared edge between them is where the labels go. The first face determines the alignment plane:
(p + coord_3d() + ggtitle("auto (default)")) +
(p + coord_3d(xlabels = c("ymax", "zmax"),
zlabels = c("xmax", "ymin")) +
ggtitle("manual placement")) +
plot_layout(ncol = 2) & theme_light()
The rotate_labels parameter controls whether labels
automatically rotate to align with the projected axis direction. Setting
it to FALSE uses the angle specified in theme settings
instead:
(p + coord_3d(rotate_labels = TRUE) + ggtitle("rotate_labels = TRUE")) +
(p + coord_3d(rotate_labels = FALSE) + ggtitle("rotate_labels = FALSE")) +
plot_layout(ncol = 2) & theme_light()
The title_position parameter affects axis titles that
fall on internal (non-peripheral) edges. "auto" places them
at the near end of the axis outside the plot area, while
"center" centers them along the edge.
Theming the 3D cube
Standard ggplot2 themes work with ggcube. Adding a complete theme behaves as expected:
(p + coord_3d() + theme_dark() + ggtitle("theme_dark()")) +
(p + coord_3d() + theme_minimal() + ggtitle("theme_minimal()")) +
plot_layout(ncol = 2)
ggcube also adds several theme elements for 3D-specific styling. One
important category is foreground panel elements, which control cube
faces rendered in front of the data. These inherit from their background
counterparts, so styling panel.background affects both
background and foreground faces, while panel.foreground
targets only the front faces. ggcube’s element_rect()
extends ggplot2’s version with an alpha parameter for
transparency. This is particularly useful for foreground panels — the
default panel.foreground uses alpha = 0.2 so
front faces don’t obscure the data:
p +
coord_3d(panels = "all") +
theme_gray() +
theme(panel.border = element_rect(color = "black"),
panel.foreground = element_rect(alpha = .3),
panel.grid.foreground = element_line(color = "gray", linewidth = .25))
For axis labels, ggcube adds axis.text.z and
axis.title.z elements that inherit from
axis.text and axis.title respectively.
By default, axis text, ticks, and grid lines get their sizes/widths
scaled to apply depth perspective. This can be controlled with
coord_3d(scale_depth = ); see the documentation for
details. Axis titles are not scaled by depth.
The margin argument to element_text()
controls padding between titles, labels, and cube edges.
p +
coord_3d(panels = "all") +
theme(panel.background = element_rect(color = "black"),
panel.border = element_rect(color = "black"),
panel.foreground = element_rect(alpha = .3),
panel.grid.foreground = element_line(color = "gray", linewidth = .25),
axis.text = element_text(color = "darkblue", size = 12),
axis.text.z = element_text(color = "darkred"),
axis.title = element_text(margin = margin(t = 30)),
axis.title.x = element_text(color = "magenta"))
The full set of 3D-specific theme elements:
-
panel.foreground: Fill and border for foreground cube faces (inherits frompanel.background) -
panel.border.foreground: Border for foreground faces (inherits frompanel.border) -
panel.grid.foreground: Grid lines on foreground faces (inherits frompanel.grid) -
panel.grid.major.foreground: Major grid lines on foreground faces (inherits frompanel.grid.foreground) -
axis.line.z: Z-axis line (inherits fromaxis.line) -
axis.ticks.z: Z-axis tick (inherits fromaxis.ticks) -
axis.text.z: Z-axis tick labels (inherits fromaxis.text) -
axis.title.z: Z-axis title (inherits fromaxis.title)
