Retinal Development: Point-Cloud and Graph Views

This case study shows the same 12,000 mouse retinal-development cells in two three-dimensional representations. The first is a published UMAP point cloud. The second is a symmetric k-nearest-neighbor graph with an independently computed graph layout. Both embeddings are fitted on 120,804 cells before extracting the same 12,000 cells for display. UMAP uses Canberra distance; the symmetric-kNN graph uses Euclidean distance. Holding cell identities, annotations, colors, and initial camera settings fixed makes the representational change visible without implying that the two geometries are equivalent.

ivue renders both views. It does not compute UMAP coordinates, construct the graph, or optimize the graph layout.

The README also shows a PHATE embedding fitted to the same full-population 20-PC matrix, using Euclidean distance and 2,000 spectral landmarks before extracting these display cells. This vignette develops the two views included in the bundled data; PHATE coordinates and Python dependencies are not bundled.

For a task index, see Finding your way around ivue; for small reproducible inputs, see Example data and recipes.

Load the prepared case study

The installed package includes a visualization-ready object. It contains two coordinate matrices, a weighted edge table, categorical annotations, and provenance. It contains no expression matrix, principal-component matrix, barcodes, sample identifiers, or local source paths.

path <- system.file("extdata", "retinal-development.rds", package = "ivue")
if (!nzchar(path)) {
  candidates <- c(
    file.path("inst", "extdata", "retinal-development.rds"),
    file.path("..", "inst", "extdata", "retinal-development.rds")
  )
  path <- candidates[file.exists(candidates)][1]
}
retina <- readRDS(path)
dim(retina$coordinates$umap)
#> [1] 12000     3
dim(retina$coordinates$sknn)
#> [1] 12000     3
nrow(retina$graph$edges)
#> [1] 4108

Rows have synthetic identifiers that agree across both coordinate matrices, the graph, and the annotation table.

ids <- retina$annotations$id
stopifnot(
  identical(rownames(retina$coordinates$umap), ids),
  identical(rownames(retina$coordinates$sknn), ids),
  identical(retina$graph$vertices, ids)
)
table(retina$annotations$age)
#> 
#>  E11  E12  E14  E16  E18   P0   P2   P5   P8  P14 
#>  849  196 2578  623 2222 1053 1728  727 1151  873

Data lineage

The source is the retained mouse retinal-development single-cell RNA-seq data from Clark et al. (2019), available as GEO GSE118614. The published analysis fitted 20 principal components to log10(CPT + 1) values for 3,290 high-variance genes in 120,804 cells, then retained 107,052 retinal cells. Its three-dimensional UMAP used Canberra distance.

The case-study sample contains 12,000 retained cells selected reproducibly across nonempty developmental-stage-by-cell-type strata, with a minimum of 20 cells per stratum and seed 20190619. Both views use those exact identities in the same order.

Source and use terms

The published coordinates and annotations are publicly available, but the source review found no separate dataset license. The upstream use agreement states a prepublication consent restriction tied to January 1, 2019. Public availability and the elapsed date do not establish public-domain status or a new permission grant. The package’s GPL code license does not establish rights in third-party values. See the installed extdata/README.md and retina$provenance$source.terms for the documented review and limitations.

Shared categorical scales

The bundle records display transformations in retina$provenance$display.transforms. Both coordinate sets are centered on the 12,000 selected cells. UMAP retains its coordinate scale; the graph layout is divided by its maximum centered radius, giving a displayed radius of one. The original graph center and radius were not retained in the historical cache and are explicitly recorded as unavailable (NA). The UMAP center is recorded, and future graph preparation runs retain both parameters.

Graph weights and fitted diagnostics are not rescaled. Do not compare displayed segment lengths directly with distance weights. Figure-specific rigid rotations are applied later during rendering and are not stored in either coordinate matrix.

Named palettes make annotation colors independent of factor order and reusable across coordinate systems.

age.colors <- c(
  E11 = "#512A84", E12 = "#4148A4", E14 = "#2E68B4",
  E16 = "#1686B7", E18 = "#009FA8", P0 = "#28B58B",
  P2 = "#67C36B", P5 = "#A4C84F", P8 = "#D2B943", P14 = "#E2873C"
)
cell.colors <- c(
  "Early RPCs" = "#3B6C8E", "Late RPCs" = "#7A5195",
  "Neurogenic Cells" = "#D45087", "Retinal Ganglion Cells" = "#E45756",
  "Amacrine Cells" = "#F58518", "Horizontal Cells" = "#9C755F",
  "Photoreceptor Precursors" = "#54A24B", Cones = "#72B7B2",
  Rods = "#4C78A8", "Bipolar Cells" = "#B279A2",
  "Muller Glia" = "#8F9D44"
)
age.scale <- color.scale.groups(retina$annotations$age, colors = age.colors)
cell.scale <- color.scale.groups(
  retina$annotations$cell.type, colors = cell.colors
)
camera <- camera.zup(elevation = 18, turn = -28, fov = 0, zoom = 0.57)

Published UMAP as a point cloud

The UMAP coordinates are taken from the published metadata and centered only for display. No UMAP fitting or graph construction occurs in this vignette. plot3D.groups() associates annotation values with coordinate rows.

umap.by.age <- plot3D.groups(
  retina$coordinates$umap,
  groups = retina$annotations$age,
  scale = age.scale,
  point.size = 2.2,
  alpha = 0.78,
  axes = FALSE,
  aspect = "equal",
  camera = camera
)
umap.by.age

Compare the embeddings without edges

