A reusable ivue example starts with coordinates, observation IDs, annotations, and the meaning of any connections or frames. This guide constructs three small illustrative inputs in base R; it does not fit an embedding or propose a dataset-generation API. Use the function guide to choose an entry point and the introduction for extended plotting controls.
| What you need | Functions to combine | What to retain or share |
|---|---|---|
| Finite n-by-3 positions and one numerical value per row | color.scale.cont(), plot3D.cont() |
Coordinates, IDs, scale, initial camera, HTML widget. |
| The same positions and category labels | color.scale.groups(), plot3D.groups() |
Named palette, level order, annotations, HTML widget. |
| A vertex set, weighted edges, and supplied positions | prepare.graph(), plot3D.graph(), optional
layers |
Vertex IDs, normalized edge order, weight meaning, coordinates. |
| Triangle indices or an independent height grid | layer3D.mesh() or layer3D.surface() with a
point plot |
Connectivity or grid coordinates; these have different reuse semantics. |
| Stable observation rows over two or more frames | map.colors(), animate.frames() |
Original frames and labels, retained indices, fixed colors, player. |
| A finished static or animated view | htmlwidgets::saveWidget(); for animation,
write.animation.gif() |
Interactive HTML plus dependencies, or a noninteractive GIF. |
Data, scales, cameras, and layer specifications can be kept together
in an R list and saved with base R’s saveRDS(). Keep the
original scientific inputs and preparation choices as well as the
widget.
This helix samples 24 equally spaced parameter values from one complete turn. Height is a numerical annotation; the sign of height defines two illustrative categories. These categories are assigned by construction, not discovered clusters. The first and last points share horizontal position but differ in height, so they are distinct observations.
n <- 24L
t <- seq(0, 2 * pi, length.out = n)
X <- cbind(x = cos(t), y = sin(t), z = seq(-1, 1, length.out = n))
ids <- sprintf("point-%02d", seq_len(n))
rownames(X) <- ids
annotations <- data.frame(
id = ids, height = X[, "z"],
half = factor(ifelse(X[, "z"] < 0, "Lower", "Upper"),
levels = c("Lower", "Upper"))
)
# Simulate an annotation table arriving in another order, then align it.
annotations <- annotations[rev(seq_len(n)), ]
stopifnot(!anyNA(annotations$id), !anyDuplicated(annotations$id),
setequal(annotations$id, rownames(X)))
annotations <- annotations[match(rownames(X), annotations$id), ]
stopifnot(identical(annotations$id, rownames(X)),
identical(dim(X), c(n, 3L)), all(is.finite(X)))
head(annotations, 3)
#> id height half
#> point-01 point-01 -1.0000000 Lower
#> point-02 point-02 -0.9130435 Lower
#> point-03 point-03 -0.8260870 LowerExplicit table matching is useful when keeping several annotation columns together. Alternatively, pass a named annotation vector: point and graph plots both match its names to observation IDs, regardless of vector order. For example, the following reversed vector still associates each height with its own point:
named.height <- setNames(annotations$height, annotations$id)
named.height <- named.height[rev(seq_along(named.height))]Named vectors must cover the coordinate IDs exactly. Duplicate,
missing, empty, partial, or extra names are errors, as are named point
annotations without explicit coordinate row names. Automatic data-frame
row numbers are not IDs. Unnamed vectors, including table columns
extracted with $, follow row position;
unname() explicitly requests that behavior for a named
vector. If you subset or reorder coordinates, rebuild any row-indexed
connectivity. Missing positions are errors in static plots. Missing
annotation values can remain and receive the scale’s missing color.
These four vertices and two connections are chosen for illustration. The weights represent supplied target distances; they are not inferred from the drawn segment lengths. The fourth vertex is isolated and is still retained.
vertices <- c("start", "bend", "end", "isolate")
edges <- data.frame(from = c("start", "bend"), to = c("bend", "end"),
weight = c(2, 4))
coords <- rbind(start = c(0, 0, 0), bend = c(1, 1, 0),
end = c(2, 0, 1), isolate = c(0, 2, 1))
graph <- prepare.graph(edges, vertices = vertices, weight.type = "distance")
stopifnot(identical(graph$vertices$id, vertices),
nrow(graph$edges) == 2L, all(is.finite(coords)),
identical(rownames(coords), graph$vertices$id))
graph$edges
#> from to weight
#> 1 1 2 2
#> 2 2 3 4The prepared edge endpoints are integer indices into
graph$vertices, not external IDs. We deliberately shuffle
coordinate and annotation input order below. As with point annotations,
plot3D.graph() aligns named inputs by exact ID. Graph
plotting additionally puts coordinates into prepared vertex
order; its layers refer to that order. Point plotting keeps the
supplied coordinate order.
coords <- coords[c("end", "start", "isolate", "bend"), , drop = FALSE]
values <- c(isolate = 40, bend = 20, start = 10, end = 30)
graph.view <- plot3D.graph(graph, X = coords, values = values,
edge.width = c(1, 3), edge.col = "gray55",
point.size = 8, legend.width = 100, height = 340,
camera = camera.zup(zoom = 0.55),
layers = list(layer3D.path(c(1, 3), col = "#D55E00", width = 2),
layer3D.labels(1:4, vertices, offset = c(0, 0, 0.15))))
info <- attr(graph.view, "ivue")
stopifnot(identical(rownames(info$X), vertices),
identical(info$mapping$colors,
map.colors(unname(values[vertices]), info$mapping$scale)$colors))
graph.viewInteractive 3D view of 4 observations.
The orange geometric path joins start directly to end without changing the graph topology; the gray graph edges go through bend. Labels sit 0.15 coordinate units above their vertices. Edge widths are explicit visual choices in prepared edge order, independent of weights. No layout is computed. See the introduction for reciprocal adjacency lists, matrices, and optional igraph layouts.
This three-frame triangle first gains a vertex and then expands by a factor of 1.4 about the coordinate origin. The change is explicitly constructed; these frames are neither solver iterations nor physical time measurements.
triangle <- rbind(a = c(0, 0), b = c(1, 0), c = c(0.5, 0.9))
first <- triangle
first[3, ] <- NA_real_
frames <- list(first, triangle, triangle * 1.4)
frame.labels <- c("Two vertices", "Complete triangle", "Expanded by 1.4")
frame.edges <- rbind(c(1, 2), c(2, 3), c(3, 1))
stopifnot(length(frames) == length(frame.labels),
all(vapply(frames, function(f) identical(dim(f), c(3L, 2L)) &&
identical(rownames(f), rownames(triangle)) &&
all(rowSums(is.finite(f)) == 2L | rowSums(is.na(f)) == 2L), logical(1))),
all(frame.edges >= 1L & frame.edges <= nrow(triangle)))player <- animate.frames(frames, edges = frame.edges, labels = frame.labels,
fps = 1, loop = FALSE, max.frames = NULL,
col = c("#2166AC", "#B2182B", "#009F87"), point.size = 9, height = 300)
playerCoordinate animation of 3 observations across 3 retained frames.
Press Play, step with the slider, or use Reset. Blue and red are present at the start; green and its two incident edges appear in the second frame. Two-dimensional input is drawn at z = 0, initially face-on. The three vertex colors are fixed across frames. No labels or categories are inferred from them. The player’s text labels describe frames, not observations.
An all-missing row means inactive, preserving its identity for later frames. A partially missing row is invalid. Bounds stay fixed across all original frames, and every retained frame has equal duration. No interpolation or alignment occurs. Record meaningful original frame labels for a real solver trace or measured sequence; uniform playback does not recover irregular time intervals. The animation guide gives larger examples and explicit frame-selection recipes.
The example below checks a small HTML export and, when magick is installed, a three-frame GIF, then removes all outputs. It does not launch a viewer. Choose your own persistent directory when keeping an export.
local({
directory <- tempfile("ivue-recipe-")
dir.create(directory)
on.exit(unlink(directory, recursive = TRUE))
htmlwidgets::saveWidget(player, file.path(directory, "triangle.html"),
selfcontained = FALSE)
stopifnot(file.exists(file.path(directory, "triangle.html")))
if (requireNamespace("magick", quietly = TRUE)) {
gif <- write.animation.gif(player, file.path(directory, "triangle.gif"),
width = 240, height = 240, final.hold = 0)
stopifnot(file.exists(gif))
}
})HTML retains interaction and must accompany its dependency folder
unless saved with selfcontained = TRUE (which needs
Pandoc). GIF is a fixed orthographic raster rendering using the R
widget’s initial camera; it does not capture browser rotations. See the
export
comparison for controls, portability, and limitations.
The bundled example adds a real identity and provenance check. Rendering the full case study is left to the retinal-development vignette, which includes static posters and complete drawing recipes.
retina <- readRDS(system.file("extdata", "retinal-development.rds", package = "ivue"))
ids <- retina$annotations$id
stopifnot(identical(names(retina$coordinates), c("umap", "sknn")),
identical(dim(retina$annotations), c(12000L, 3L)),
!anyNA(ids), !anyDuplicated(ids),
identical(retina$graph$vertices, ids),
nrow(retina$graph$edges) == 4108L,
all(retina$graph$edges$from %in% ids),
all(retina$graph$edges$to %in% ids))
for (coords in retina$coordinates) {
stopifnot(identical(dim(coords), c(12000L, 3L)),
identical(rownames(coords), ids), all(is.finite(coords)))
}
data.frame(representation = names(retina$coordinates),
observations = 12000L, dimensions = 3L)
#> representation observations dimensions
#> 1 umap 12000 3
#> 2 sknn 12000 3
c(age.levels = nlevels(retina$annotations$age),
cell.type.levels = nlevels(retina$annotations$cell.type),
displayed.edges = nrow(retina$graph$edges))
#> age.levels cell.type.levels displayed.edges
#> 10 11 4108There are two 12,000-by-3 matrices (umap,
sknn), an undirected distance weighted graph with 4,108
edges, and an annotation table with synthetic id, 10-level
age, and 11-level cell.type.
provenance records the source, fitting cohort, methods,
sample, checksums, and upstream package versions.
The upstream fitting cohort had 120,804 cells. The published analysis retained 107,052 retinal cells; the bundle displays a stratified sample of 12,000 retained cells. Both embeddings were fitted on the full cohort before extracting these display rows. The displayed graph is an induced subgraph of the full fitting graph: it keeps only edges whose endpoints both survive display selection and can be disconnected. No embedding is refitted on this smaller graph.
Published UMAP used Canberra distance on the upstream representation. The symmetric 4-nearest-neighbor graph used Euclidean distance, followed by weighted-GRIP and edge-KK layout. Distance metrics define comparisons of upstream observations; embedding/layout methods produce display coordinates. ivue renders those results. UMAP coordinates were centered for bundling; the README capture additionally applies independent rigid rotations to face each P14 centroid toward the viewer. The recipe camera alone does not perform that alignment. The README’s PHATE view uses Euclidean distance and was also fitted on the full cohort, but its coordinates occur only in external assets, not in this RDS. No raw expression, principal-component matrix, original barcodes, or sample identifiers are bundled.
The values come from Clark et
al. (2019), GEO GSE118614, and the Goff lab’s published coordinates
and annotations. Their source
use agreement contains a prepublication consent restriction tied to
January 1, 2019. The documented source review found no separate dataset
license. Public availability and the elapsed date are not a
public-domain claim or a new permission grant; the package’s GPL code
license does not establish rights in third-party values. The installed
extdata/README.md and stored
provenance$source.terms preserve these limitations.
Shared colors and IDs help compare the same cells, but apparent proximity, separation, or a smooth age gradient is not by itself evidence of equivalent geometries, clustering fidelity, lineage, or biological causation. Interpret the plots together with the upstream analyses and the case study’s limitations.
Reusing only a camera allows each scene to fit its own coordinate extent. For a spatial comparison, also fix the framing range, equal aspect, and widget size. This illustrative triangle is contracted by one half without changing its observation IDs. Both views use the same range and orthographic camera, so the contracted triangle occupies half the linear screen extent.
reference <- rbind(a = c(-1, -1, 0), b = c(1, -1, 0), c = c(0, 1, 1))
common.bounds <- rbind(x = c(-2, 2), y = c(-2, 2), z = c(-2, 2))
orientation <- camera.zup(elevation = 35, turn = 0)
full <- plot3D.plain(reference, limits = common.bounds, camera = orientation,
width = 300, height = 300, point.size = 8,
description = "Reference triangle at its original scale.")
contracted <- plot3D.plain(reference / 2, limits = common.bounds,
camera = orientation, width = 300, height = 300, point.size = 8,
description = "The same triangle contracted by half.")
htmltools::tagList(full, contracted)Reference triangle at its original scale.
The same triangle contracted by half.
The ranges must contain every observation. They frame the view
without adding points or clipping planes. Spheres, meshes, surfaces, and
labels cannot expand an explicit range; leave enough room for them. With
aspect = "normalized", axis stretching is calculated from
the supplied ranges, so equal data-unit lengths across axes no longer
have equal screen scales.