Skip to contents

ggcube is a ggplot2 extension for 3D plotting. It provides 3D geoms, stats, and a 3D coordinate system that let you build 3D visualizations using the familiar ggplot2 grammar: map your data to aesthetics, add layers, and customize the result with scales, guides, and themes.

The basics

The essential ingredient of a ggcube plot is coord_3d(). Adding it to a standard ggplot that includes a z aesthetic creates a 3D plot:

ggplot(mpg, aes(x = displ, y = hwy, z = drv, color = class)) +
      geom_point() +
      coord_3d()

Some standard ggplot2 layers like geom_point() work in 3D automatically. ggcube also provides 3D-specific layer functions for surfaces, paths, volumes, and text, described below.

Because ggcube works within ggplot2’s 2D graphics engine, there are some important things to understand about how rendering works. Each 3D layer projects its geometry onto a 2D plane, with depth sorting to determine which elements appear in front of others. This sorting happens within each layer, but not across layers — just as in standard ggplot2, later layers are drawn on top of earlier ones regardless of their 3D depth. This means that layer order in your code matters, and complex multi-layer scenes may require some thought about stacking.

Controlling the view

coord_3d() controls how the 3D scene is projected onto 2D. Rotation is specified via three angles — pitch (tilt around the x-axis), roll (tilt around the y-axis), and yaw (spin around the z-axis):

ggplot(mpg, aes(displ, hwy, drv, color = class)) +
      geom_point() +
      coord_3d(pitch = 0, roll = 60, yaw = 0, dist = 1.4,
               ratio = c(2, 1, 1), panels = "all") +
      theme(panel.border = element_rect(color = "black"),
            panel.foreground = element_rect(alpha = .1))

Perspective projection is on by default, making distant objects appear smaller. The dist parameter controls the strength of this effect (larger values = less distortion), and persp = FALSE switches to orthographic projection where parallel lines stay parallel.

The scales parameter controls how axis ranges map to visual size. "free" (the default) stretches each axis independently to fill the cube, while "fixed" preserves the relative scale of the data (like coord_fixed() in 2D). The ratio parameter lets you set custom proportions for the three axes. And zoom adjusts overall framing without changing the rotation or projection.

See the 3D view article for a comprehensive guide to all view parameters.

3D layers

Most standard ggplot2 geoms are designed around 2D coordinate assumptions, and won’t render correctly with coord_3d(). (An exception is geom_point(), which works out of the box as shown above, albeit with ordering and sizing limitations.) ggcube provides a range of 3D-native layer functions that cover common 3D plot types, including points, surfaces, bars, paths, and text.

Here’s a quick tour of the main categories.

Surfaces

Several geoms and stats work together to render surfaces. geom_surface_3d() renders data as a tessellated surface, geom_contour_3d() creates layer-cake contour stacks, and geom_ridgeline_3d() shows cross-sectional slices:

ggplot(mountain, aes(x, y, z)) +
      geom_surface_3d(aes(fill = z, color = z)) +
      scale_fill_viridis_c() + scale_color_viridis_c() +
      coord_3d(ratio = c(1.5, 2, 1), expand = FALSE, panels = "zmin",
               light = light(direction = c(1, 0, 0))) +
      guides(fill = guide_colorbar_3d()) +
      theme_light()

Stats like stat_function_3d(), stat_smooth_3d(), and stat_density_3d() generate surface data from functions, model fits, or kernel density estimates. These stats can be paired with any of the surface geoms. See the 3D surfaces article for more detail on surface plotting options.

Points

geom_point_3d() extends scatter plots with depth-scaled point sizes (closer points shown larger), proper depth sorting (closer points plotted on top), and optional reference lines and points that project onto cube faces:

ggplot(mpg, aes(x = displ, y = hwy, z = drv, fill = class)) +
      geom_point_3d(size = 3, shape = 21, color = "black", stroke = .1,
                    ref_lines = TRUE, ref_faces = c("ymax", "xmax")) +
      coord_3d()

Paths

geom_path_3d() connects observations with depth-sorted, depth-scaled line segments:

x <- seq(0, 20*pi, pi/16)
spiral <- data.frame(x = x, y = sin(x), z = cos(x), time = 1:length(x))
ggplot(spiral, aes(x, y, z, color = time)) +
      geom_path_3d() +
      scale_color_gradientn(colors = c("blue", "purple", "red", "orange")) +
      coord_3d() +
      theme_light()

geom_segment_3d() is also available for drawing individual segments defined by start and end coordinates.

Prisms

geom_col_3d() produces 3D column charts, geom_bar_3d() creates 3D histograms with automatic binning, and geom_voxel_3d() renders arrays of cubes:

ggplot(iris, aes(Species, Sepal.Length, fill = Species)) +
      geom_bar_3d(bins = 20, width = c(.5, 1)) +
      coord_3d(scales = "fixed", ratio = c(1, 3, .25), yaw = 60) +
      scale_z_continuous(expand = c(0, 0)) +
      theme(legend.position = "none")

Hulls

geom_hull_3d() computes and renders triangulated hulls from 3D point clouds, including convex and alpha hulls:

ggplot(sphere_points, aes(x, y, z)) +
      geom_hull_3d(method = "convex", fill = "#9e2602", color = "#5e1600") +
      coord_3d()

Distributions

