For a task index, see Finding your way around ivue; for small reproducible inputs, see Example data and recipes.
On this page: A sample from a saddle · Reusable numerical colors · Textbook-style axes and a z-up camera · A triangular mesh beneath the spheres · Groups and highlighting · A gridded reference surface · Weighted graphs and geometric layers · Cameras, metadata, and export
The poster uses the same observations as the first interactive example below. It shows the saddle shape without requiring scripts or WebGL; it is a static projection, with no interactive rotation.
The input is an n x 3 numeric matrix, or an all-numeric
data frame. Each row is one observation. Coordinates must be finite;
ivue does not silently drop rows with missing coordinates.
Explicit coordinate row names are observation IDs. Named
values, groups, point colors, logical
highlight masks, and style colors match those IDs exactly, as in graph
plots; missing, duplicate, or extra names cause errors. Unnamed vectors
follow row position. Use unname() deliberately if
annotation names are not IDs. Point coordinates and indexed layers keep
their supplied row order.
set.seed(1)
xs <- runif(250, -1, 1)
ys <- runif(250, -1, 1)
zs <- 1.2 * (xs^2 - ys^2)
X <- cbind(x = xs, y = ys, z = zs)plot3D.plain() uses rgl to create an
interactive browser widget. Assigning the result to plain
defers viewing; the final plain expression displays it in
this vignette, in RStudio’s Viewer, or in a browser from an interactive
R console. Scene creation uses an off-screen device, so no native
graphics window is needed.
All plot3D functions start with z pointing upward and x
and y in the horizontal data plane. The default is
camera.zup(elevation = 20, turn = -135, fov = 0, zoom = 0.8):
an orthographic view with positive x down-left and positive y down-right
on screen. Dragging the widget can change this orientation.
plain <- plot3D.plain(X, col = "#197A68", point.size = 5,
axes = TRUE, xlab = "x", ylab = "y", zlab = "z",
description = "250 green observations on a saddle; opposite sides curve upward and downward.")
plain250 green observations on a saddle; opposite sides curve upward and downward.
point.size is a screen-pixel size. Spheres are a
separate primitive, with point.type = "sphere" and
sphere.radius measured in coordinate units. For example,
plot3D.plain(X, point.type = "sphere", sphere.radius = 0.02)
draws spheres of radius 0.02. A missing radius uses one percent of the
largest coordinate span. Dense sphere scenes can be much more expensive
than points.
The default aspect = "equal" preserves equal units
across axes. Choose aspect = "normalized" only when
stretching axes independently is intended; it changes relative geometric
distances.
Continuous interpolation is the default. Here zero has a scientific
meaning, so we specify both a center and a diverging palette. Automatic
limits become symmetric around that center. Creating a scale and
inspecting its color and legend mappings do not require rgl
or open a plot.
height.scale <- color.scale.cont(zs, center = 0,
palette = c("#2166AC", "#F7F7F7", "#B2182B"))
mapping <- map.colors(zs, height.scale)
head(mapping$colors)
#> [1] "#EAEEF2FF" "#6091C2FF" "#F6F4F4FF" "#CB6A76FF" "#DEA7AEFF" "#F5F2F3FF"
mapping$legend
#> label color count
#> 1 -1.180 #2166ACFF NA
#> 2 -0.588 #8AADD1FF NA
#> 3 0.000 #F6F6F6FF NA
#> 4 0.588 #D48791FF NA
#> 5 1.180 #B2182BFF NAcolored <- plot3D.cont(X, values = zs, scale = height.scale,
point.size = 5, legend.title = "Saddle height")
coloredInteractive 3D view of 250 observations.
Construct one scale from a reference sample and reuse it to compare
plots. limits fixes the numerical range. Values outside it
are squished by default; oob = "censor" maps them to the
missing color, while oob = "error" rejects them. Missing
values use na.color without removing coordinate rows.
For binned colors, request them explicitly. Break calculations retain
full precision. The default digits = NULL increases legend
precision until nearby boundaries are distinguishable; an explicit
digits fixes precision and warns if labels are ambiguous.
There is no automatic winsorization. center is a
continuous-scale control; for binned diverging colors, supply the
intended breaks and bin colors directly.
binned <- color.scale.cont(zs, mode = "binned",
breaks = c(-1.2, -0.4, 0.4, 1.2),
palette = c("#2166AC", "#EEEEEE", "#B2182B"))
map.colors(zs, binned)$legend
#> label color count
#> 1 [-1.2, -0.4] #2166AC 71
#> 2 (-0.4, 0.4] #EEEEEE 136
#> 3 (0.4, 1.2] #B2182B 43palette can be a vector of colors or a function of the
requested number of colors. color.map instead receives
numerical values and must return one color per value. Do not supply
both. Numeric palette indices, including missing colors, are fixed when
a scale is fitted, so later changes to R’s palette() do not
change its colors. A color.map callback is run at mapping
time; its dependence on mutable external state remains the caller’s
responsibility.
The earlier saddle sample is uniform in its horizontal parameter
square, not in surface area. For z = C * (x^2 - y^2), the
surface-area factor is sqrt(1 + 4*C^2*(x^2 + y^2)).
Accepting uniform-square proposals with probability proportional to this
factor gives a surface-area-uniform sample. Batches accumulate until
there are at least n accepted points; the final line
retains exactly n of them.
set.seed(1)
n <- 500
C <- 0.8
half.width <- 1
xy <- matrix(numeric(), ncol = 2)
while (nrow(xy) < n) {
proposal <- matrix(runif(4000, -half.width, half.width), ncol = 2)
area <- sqrt(1 + 4*C^2 * rowSums(proposal^2))
accept <- runif(nrow(proposal)) < area / sqrt(1 + 8*C^2*half.width^2)
xy <- rbind(xy, proposal[accept, , drop = FALSE])
}
xy <- xy[seq_len(n), , drop = FALSE]
saddle <- cbind(x = xy[, 1], y = xy[, 2],
z = C * (xy[, 1]^2 - xy[, 2]^2))
byr.scale <- color.scale.cont(saddle[, "z"], center = 0,
palette = c("blue", "yellow", "red"))layer3D.axes() adds three axes through the origin, with
arrowheads at their positive ends. It does not rotate the data or choose
a camera. The example below keeps the default z-up orientation and uses
zoom = 0.4 to leave room for the origin-crossing axes and
legend:
saddle.view <- plot3D.cont(
saddle, values = saddle[, "z"], scale = byr.scale,
point.type = "sphere", sphere.radius = 0.02,
legend.title = "Saddle height", legend.width = 160,
axes = FALSE, aspect = "equal",
layers = list(layer3D.axes(head.length = 0.04, head.angle = pi/8)),
camera = camera.zup(elevation = 20, turn = -135, fov = 0, zoom = 0.4),
height = 500L
)Interactive 3D view of 500 observations.
Zero is yellow; negative and positive heights move toward blue and
red, respectively. scale takes a color-scale object, not a
raw color vector. Reusing byr.scale and the original height
values preserves observation colors if these points are subsequently
displayed in another coordinate configuration.
The axes extend beyond the data automatically. Set
origin to move their intersection, or use explicit
limits, for example
rbind(x = c(-1.2, 1.2), y = c(-1.2, 1.2), z = c(-1, 1)), to
fix endpoints across views. These limits do not clip data.
head.length is a fraction of the full axis span;
head.angle is the cone half-angle in radians.
width, col, cex, and
label.offset control the shafts, colors, label size, and
label gaps. The heads are solid cones in data coordinates, so they keep
their 3D geometry while the widget rotates. Use equal aspect to preserve
their proportions.
With turn = -135, positive x points down-left and
positive y down-right. With turn = 0, positive x is
horizontal to the right and positive y recedes. elevation
changes the angle above the xy plane; fov = 0 removes
perspective. The helper only specifies the initial view: dragging can
still tilt z, and at elevations of plus or minus 90 degrees z points
along the viewing direction. Neither the axes constructor nor the camera
helper needs rgl until a plot is actually drawn. The same layer and
camera also work with plot3D.graph().
This poster projects the same sample and triangulation used below. It is a flat diagram of the geometry; it does not reproduce WebGL lighting or spheres.
Connecting neighboring observations with triangular faces makes the saddle’s shape easier to see. Construct a Delaunay triangulation in the original two-dimensional parameter plane, then draw its faces at the three-dimensional saddle coordinates. Triangulating the 3D coordinates instead would construct a different object, not this surface mesh.
The geometry package is an add-on, not part of base R.
Install it with install.packages("geometry") to run this
example. It is suggested for the vignette, but
layer3D.mesh() does not require it when triangle indices
are already available. The example below is evaluated when both
geometry and rgl are installed.
triangles <- geometry::delaunayn(saddle[, c("x", "y")])
surface <- layer3D.mesh(
triangles, col = "gray75", alpha = 0.2,
edge.col = "gray45", edge.alpha = 0.35, edge.width = 1
)Each row of triangles contains three indices into
saddle. The mesh joins those observations with planar
faces, approximating the surface over the sample’s parameter-space
convex hull. It does not extend to unsampled corners of the square. The
points, colors, axes, and initial camera below are the same as in the
preceding plot.
saddle.mesh <- plot3D.cont(
saddle, description = "500 saddle observations joined by triangular faces; blue is below zero, yellow is zero, red is above zero.", values = saddle[, "z"], scale = byr.scale,
point.type = "sphere", sphere.radius = 0.02,
legend.title = "Saddle height", legend.width = 160,
axes = FALSE, aspect = "equal",
layers = list(surface, layer3D.axes()),
camera = camera.zup(elevation = 20, turn = -135, fov = 0, zoom = 0.4),
height = 500L
)500 saddle observations joined by triangular faces; blue is below zero, yellow is zero, red is above zero.
alpha controls face opacity; edge.alpha
independently controls the mesh lines. Shared edges are drawn once. Set
alpha = 0 for a wireframe or edges = FALSE to
show faces without their outlines.
To inspect another embedding of these same observations, retain
surface and replace only the plotting coordinates. For
example, if Z contains the metric-MDS or
metric-MDS-followed-by-edge-KK coordinates in the same row order:
plot3D.cont(
Z, values = saddle[, "z"], scale = byr.scale,
point.type = "sphere", sphere.radius = 0.02, axes = FALSE,
layers = list(surface, layer3D.axes()), camera = camera.zup()
)Do not retriangulate Z: fixed triangle connectivity
reveals how the original mesh stretches, folds, or collapses. Original
heights continue to determine point colors. The mesh is a visualization
overlay, separate from any symmetric-kNN graph used for fitting or
fidelity scoring; adding it does not change that graph or its
objectives.
Groups need not be clusters. Factors retain their level order; other
vectors use first-occurrence order. Named colors make the assignment
explicit and stable across datasets. Unknown groups raise an error
unless the scale uses unknown = "missing". Missing factor
levels are treated as missing, not as the literal group
"NA". Empty strings remain valid groups. Legends quote
group labels when needed to distinguish an empty label or a group called
"Missing" from the missing-value entry.
First, color and highlight the original 250-point sample. Then reuse the same group scale on the mesh-backed saddle below.
groups <- factor(ifelse(zs < 0, "Negative", "Nonnegative"),
levels = c("Negative", "Nonnegative"))
group.scale <- color.scale.groups(groups,
colors = c(Negative = "#D95479", Nonnegative = "#009F87"))
map.colors(groups, group.scale)$legend
#> label color count
#> 1 Negative #D95479 135
#> 2 Nonnegative #009F87 115plot3D.groups(X, groups, scale = group.scale, point.size = 5,
highlight = abs(zs) > 0.4,
highlight.style = list(point.size = 7),
non.highlight.style = list(col = "gray75", alpha = 0.25))Interactive 3D view of 250 observations.
highlight changes styling, not membership: all 250 rows
keep their original IDs and the color scale is not refitted. Legends
describe the base color scale and global alpha, not the
per-highlight overrides. Scale colors can include alpha, and global
alpha multiplies it. A highlight-style alpha
overrides the global multiplier for that subset, while preserving its
color’s alpha.
plot3D.groups() accepts the same sphere, mesh, axis, and
camera controls as plot3D.cont(). To color the preceding
mesh-backed saddle by group, replace the continuous values
and byr.scale with group labels and
group.scale. Derive the labels from saddle,
which has 500 rows, rather than reusing the 250 labels associated with
X:
saddle.groups <- factor(ifelse(saddle[, "z"] < 0, "Negative", "Nonnegative"),
levels = c("Negative", "Nonnegative"))saddle.mesh.groups <- plot3D.groups(
saddle, groups = saddle.groups, scale = group.scale,
point.type = "sphere", sphere.radius = 0.02,
legend.title = "Height group", legend.width = 160,
axes = FALSE, aspect = "equal",
layers = list(surface, layer3D.axes()),
camera = camera.zup(elevation = 20, turn = -135, fov = 0, zoom = 0.4),
height = 500L
)Interactive 3D view of 500 observations.
Negative-height spheres are pink and nonnegative-height spheres are
green, using the same named colors as the simpler example. The gray
surface, mesh connectivity, sphere radii, axes, and camera are unchanged
from saddle.mesh; only the point coloring and legend
differ. Here all points retain their group colors, without highlighting.
Reusing surface does not require another triangulation.
The triangular mesh above follows the plotted observations. When a
surface is known analytically, layer3D.surface() can
instead draw a reference with its own coordinates. Here the original
250-point sample X lies on
z = 1.2 * (x^2 - y^2); evaluate that formula on a regular
grid covering its parameter square:
grid.x <- grid.y <- seq(-1, 1, length.out = 31)
grid.z <- outer(grid.x, grid.y, function(x, y) 1.2 * (x^2 - y^2))
reference <- layer3D.surface(grid.x, grid.y, grid.z,
col = "lightblue", alpha = 0.3)Entry grid.z[i, j] is the height at
(grid.x[i], grid.y[j]). No triangulation call or
geometry installation is needed. Overlay the reference
beneath the sample, reusing the earlier height scale:
reference.view <- plot3D.cont(
X, values = X[, "z"], scale = height.scale, point.size = 5,
legend.title = "Saddle height", legend.width = 160, axes = FALSE,
layers = list(reference, layer3D.axes()),
camera = camera.zup(zoom = 0.6), height = 450
)Interactive 3D view of 250 observations.
Set edges = TRUE in layer3D.surface() for
grid lines and lit = TRUE for lighting. Faces are planar
triangles; increasing grid resolution approximates the smooth surface
more closely. Unlike the earlier mesh, reference stays
fixed when reused with another set of plotting coordinates. To compare
an MDS or refined embedding against it, first align the embedding to the
reference coordinate system. The layer performs no alignment or
rescaling and does not modify the graph used to compute the
embedding.
A graph supplies topology and weights. Coordinates supply geometry; one does not substitute for the other. The following graph contains an isolated vertex.
vertices <- data.frame(id = c("A", "B", "C", "D"),
label = c("Start", "Middle", "End", "Isolate"))
edges <- data.frame(from = c("A", "B"), to = c("B", "C"), weight = c(2, 4))
coords <- rbind(A = c(0, 0, 0), B = c(1, 1, 0),
C = c(2, 0, 1), D = c(0, 2, 1))
graph <- prepare.graph(edges, vertices = vertices, weight.type = "distance")
graph$edges
#> from to weight
#> 1 1 2 2
#> 2 2 3 4
coords <- coords[c("C", "A", "D", "B"), ]
values <- c(D = 40, B = 20, A = 10, C = 30)plot3D.graph(graph, X = coords, values = values, edge.col = "gray50",
edge.width = 1 + graph$edges$weight,
point.type = "sphere", sphere.radius = 0.07,
layers = list(layer3D.path(c(1, 2, 3), col = "#D55E00", width = 4),
layer3D.labels(1:4, vertices$label, offset = c(0, 0, 0.15))))Interactive 3D view of 4 observations.
The other accepted formats are paired
adj.list/weight.list lists, dense or sparse
adjacency matrices, and igraph objects. Sparse matrix inputs require
Matrix; igraph inputs require igraph.
Preparing a graph with prepare.graph() does not require
rgl. For the same graph:
adjacency <- list(
adj.list = list(A = 2L, B = c(1L, 3L), C = 2L, D = integer()),
weight.list = list(A = 2, B = c(2, 4), C = 4, D = numeric()))Undirected adjacency entries must be reciprocal, with matching weights. Matrices use zero to mean no edge; use tables or lists for actual zero-weight edges. Coordinate row names are aligned to vertex IDs. Without row names, coordinates follow vertex order. Named values and groups are independently aligned by exact vertex ID, as in the shuffled example above. Unnamed annotation vectors follow vertex order. Duplicate, missing, partial, and extra names are rejected. Named point colors, highlight-style color vectors, and logical highlight masks use the same rule. Layer row indices and numeric highlight indices refer to graph vertex order after alignment.
Weights do not automatically control edge width or color. The example
chooses 1 + graph$edges$weight explicitly. Edge colors and
widths follow the edge order exposed by prepare.graph()
before rendering; matrix/list inputs use one copy of each undirected
edge in vertex-row order. Table inputs retain their supplied order.
Prepared graphs can be reused across plots. Vertex identities, isolates, weights, and vertex and edge table attributes are retained. Integer edge endpoints index the prepared vertex table: to change vertex order, prepare a new graph from vertex IDs rather than reorder that table in place.
ivue can display coordinates produced by dgraphs,
grip, igraph, graphlayouts,
or other embedding tools. Choose a method that returns three-dimensional
coordinates, then supply them through X; ivue renders the
result without recomputing the layout. Examples include spectral
embeddings from dgraphs, multiscale layouts from
grip, and three-dimensional stress layouts from graphlayouts::layout_with_stress3D().
Not every layout in these packages supports three dimensions. The
coordinate matrix must have three columns, with rows matched to graph
vertices as described above. For table, list, or matrix graph inputs,
supplying X also avoids constructing an igraph object.
Alternatively, let ivue compute coordinates by supplying
layout instead of X. The built-in choices
"kk" and "fr" require igraph. A
custom layout function can use another package: it receives ivue’s
normalized graph and must return an n x 3 coordinate matrix
in vertex order. The examples below use the built-in adapters:
layout.widget <- plot3D.graph(adjacency, layout = "kk", weight.type = "distance",
seed = 1)
stopifnot(inherits(layout.widget, "htmlwidget"))
unweighted <- igraph::make_ring(4)
unweighted.widget <- plot3D.graph(unweighted, layout = "fr", weight.type = "unweighted")
stopifnot(inherits(unweighted.widget, "htmlwidget"))kk treats positive weights as distances; fr
requires positive strengths. No weight inversion occurs.
weight.type = "unweighted" allows missing or unit weights
only. Zero and negative weights may be drawn with supplied coordinates,
but cannot be used by these layout adapters. Directed rendering,
self-loops, and parallel edges are explicitly unsupported in this
release.
Use layer3D.edges() when you already have geometric
endpoint pairs, without requiring a graph object.
layer3D.callback() supports advanced rgl drawing; callbacks
receive coordinates and row/draw IDs but must not open, close, or switch
devices. Layers are applied before serialization, not appended to a live
browser widget later.
The widget’s metadata describes the captured R scene. Reusing its initial camera keeps related plots comparable. Rotating a widget in the browser does not update this R-side camera metadata.
camera <- attr(colored, "ivue")$camera
comparison <- plot3D.plain(X, camera = camera)
stopifnot(identical(attr(comparison, "ivue")$row.ids, seq_len(nrow(X))))
comparisonInteractive 3D view of 250 observations.
Creating or displaying a widget does not save a file. To share the
colored saddle plot, export colored explicitly with
htmlwidgets::saveWidget(). This example writes a temporary
HTML file and removes it and its dependencies after checking that the
export succeeded:
out <- tempfile(fileext = ".html")
htmlwidgets::saveWidget(colored, out, selfcontained = FALSE)
stopifnot(file.exists(out))
unlink(c(out, sub("\\.html$", "_files", out)), recursive = TRUE)For a single portable HTML file, use
selfcontained = TRUE; Pandoc must be installed. Otherwise
distribute the HTML file together with its dependency directory. Widgets
can also be used in Shiny through rgl::rglwidgetOutput()
and rgl::renderRglwidget(); Shiny is not an ivue
dependency.
Default widget width follows its container, with a height of 600
pixels. Explicit width and height set
dimensions. Long legends scroll within their allocated area, and
legend.show = FALSE permits a separate application legend
built from the same map.colors() result.