ggcube (development version)
Depth sorting
Changes to default behaviors, which alter the rendering of some plots relative to the prior version:
- All ggcube layers in a panel are now depth-sorted together, as one scene, so the geometry of one layer can pass in front of or behind that of another: points behind a surface drawn by another layer are hidden by it, a plane drawn by one layer cuts through segments drawn by another, and so on. Previously each layer was sorted on its own and drawn on top of the layers before it. Layers from other packages are drawn as before, and separate the ggcube layers on either side of them into separate scenes. Coplanar polygons from different layers are drawn in layer order.
- Depth sorting is now always pairwise: every pair of elements that overlap on screen is ordered from their 3D geometry. The faster but approximate
"painter"method, and the"auto"choice between the two, are gone, along with thesort_methodargument of layer functions, which is deprecated and has no effect. Plots of layers that used painter sorting by default, such asgeom_point_3d(),geom_path_3d(),geom_segment_3d(), and large surfaces, are now drawn correctly where their elements overlap. - Point markers and lines are now ordered using their drawn size: overlapping markers are drawn farther first, a marker overlapping the edge of a polygon is ordered against it, and lines are ordered where their drawn widths overlap. Previously markers were treated as points, so two overlapping markers could be drawn in either order. Elements are now ordered when the plot is drawn, since their drawn size depends on the size of the panel.
Performance:
- Pairwise sorting has been rewritten in C++, and compares only elements that overlap on screen rather than every pair, so its time and memory grow with the number of overlapping pairs rather than the square of the number of elements. Sorting is now about as fast as painter sorting was: a scatterplot of 10,000 points, which previously took minutes to sort pairwise, now renders in about two seconds, and a 100 x 100 surface, which previously ran out of memory, in about six. ggcube now contains compiled code.
Annotations
-
annotate_3d()now returns a layer, added to a plot with+like ggplot2’sannotate(), e.g.p + annotate_3d("point", x = 0, y = 0, z = 0). Since all 3D layers in a panel are now depth-sorted together, annotations no longer need to be embedded in another layer. Annotations now appear in every facet panel, rather than only the first. - The
annotateargument of layer functions is deprecated. Annotations given through it are now added to the plot as layers of their own. - New
geom_plane_3d()draws planes clipped to the panel. Planes can be given asxintercept,yintercept, orzintercept(perpendicular to an axis); asintercept,slope_x, andslope_y(z = intercept + slope_x * x + slope_y * y, as from a linear model); or as general coefficientsa * x + b * y + c * z = d. Given as parameters, all forms are vectorized, so e.g. a linear system can be drawn in one call; the regression and general forms can also be mapped from data. Planes are split where they intersect other polygons, lit from the side facing the viewer, never removed by back-face culling, and don’t expand the plot’s axis ranges. - New
geom_refline_3d()draws infinite lines through a point (x,y,z) in a direction (dx,dy,dz), clipped to the panel, such as the line spanned by a vector. Directions can be given as parameters or mapped from data. Like planes, lines don’t expand the plot’s axis ranges. - New
geom_axes_3d()draws x, y, and z axis lines through an origin (default0), each spanning the panel’s full range, with arrowheads and labels at their high ends. Unlike the axes on the edges of the panel, they are part of the 3D scene, and are depth-sorted with it. -
annotate_3d()gains the types"polygon"(filled polygons from their vertices, withgroupto draw several in one call), and"axes","refline", and"plane", which build layers with the new geoms above. Segment annotations take anarrow, likegeom_segment_3d(). -
geom_point_3d(),geom_segment_3d(),geom_path_3d(),geom_polygon_3d(),geom_text_3d(), and the new geoms gain anon_topargument, which draws the layer on top of the rest of the scene instead of depth-sorting it with it, so other geometry never hides or cuts it. This keeps labels legible wherever they fall, as inannotate_3d("text", ..., on_top = TRUE).
Reference elements
- New
geom_dropline_3d()draws lines from points to their projections on faces of the panel, given byfaces(default"zmin", the floor). -
geom_point_3d()gains aflatargument: withflat = TRUE, points placed on a face of the panel byposition_on_face()are drawn as markers lying flat in that face, like shadows, rather than as symbols facing the viewer. Flat markers can be open, solid, or filled circles, squares, diamonds, or triangles, match the size of ordinary markers at the centre of the panel, and are drawn in true perspective, outlines included, regardless ofscale_depth. Withposition_on_face(c("3D", "zmin")), one layer draws the points and their shadows. - The reference element arguments of
geom_point_3d()andstat_point_3d()(raw_points, and those startingref_) are deprecated in favour of separate layers: drop lines withgeom_dropline_3d(), and reference points withgeom_point_3d(position = position_on_face()), addingflat = TRUEfor circles. They still work, with a warning. - The
pointsargument ofgeom_smooth_3d()andstat_smooth_3d(), and thepoint_*arguments that style the points, are deprecated. Draw the data points with a separate layer, e.g.+ geom_point_3d(); points requested throughpoints = TRUEare now added as such a layer.
Other changes
ggcube now requires ggplot2 4.0.0 or later.
geom_polygon_3d()now lights each polygon using the normal of its own plane, oriented by its vertex winding. Lighting previously treated every polygon as a tile of a surfacez = f(x, y), so vertical polygons were lit as if horizontal, and polygons wound clockwise were lit as their reverse side. With the default lighting, polygons are now lit the same way whatever their winding order.geom_contour_3d()andgeom_ridgeline_3d()now work with facets. They processed every panel’s data together, so faceted plots mixed panels’ contours and could leave panels empty.ggcube now requires systemfonts 1.2.0 or later. Earlier versions lack the glyph outline functions ggcube uses, so loading ggcube against them failed.
New
direction()builds the length-3 direction vectors used bycoord_3d()andlight()from an azimuth and an elevation in degrees, for cases where angles are easier to reason about than components. Azimuth is a compass bearing measured clockwise from the positive y axis, matching terrain shading conventions.coord_3d()gains adirectionargument: an alternative topitch,roll, andyawthat specifies the direction the scene is viewed from, sodirection = c(1, 1, 1)views the cube from its xmax-ymax-zmax corner. The z axis is kept pointing up the plot, and supplyingdirectionalongside any of the Euler angles is an error. The default rotation corresponds todirection(210, 30).print.light()now reports a directional light’s azimuth and elevation alongside its direction vector.orbit_3d()andanimate_3d()gainlight_azimuthandlight_elevationarguments for moving the light, alone or together with the camera. They follow the same conventions asdirection()and take the same forms as the rotation arguments, soorbit_3d(p, yaw = c(360, 0), light_elevation = c(0, 90))spins the camera with horizontal drag and raises the light with vertical drag. They move the plot-level light, and error if lighting is disabled, positional, or overridden by a layer’s ownlight.New
facet_wrap_views()andfacet_grid_views()show a plot from several angles at once, with one panel per view. They vary viewing angles the wayfacet_wrap()andfacet_grid()vary data variables:p + facet_wrap_views(~yaw)shows views around the vertical axis, andp + facet_grid_views(roll ~ yaw)varies roll down the rows and yaw across the columns. Light angles can be faceted too. Each angle in the formula gets a sensible default range, which its argument can override, and every panel is framed identically.orbit_3d()can now show the angles of the current view, so a view found by dragging can be copied intocoord_3d(). Pressron any orbit to show or hide the readout, or usereadout = TRUEto have it showing when the orbit opens. When the orbit moves the light, the readout also shows the light’s azimuth and elevation, for use withdirection(). Angles are shown in the terms the orbit was given, soyaw = c(0, 360)reports values from 0 to 360.Fixed a bug where
geom_text_3d()(billboard method) could draw labels at each other’s depth-scaled sizes under perspective projection.geom_segment_3d()andgeom_path_3d()gain anarrow.fillargument. Closed arrowheads are now filled with the line colour by default, as in ggplot2; previously they were drawn hollow.Points, segments, and text lying in the plane of a planar polygon (e.g. a line drawn along a plane) are now always drawn on top of it. Previously, especially under perspective projection, they were often hidden behind it.
Depth sorting now treats the reference lines of point layers as segments rather than degenerate polygons.
coord_3d()now draws axis lines from theaxis.linetheme elements, plus a newaxis.line.z. Each line runs along the cube edge carrying its axis’s ticks and labels, so it appears only alongside rendered panels. Axis lines are blank in most complete themes, but plots usingtheme_classic()now show them. Lines run toward increasing data values, so arrows set withelement_line(arrow = )point up each axis. They are depth-scaled with the cube edges, under the"border"key ofcoord_3d(scale_depth = ).Depth sorting now splits polygons that pass through one another, so intersecting planes and surfaces render with correct occlusion along their line of intersection. Outlines are drawn only along original polygon edges, not along the cuts.
Depth sorting now determines the order of a polygon and any element overlapping it on screen (polygon, segment, point, or text) exactly from their 3D geometry where possible, rather than from sampled screen-space depths. This fixes misordering under perspective projection, which was common for elements close to large polygons.
Segments that pass through polygons are now split at the exact 3D crossing point. Previously, under perspective projection, most such crossings went undetected and the rest were split at visibly wrong positions.
When depth sorting finds cyclic overlaps, it now drops only constraints that are part of a cycle. Previously it could discard many unrelated constraints, and pairwise sorting of large layers is now considerably faster when cycles occur.
Fixed a spurious “Cycle breaking exceeded maximum iterations” warning for layers containing a single element.
Fixed a bug where depth sorting never constrained the order of a segment relative to a polygon it overlapped on screen, so segments (e.g. residual lines and annotation segments) could be drawn on the wrong side of polygons.
ggcube 0.3.0
CRAN release: 2026-09-05
Changes to default behaviors, which slightly alter styling relative to prior version:
-
coord_3d()now draws axis ticks by default, whereas previously axis ticks were not implemented. They can be removed or styled with the standardaxis.ticksandaxis.ticks.lengththeme elements plus a newaxis.ticks.z. - Axis text now gets depth scaling, i.e. perspective-based sizing, by default. (Axis titles remain unscaled.) You can use
coord_3d(scale_depth = c(text = FALSE))to opt out. - Axis text and title placement is now computed at draw time from measured text dimensions. This fixes inexact size estimates that previously sometimes led to text overlapping titles and/or axes on small devices.
New options:
- Lighting can now be set for a whole plot by adding
light()to it, e.g.ggplot(...) + coord_3d() + light(direction = c(1, 0, 0)). This is the recommended way to specify plot-level lighting, and works regardless of where it appears in the plot expression. Thelightargument ofcoord_3d()continues to work; supplying both is an error. -
light("none")is a new way to disable lighting, equivalent to the string"none"accepted bylightarguments. -
scale_deptharguments are now more customizable: in 3D layer functions you can now pass continuous values to reduce or exaggerate the effect (rather than just a logical flag), and incoord_3d()you have more granular control over scaling for gridlines, ticks, and axis text.
Bug fixes:
- Fixed a bug where
scale_z_continuous()andscale_z_discrete()stored z scales in a package-level cache, causing plots to sometimes contaminate each other’s z-axis ranges. Now, the z scale is stored directly in the plot being built, preventing cross-contamination. -
geom_hull_3d()’s"alpha"method got some bug fixes related to face orientation and theradiusparameter.
ggcube 0.2.0
CRAN release: 2026-07-21
- The new
orbit_3d()function builds HTML widgets that let you interactively rotate ggcube plots. - Plotmath is now supported in axis text and titles; e.g.,
xlab(expression(italic(alpha[1])))now behaves as expected. - Fixed several bugs, including issues with
element_rect(),guide_legend_3d(), and camera-anchored lighting.
