--- title: "Retinal Development: Point-Cloud and Graph Views" output: rmarkdown::html_vignette: vignette: > %\VignetteIndexEntry{Retinal Development: Point-Cloud and Graph Views} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", out.width = "100%") library(ivue) ``` 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](function-guide.html); for small reproducible inputs, see [Example data and recipes](example-data.html). ## 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. ```{r load-data} 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) dim(retina$coordinates$sknn) nrow(retina$graph$edges) ``` Rows have synthetic identifiers that agree across both coordinate matrices, the graph, and the annotation table. ```{r identity-check} 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) ``` ## Data lineage The source is the retained mouse retinal-development single-cell RNA-seq data from [Clark et al. (2019)](https://pmc.ncbi.nlm.nih.gov/articles/PMC6768831/), available as [GEO GSE118614](https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=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](https://github.com/gofflab/developing_mouse_retina_scRNASeq/blob/3bfeea29ecc957d59e4a156229449b060ffda0a8/README.md#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. ```{r scales} 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. ```{r umap-code, eval=FALSE} 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 ``` ```{r umap-poster, echo=FALSE} knitr::include_graphics("figures/retinal-umap.png") ``` ## 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. ```{r sknn-points-code, eval=FALSE} 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 ``` ```{r comparison-poster, echo=FALSE} knitr::include_graphics("figures/retinal-comparison.png") ``` 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. ```{r prepare-graph} graph <- prepare.graph(retina$graph) nrow(graph$vertices) nrow(graph$edges) graph$weight.type ``` ```{r graph-code, eval=FALSE} 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. ```{r graph-poster, echo=FALSE} knitr::include_graphics("figures/retinal-sknn.png") ``` ## Change the annotation, not the geometry The cell-type view reuses the coordinates, graph, camera, and rendering settings. Only `groups` and `scale` change. ```{r cell-type-code, eval=FALSE} 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: ```{r regeneration, eval=FALSE} make retinal-vignette ``` The README's three-panel comparison adds the separately computed PHATE coordinates. The repository includes its [PHATE fitting script](https://github.com/pgajer/ivue/blob/main/tools/fit-retinal-phate.py) and [rendering script](https://github.com/pgajer/ivue/blob/main/tools/render-retinal-readme.R). The stored provenance records source checksums, graph-selection diagnostics, layout information, and the versions of the packages that computed the graph and layout. ```{r provenance} retina$provenance[c("citation", "sample", "umap")] retina$provenance$graph.input retina$provenance$graph.selection$selected retina$provenance$fitting.graph retina$provenance$graph.layout$method ```