Skip to contents

View an object

cellucid_view() opens a Seurat, SingleCellExperiment or SummarizedExperiment object in the Cellucid viewer: in the RStudio or Positron Viewer pane, in your browser, or, in a Jupyter notebook (IRkernel), in the cell’s output. Nothing is written to disk.

library(cellucid)
library(SeuratObject)
data("pbmc_small")

viewer <- cellucid_view(pbmc_small)

The object is read as cellucid_prepare(data = pbmc_small) reads it – cell metadata, every reduction as X_<name>, the assays, the SNN graph – into the same Cellucid-ready AnnData, held in memory and uncompressed (the browser reads it over loopback): only what the viewer asks for is made, when it asks, and an object’s genes x cells matrix is never transposed whole. Several assays (RNA and ADT, say) open as modalities, with a Modality choice in the viewer’s Data mapping.

The viewer is yours to talk to. Everything a person can do in it is an action, and R sends any of them: cellucid_act(viewer, "<type>", ...), or the action’s own function (?cellucid_color_by, ?cellucid_highlight_cells, and one for every other action). Each is one step of the viewer’s history, labelled as R’s, undone like any other; the request comes back at once, and cellucid_action_result() collects the viewer’s answer – a read’s result, or what a change did. Undo, redo and a jump to any step are actions too (cellucid_history_undo(), cellucid_history_redo(), cellucid_history_jump()): they move the history, as the viewer’s own buttons do, and record no step:

cellucid_color_by(viewer, field = list(source = "obs", key = "groups"))
cellucid_highlight_cells(viewer, colnames(pbmc_small)[1:20], color = "#377eb8")
history <- cellucid_action_result(cellucid_history_get(viewer))
cellucid_history_undo(viewer)  # the highlight is taken back; redo brings it again

cellucid_on(viewer, "selection", function(event) {
  message(length(event$cells), " cells selected")
})
# ... select cells in the viewer with the lasso, then:
selected <- cellucid_selection(viewer)
subset(pbmc_small, cells = selected$cell_names)

# Every highlight group, label field, score and gene set made in the viewer,
# as meta.data columns (and misc$cellucid$gene_sets), in one call:
pbmc_small <- cellucid_pull(pbmc_small, viewer)
# A column of yours, shown in the viewer as a field:
cellucid_push(viewer, pbmc_small$RNA_snn_res.1, key = "clusters")
cellucid_stop(viewer)

The R console stays usable while the viewer is open: the server answers the viewer whenever R is idle. Without a viewer, cellucid_pull() reads what the web app exports (obs-column CSV files, GMT gene sets, .cellucid-session files): cellucid_pull(pbmc_small, "~/Downloads/cellucid-exports").

A session file holds the viewer’s whole history – every step, by whom, in every tracker and branch, with its notes, bookmarks and turns. cellucid_read_session() reads it, cellucid_tracker(), cellucid_steps(), cellucid_branches() and cellucid_turns() read the history (as data frames), and cellucid_apply_session() plans, then writes, the state at any step onto the object:

session <- cellucid_session_result(cellucid_request_session(viewer))
tracker <- cellucid_tracker(session)
cellucid_steps(tracker)[, c("id", "branch", "label", "origin")]
plan <- cellucid_apply_session(pbmc_small, session, step = "s4")
plan  # what would change, one row each
pbmc_small <- cellucid_apply_plan(plan)
cellucid_history_applied(pbmc_small)

Code run inside cellucid_turn() is one turn of the viewer’s history: the State panel groups its steps, and cellucid_history_turn_undo() takes them back together. cellucid_on(viewer, "history_changed", ...) hears every change of the history, and cellucid_load_session() pushes a session file into the viewer:

turn <- cellucid_turn(viewer, "Mark the clusters", {
  cellucid_color_by(viewer, field = list(source = "obs", key = "RNA_snn_res.1"))
})
cellucid_load_session_result(cellucid_load_session(viewer, "pbmc.cellucid-session"))

In a Jupyter notebook