Before drawing graph topology, render the symmetric-kNN layout coordinates with the same point function used for UMAP. This isolates the change in geometry from the visual effect of an edge overlay.

sknn.points.by.age <- plot3D.groups(
  retina$coordinates$sknn,
  groups = retina$annotations$age,
  scale = age.scale,
  point.size = 2.2,
  alpha = 0.78,
  axes = FALSE,
  aspect = "equal",
  camera = camera
)
sknn.points.by.age

The code holds cell identities, stage colors, point styling, projection, and initial camera fixed. The static poster additionally uses the independent rigid orientations described below. The two widgets created by the code rotate independently; synchronized capture is a separate README production step.

Add the symmetric kNN graph

For the graph view, dgraphs constructed Euclidean symmetric kNN graphs from the same 20-PC representation used upstream of UMAP, using all 120,804 cells. The connected k = 4 reference was selected after reviewing a response series (k = 3, 4, 6, 8, 12, 16, 29) and three layout seeds at k = 3 and k = 4. grip fitted weighted-GRIP followed by edge-KK refinement to the full graph with 374,597 edges, before extracting the displayed cells. Connectivity alone does not establish geometric fidelity.

The bundled edge table contains only original edges whose endpoints both occur in the display sample. This induced subgraph can be disconnected even though the full fitting graph is connected. No layout is refitted on the induced graph.

prepare.graph() validates the bundled edge table without loading rgl or recomputing a layout. The weights are declared as distances, but visual edge width is chosen explicitly rather than inferred from them.

graph <- prepare.graph(retina$graph)
nrow(graph$vertices)
#> [1] 12000
nrow(graph$edges)
#> [1] 4108
graph$weight.type
#> [1] "distance"
graph.by.age <- plot3D.graph(
  graph,
  X = retina$coordinates$sknn,
  groups = retina$annotations$age,
  scale = age.scale,
  point.size = 2.1,
  alpha = 0.84,
  edge.col = "#59687324",
  edge.width = 1,
  axes = FALSE,
  aspect = "equal",
  camera = camera
)
graph.by.age

The README preview shows the extracted coordinates without edges; the code above adds the induced graph’s edge overlay.

Change the annotation, not the geometry

The cell-type view reuses the coordinates, graph, camera, and rendering settings. Only groups and scale change.

umap.by.cell.type <- plot3D.groups(
  retina$coordinates$umap,
  groups = retina$annotations$cell.type,
  scale = cell.scale,
  point.size = 2.2,
  alpha = 0.78,
  axes = FALSE,
  aspect = "equal",
  camera = camera
)

graph.by.cell.type <- plot3D.graph(
  graph,
  X = retina$coordinates$sknn,
  groups = retina$annotations$cell.type,
  scale = cell.scale,
  point.size = 2.1,
  alpha = 0.84,
  edge.col = "#59687324",
  edge.width = 1,
  axes = FALSE,
  aspect = "equal",
  camera = camera
)

This separation is useful when comparing views: changing geometry should not silently change the sampled observations, annotation mapping, or palette.

Interpretation and boundaries

The UMAP and graph-layout coordinates answer different visualization questions. UMAP displays a nonlinear embedding produced by its own neighbor and optimization choices. The graph layout displays one selected symmetric kNN topology and its weighted drawing objective. Apparent proximity in either view is not evidence that the other representation contains the same neighborhoods.

Reproduction and provenance

The README’s graph animation and the original two-panel comparison use the same bundled visualization values but are captured as synchronized 72-frame rotations, each lasting 14.4 seconds. Both comparison panels use the same point styling, orthographic projection, and camera angles at each frame. Before rotation, each embedding is independently oriented so that its P14 centroid faces the viewer. These are rigid display rotations, not refits or deformations of the coordinates.

In the source repository, make readme-retinal reconstructs the upstream PC matrix and graph layout, renders the point-cloud hero and UMAP comparison, verifies their WebGL canvases, and encodes the GIFs. The graph animation, two-panel comparison, case-study object, and vignette posters are regenerated with:

make retinal-vignette

The README’s three-panel comparison adds the separately computed PHATE coordinates. The repository includes its PHATE fitting script and rendering script.

The stored provenance records source checksums, graph-selection diagnostics, layout information, and the versions of the packages that computed the graph and layout.

retina$provenance[c("citation", "sample", "umap")]
#> $citation
#> [1] "Clark et al. (2019), Neuron 102:1111-1126.e5; doi:10.1016/j.neuron.2019.04.010; GEO GSE118614"
#> 
#> $sample
#> [1] "12,000 cells stratified by developmental stage and cell type; minimum 20 per nonempty stratum; seed 20190619"
#> 
#> $umap
#> [1] "Published three-dimensional UMAP fitted on 120,804 cells, then subsetted to the matched display cells; Canberra distance"
retina$provenance$graph.input
#> $representation
#> [1] "first 20 PCs of log10(CPT + 1)"
#> 
#> $genes
#> [1] 3290
#> 
#> $distance
#> [1] "Euclidean"
#> 
#> $cells
#> [1] 120804
#> 
#> $retained.cells
#> [1] 107052
#> 
#> $sampled.cells
#> [1] 12000
retina$provenance$graph.selection$selected
#> [1] 4
retina$provenance$fitting.graph
#> $vertices
#> [1] 120804
#> 
#> $edges
#> [1] 374597
#> 
#> $components
#> [1] 1
#> 
#> $displayed.edges
#> [1] 4108
#> 
#> $display.rule
#> [1] "induced subgraph; layout fitted before display subsampling"
retina$provenance$graph.layout$method
#> [1] "grip::edge.kk(init = 'weighted_grip')"