stat_distributions_3d() computes 1D kernel density estimates per group and arranges them as ridgeline surfaces, the 3D analog of ggridges::geom_density_ridges():

ggplot(iris, aes(y = Sepal.Length, x = Species, fill = Species)) +
      stat_distributions_3d() +
      scale_z_continuous(expand = expansion(mult = c(0, NA))) +
      coord_3d() +
      theme(legend.position = "none")

Text

geom_text_3d() renders text in 3D, either as “billboard” labels that always face the camera, or as polygon outlines that can be oriented in any direction:

df <- expand.grid(x = c("H", "B"), y = c("a", "o", "u"), z = c("g", "t"))
df$label <- paste0(df$x, df$y, df$z)
ggplot(df, aes(x, y, z, label = label, fill = x)) +
      geom_text_3d(method = "polygon", facing = "zmax",
                   size = 5, weight = "bold") +
      coord_3d(scales = "fixed", light = NULL)

Lighting

Lighting modifies the fill and/or color of polygon faces based on their orientation relative to a light source, giving surfaces a sense of depth and shape. It’s controlled via the light() function, which can be passed to coord_3d() (applying to all layers) or to individual layer functions (overriding the coord-level setting):

p <- ggplot(sphere_points, aes(x, y, z)) +
      geom_hull_3d(fill = "#9e2602", color = "#5e1600")

p + coord_3d(light = light(method = "direct", mode = "hsl",
                           direction = c(0, 0, 1)))

Use light = "none" to disable lighting entirely, or light = NULL in a layer to inherit the coord-level setting. For a comprehensive guide to lighting methods, color modes, light direction, and backface handling, see the lighting article.

Scales, guides, and themes

Z-axis scales

ggcube provides scale_z_continuous() and scale_z_discrete() for controlling the z-axis, with the same interface as their x/y counterparts. zlim() is a shorthand for setting z-axis limits:

ggplot(mtcars, aes(mpg, wt, z = qsec)) +
      geom_point() +
      zlim(15, 20) +
      coord_3d()

Shaded guides

When lighting is active, standard color guides don’t reflect the shading visible in the plot. guide_colorbar_3d() and guide_legend_3d() create guides that show the range of shaded colors:

ggplot(mountain, aes(x, y, z, fill = z)) +
      stat_surface_3d(light = light(mode = "hsl", direction = c(1, 0, 0))) +
      guides(fill = guide_colorbar_3d()) +
      scale_fill_gradientn(colors = c("tomato", "dodgerblue")) +
      coord_3d()

Panels and themes

The panels argument to coord_3d() controls which cube faces are drawn. Faces behind the data are “background” panels; faces in front are “foreground” panels (which default to semi-transparent so they don’t obscure the data):

ggplot(sphere_points, aes(x, y, z)) +
      geom_hull_3d() +
      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"),
            axis.text.z = element_text(color = "darkred"),
            axis.title = element_text(margin = margin(t = 30)),
            axis.title.x = element_text(color = "magenta"))

Standard ggplot2 themes and theme() customization work as expected. ggcube adds foreground-specific elements (panel.foreground, panel.grid.foreground, panel.border.foreground) and z-axis text elements (axis.text.z, axis.title.z). ggcube’s element_rect() extends ggplot2’s version with an alpha parameter for transparency. For details, see the theming section of the 3D coordinates article.

Annotations

annotate_3d() adds reference geometry — points, text labels, or segments — to any 3D layer. Unlike adding a separate layer, annotations are embedded within the host layer so they participate in the same depth sorting:

summit <- filter(mountain, z == max(z))
ggplot(mountain, aes(x, y, z)) +
      geom_contour_3d(
            annotate = list(
                  annotate_3d("point", x = summit$x, y = summit$y, z = summit$z, color = "red"),
                  annotate_3d("text", x = summit$x, y = summit$y, z = summit$z, color = "red",
                              label = "Summit", fontface = "bold", vjust = -1)
            ), fill = "black"
      ) +
      coord_3d(ratio = c(2, 3, 1.5), light = "none")

Mixing 2D and 3D

position_on_face() lets you project layers onto cube faces, enabling a mix of 3D and 2D content. You can flatten a 3D layer onto a face, or place a natively 2D layer (like stat_density_2d()) onto a specific face using the axes parameter:

ggplot(iris, aes(Sepal.Length, Sepal.Width, Petal.Length,
                 color = Species, fill = Species)) +
      coord_3d() + xlim(4, 8) +
      stat_density_2d(position = position_on_face(faces = "zmin", axes = c("x", "y")),
                      geom = "polygon", alpha = .1, linewidth = .25) +
      geom_hull_3d(position = position_on_face("ymax"), alpha = .5) +
      geom_point_3d(shape = 21, color = "black", stroke = .25)

Animation

animate_3d() creates animated rotations of any ggcube plot. Rotation angles are specified as keyframe vectors that get interpolated across frames:

p <- ggplot(mountain, aes(x, y, z)) +
      geom_contour_3d(fill = "black", color = "white", linewidth = .5) +
      coord_3d(ratio = c(1.5, 2, 1), light = "none", zoom = 1.5) +
      theme_void()

animate_3d(p, yaw = c(0, 360))

Output format is controlled via renderers: gifski_renderer_3d() (GIF, the default), av_renderer_3d() (MP4), or file_renderer_3d() (individual frames). Use anim_save_3d() to save the result to a file.