5  Three-dimensional scatterplots

library(rtemis.draw)

Attaching package: 'rtemis.draw'
The following object is masked from 'package:graphics':

    Axis

draw_scatter3d() uses ECharts-GL for interactive points and paths. Drag to rotate the view and scroll to zoom. Each axis spans its observed range inside an equal-sided box; the displayed lengths do not imply identical units or physical scales.

5.1 Compare three measurements

birds <- penguins[complete.cases(penguins[c("bill_len", "bill_dep", "flipper_len")]), ]
draw_scatter3d(birds[c("bill_len", "bill_dep", "flipper_len")],
  group = birds[["species"]], xlab = "Bill length (mm)",
  ylab = "Bill depth (mm)", zlab = "Flipper length (mm)", height = 650)

The orthographic camera preserves parallel lines. alpha changes elevation, beta changes azimuth, and view_size controls the viewing extent. Set these when preparing a reproducible static view.

5.2 Configure and export

config <- setup_Scatter3DConfig(x = "bill_len", y = "bill_dep", z = "flipper_len",
  group = "species", alpha = 25, beta = 35,
  xlab = "Bill length (mm)", ylab = "Bill depth (mm)", zlab = "Flipper length (mm)")
figure <- draw(config, data = birds)
figure

SVG output contains depth-ordered vector circles, a wireframe, and editable axis labels. Its point positions match the configured browser camera. Axis label placement is adapted for static readability. Export uses the authored camera, rather than a later browser rotation or zoom.

save_drawing(figure, "penguins-3d.svg", width = 900, height = 700)

5.3 Follow a trajectory

Use mode = "lines" or mode = "both" when observation order carries meaning. Here each vertex is the same trading day across three stock indices:

indices <- as.matrix(datasets::EuStockMarkets)
indices <- indices[seq(1, nrow(indices), by = 10), 1:3]
draw_scatter3d(indices, mode = "lines", line_width = 2,
  title = "European stock indices over time", height = 650)

Paths preserve input order. Use order = "x" to sort each complete run by x; missing coordinates break a path and sorting does not connect across a break. Groups create separate paths with linked point and line legend entries.

5.4 Add model predictions

Fit the model first, then pass predictions on a rectangular grid. The drawing stores numerical predictions, so it does not depend on a particular model class or estimation language. For a surface, the prediction matrix has one row per x coordinate and one column per y coordinate.

fuel_model <- lm(mpg ~ wt + hp, data = mtcars)
weight_grid <- seq(min(mtcars$wt), max(mtcars$wt), length.out = 15)
power_grid <- seq(min(mtcars$hp), max(mtcars$hp), length.out = 15)
prediction_grid <- expand.grid(wt = weight_grid, hp = power_grid)
prediction <- matrix(predict(fuel_model, prediction_grid),
  nrow = length(weight_grid))

observed <- draw_scatter3d(mtcars$wt, mtcars$hp, mtcars$mpg,
  xlab = "Weight (1000 lb)", ylab = "Horsepower", zlab = "Fuel economy (MPG)",
  palette = "#4078A6", height = 650)
surface_figure <- draw_add_surface(observed, weight_grid, power_grid, prediction,
  name = "Linear prediction", color = "#EF8A00", opacity = 0.35)
surface_figure

Grid coordinates are sorted together with their predictions. Missing predictions remove adjacent grid cells. Adding a surface extends the axis ranges to include it; expand = FALSE instead requires predictions to lie within the existing ranges. Surface shading uses a constant color, with opacity controlling how much of the observations remain visible through it.

The same save_drawing() call exports paths and surfaces as vector geometry. The exporter splits intersecting shapes to preserve visibility and retains editable text. Very dense or highly intersecting surfaces can generate large SVG files; use a coarser prediction grid when preparing a figure for export.

save_drawing(surface_figure, "fuel-surface.svg", width = 900, height = 650)
© 2026 E.D. Gennatas