In an IRkernel notebook (JupyterLab, Notebook 7, VS Code, Google Colab’s R runtime) the same cellucid_view(pbmc_small) shows the viewer in the cell’s output: a notebook widget, the one cellucid-python’s show_anndata() shows, reached through the notebook’s own connection, so no port or server is involved. It needs the IRkernel and IRdisplay packages, and the Python package anywidget where the notebook server runs. R hears the viewer between cells: show the viewer in one cell, work in it, and call cellucid_pull() or cellucid_selection() in the next. Runnable notebooks, each with a Python twin and an “Open in Colab” badge: https://docs.cellucid.com/en/latest/user_guide/r_package/f_notebooks_tutorials/index.html.

Serve from a cluster

On a machine without a screen, cellucid_serve() serves the same viewer and prints its address; reach it through an SSH tunnel:

viewer <- cellucid_serve(sce)
#> Serving sce at http://127.0.0.1:8765
#> Open http://127.0.0.1:8765/?viewer=true&viewerId=...&viewerToken=...&anndata=true in a browser (from another machine: ssh -L 8765:127.0.0.1:8765 <this host>, then open the address there).
#> Stop it with cellucid_stop().

Port 8765 is the default because Google accepts only that local address for opening Google Drive files; when it is taken, the next free port is used and the message says so. (Community annotation signs in with GitHub, which works only on www.cellucid.com.) Behind RStudio Server or Posit Workbench, name the proxy’s host with allowed_hosts = "rstudio.example.org".

What the viewer reads

The web app itself is downloaded once, at first use, from www.cellucid.com into file.path(tools::R_user_dir("cellucid", "cache"), "web"), and every file is checked against the build’s SHA-256 inventory before it is served. cellucid_clear_web_cache() removes it.

The data side can be shown here without a browser: serve a store with serve_web_ui = FALSE and ask the server what a viewer would ask. Because R serves and asks in one process here, fetch() turns R’s event loop while it waits for the answer.

library(cellucid)

store <- cellucid_prepare(
  file.path(tempdir(), "viewer-example.zarr"), "zarr",
  embeddings = list(X_umap = matrix(c(0, 1, 0, 1, 0, 0, 1, 1), ncol = 2)),
  obs = data.frame(cluster = factor(c("a", "a", "b", "b"))),
  dataset_id = "viewer-example", dataset_name = "Viewer example",
  force = TRUE
)

viewer <- cellucid_serve(store, port = 0L, serve_web_ui = FALSE, quiet = TRUE)
viewer
#> <Cellucid viewer> Viewer example (viewer-example), running
#>   address:  http://127.0.0.1:11336/?viewer=true&viewerId=fe25f712dd81e415&viewerToken=0e837f4cd566e6858891a0982915c0d6&anndata=true
#>   data:     zarr, 4 cells
#>   channel:  waiting for the viewer

fetch <- function(path) {
  answer <- NULL
  pool <- curl::new_pool()
  curl::curl_fetch_multi(
    paste0(viewer$url, path), pool = pool,
    done = function(response) answer <<- response,
    fail = function(message) stop(message)
  )
  while (is.null(answer)) {
    curl::multi_run(timeout = 0, poll = TRUE, pool = pool)
    later::run_now(0.01)
  }
  rawToChar(answer$content)
}

cat(fetch("/_cellucid/datasets"))
#> {"datasets":[{"id":"viewer-example","name":"Viewer example","anndata":{"file":{"url":"_cellucid/anndata/viewer-example.zarr/","kind":"zarr","layout":"folder"},"defaults":{}}}]}
cat(fetch("/_cellucid/anndata/viewer-example.zarr/obs/.zattrs"))
#> {"_index":"_index","column-order":["cluster","cellucid_original_position"],"encoding-type":"dataframe","encoding-version":"0.2.0"}

cellucid_stop(viewer)

The browser opens _cellucid/datasets, then reads the store key by key with range requests, exactly as it reads a Cellucid-ready AnnData on any web host. A file given by path (an .h5ad, a .zarr.zip, a Loom or 10x .h5, an .rds) is served from disk as it is; the browser reads it itself.