--- title: "Example data and reusable visualization recipes" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Example data and reusable visualization recipes} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 3) library(ivue) have.rgl <- nzchar(system.file(package = "rgl")) ``` 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](function-guide.html) to choose an entry point and the [introduction](ivue-introduction.html) for extended plotting controls. ## Choose a recipe | 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. ## Construct a point cloud 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. ```{r point-data} 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) ``` Explicit 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: ```{r named-annotation} 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. ## Share a scale and camera Compare height before and after an illustrative change to the annotation, while keeping positions and row identity fixed. The second value is half the first; it is not another embedding. A single numerical scale makes that reduction visible. Separate automatic scales would color both panels almost identically despite their different values. ```{r shared-settings} original <- annotations$height reduced <- original / 2 scale <- color.scale.cont(c(original, reduced), limits = c(-1, 1), center = 0, palette = c("#2166AC", "#F7F7F7", "#B2182B")) camera <- camera.zup(elevation = 20, turn = -135, fov = 0, zoom = 0.65) original.mapping <- map.colors(original, scale) reduced.mapping <- map.colors(reduced, scale) stopifnot(identical(original.mapping$legend, reduced.mapping$legend)) original.mapping$legend ``` ```{r shared-views, eval=have.rgl} original.view <- plot3D.cont(X, named.height, scale = scale, camera = camera, aspect = "equal", point.size = 7, height = 360, legend.title = "Original height", legend.width = 120) reduced.view <- plot3D.cont(X, reduced, scale = scale, camera = camera, aspect = "equal", point.size = 7, height = 360, legend.title = "Half height", legend.width = 120) stopifnot(identical(attr(original.view, "ivue")$mapping$colors, original.mapping$colors), identical(attr(original.view, "ivue")$observation.ids, ids)) original.view reduced.view ``` Both widgets start with identical coordinates, IDs, point sizes, aspect, projection, camera, and scale limits. Colors alone encode the changed values; no coordinate scaling or alignment occurs. At the negative endpoint the original view is dark blue, while half height has a lighter blue. Each widget rotates independently: this workflow does **not** synchronize interaction. For a noninteractive reader, the following small table records the same change. ```{r shared-summary} rows <- c(1L, 12L, 24L) data.frame(id = ids[rows], original = original[rows], reduced = reduced[rows], original.color = original.mapping$colors[rows], reduced.color = reduced.mapping$colors[rows]) ``` A categorical alternative uses the same observation order. This constructs and checks another view without adding a third embedded widget to the guide. Print `group.view` interactively to display it. ```{r group-recipe} group.scale <- color.scale.groups(annotations$half, colors = c(Lower = "#2166AC", Upper = "#B2182B")) map.colors(annotations$half, group.scale)$legend ``` ```{r group-view, eval=have.rgl} group.view <- plot3D.groups(X, annotations$half, scale = group.scale, camera = camera, height = 300, legend.width = 120, legend.title = "Helix half") stopifnot(inherits(group.view, "htmlwidget")) ``` Equal colors across views establish a shared annotation mapping. Equal cameras establish an initial orientation. Neither establishes equivalent geometries, clustering quality, a developmental trajectory, or biological causation. ## Prepare a small embedded weighted graph 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. ```{r graph-data} 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 ``` The 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. ```{r graph-view, eval=have.rgl} 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.view ``` 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](ivue-introduction.html#weighted-graphs-and-geometric-layers) for reciprocal adjacency lists, matrices, and optional igraph layouts. ## Record a short coordinate sequence 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. ```{r frame-data} 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))) ``` ```{r frame-view, eval=have.rgl} 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) player ``` 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](animation.html) gives larger examples and explicit frame-selection recipes. ### Save and clean up 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. ```{r export-recipe, eval=have.rgl} 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](function-guide.html#recorded-coordinate-animation-and-export) for controls, portability, and limitations. ## Inspect the retinal bundle without refitting The bundled example adds a real identity and provenance check. Rendering the full case study is left to the [retinal-development vignette](retinal-development.html), which includes static posters and complete drawing recipes. ```{r retinal-structure} 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) c(age.levels = nlevels(retina$annotations$age), cell.type.levels = nlevels(retina$annotations$cell.type), displayed.edges = nrow(retina$graph$edges)) ``` There 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)](https://doi.org/10.1016/j.neuron.2019.04.010), GEO GSE118614, and the Goff lab's published coordinates and annotations. Their [source use agreement](https://github.com/gofflab/developing_mouse_retina_scRNASeq/blob/3bfeea29ecc957d59e4a156229449b060ffda0a8/README.md#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. ## Compare spatial extents 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. ```{r shared-bounds, eval=have.rgl} 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) ``` 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.