Skip to contents

cellucid_view() opens a Seurat, SingleCellExperiment or SummarizedExperiment object, or a file, in the Cellucid viewer: in a Jupyter notebook cell's output, the RStudio or Positron Viewer pane, or a browser. cellucid_serve() serves it for a browser elsewhere (a computing cluster reached through an SSH tunnel) and prints the address. Neither needs Python.

Usage

cellucid_view(
  data,
  ...,
  pane = c("auto", "viewer", "browser", "notebook", "none"),
  port = 8765L,
  host = "127.0.0.1",
  allowed_hosts = NULL,
  serve_web_ui = TRUE,
  web_cache_dir = NULL,
  web_source_url = "https://www.cellucid.com",
  cache_bytes = NULL,
  quiet = FALSE
)

cellucid_serve(
  data,
  ...,
  port = 8765L,
  host = "127.0.0.1",
  allowed_hosts = NULL,
  serve_web_ui = TRUE,
  web_cache_dir = NULL,
  web_source_url = "https://www.cellucid.com",
  cache_bytes = NULL,
  quiet = FALSE
)

cellucid_stop(viewer)

cellucid_stop_all()

cellucid_viewers()

Arguments

data

A Seurat, SingleCellExperiment or SummarizedExperiment object (read as cellucid_prepare(data = ) reads it), or the path of a file or folder the viewer opens: a Cellucid-ready or any AnnData .h5ad, .zarr folder or .zarr.zip, a MuData Zarr, .h5mu, .loom, a 10x .h5, an .rds or .rda (every format of the web app's registry, system.file("input_formats.json", package = "cellucid")). Cellucid's own files are not datasets: a .cellucid-session file (the history of one dataset; read it with cellucid_read_session()) or a .cellucid-library file (datasets of the web app's My datasets) is refused before anything is served, with the sentence the web app says for it (the registry's cellucidFiles). An h5Seurat file is not opened: it is refused before anything is served, with the route that converts it (read it with SeuratDisk::LoadH5Seurat() and pass the object, or save it with saveRDS()). A gzip-compressed copy of a file read in parts (.h5ad.gz, .loom.gz, .h5.gz, .h5mu.gz) is refused too, with how to decompress it: a served file is read in parts, which a gzip stream has none of (the web app itself opens such a copy from a link or from the computer, reading it whole once).

...

For an object, the arguments of cellucid_prepare() that choose what is shown: dataset_id, dataset_name, embeddings, obs, modalities, modality, latent_key, embedding_defaults, obs_keys and the others, except out_path, format, zarr_format and force. For a path: dataset_id, dataset_name and the launch defaults (embedding_defaults, latent_key, obs_keys, var_gene_id_column, vector_field_default, modality, normalize_embeddings, centroid_min_points), which the browser checks against the file. Without dataset_id and dataset_name (and no record of them in the object, misc$cellucid or metadata(x)$cellucid), the dataset is named after the expression passed as data, or the file. Of a file, only a Zarr store's own identity (uns['cellucid']) is read; an .h5ad or .h5mu is named after the file (this package reads no HDF5), where cellucid-python's serve_anndata() reads its uns['cellucid'] and refuses a file that names none. Sessions are kept under the id, so give dataset_id to keep them across both.

pane

Where the viewer opens: "auto" the cell's output in a Jupyter notebook (an IRkernel session), else the RStudio or Positron Viewer pane when there is one, else the browser; "notebook" the cell's output only; "viewer" the pane only; "browser" the default browser; "none" nowhere (the address is printed). See "In a Jupyter notebook" below.

port

The port to serve on. The default 8765 is the one local address Google accepts for opening Google Drive files (community annotation's GitHub sign-in works only on www.cellucid.com); when it is taken, the next free port is used and the message says so. A port you choose is used or refused. 0 picks a free port.

host

The address to listen on. The default listens on this machine only – which, on a machine several people use at once (an HPC node, RStudio Server, Posit Workbench), is every user of it: the served data carries no secret, so any of them can read it while the viewer runs; use the Jupyter widget (no port) or a machine only you use when that matters. "0.0.0.0" listens on every interface (others on the network can then open the viewer).

allowed_hosts

Extra host names the server answers to, needed only behind a proxy that forwards to it under another name (RStudio Server, Posit Workbench, a reverse proxy): bare names such as "rstudio.example.org", matched on any port.

serve_web_ui

FALSE serves the data and the channel only, for a viewer page served elsewhere.

web_cache_dir

The folder holding the web app's verified build; tools::R_user_dir("cellucid", "cache")/web when NULL.

web_source_url

The site the web app is downloaded from, at first use and whenever it publishes a new build. NULL serves the cached build as it is, with no network access. In a Jupyter notebook the browser loads the app from this site itself (it must then be an http(s) address).

cache_bytes

The memory the served store may keep of what it makes by a transform, in bytes (least recently used first out); NULL keeps a 64th of the machine's memory (256 MiB on a 16 GB machine), the share the web app gives its decoded chunks and cellucid-python's served stores keep: chunks of text and of a dense matrix. A chunk that is a slice of a vector (most of them) is made again each time, in one pass at memory speed, and never kept. An object's sparse expression matrix is laid out gene-major once, in the background, from the moment the viewer starts (X first, then any other matrix), on every core (two under R CMD check): each thread lays out a share of its cells (a genes x cells matrix: its values of each gene counted, then placed, one binary search a cell plus a write a value) or of its genes (a cells x genes matrix: each gene's values sorted by priority), into memory-mapped scratch files in R's temporary folder (removed as soon as they are mapped; the operating system keeps their pages or writes them to disk), never a copy in R's memory, as cellucid-python's served store lays its matrices out. Until a matrix is laid out, a gene the viewer reads is made from the layout's progress: the values already laid out are copied, and those of the cells not reached yet are found by one scan of the matrix's values in its own order, on every core while the layout stands; when the layout would end sooner (a gene asked near its end, or a burst of genes), it is waited for. Then a gene, or a subsample's first values of one, is a copy of its bytes. A dense matrix is read through its transpose, block by block, and never copied whole. A matrix held outside R's memory (BPCells, a DelayedArray backend) is read only by R's own thread, so it is laid out there, before the viewer opens, block by block into the same scratch files (8 bytes a stored value): one pass counts its values, then one pass places each quarter of the machine's memory of them; it is never held whole in R's memory, whatever its size. The neighbour graph is not copied either: each chunk of it (\(2^{18}\) entries) is made from the graph as it is, both axes in priority order, when the viewer reads it, and not kept (at one million cells and 20 million edges the served store holds no second graph).

quiet

TRUE prints nothing: neither the address nor the notes reading the data writes (columns not stored, gene names made unique).

viewer

A viewer cellucid_view() or cellucid_serve() returned.

Value

cellucid_view() and cellucid_serve() return the viewer, invisibly: pass it to cellucid_act() and the action functions (cellucid_color_by(), cellucid_highlight_cells(), ...), cellucid_on() and the other functions that talk to it. cellucid_stop() returns the viewer invisibly; cellucid_stop_all() the number of viewers stopped; cellucid_viewers() a list of the running viewers.

Details

An object is read once, when the viewer starts, into the store cellucid_prepare(format = "zarr") would write – the same plan, the same arrays and values – held in memory: its metadata is built at once and every chunk is made only when the viewer asks for it, so a large object opens in about the time its cells' annotations and first embedding take. The served store is not compressed (it declares no compressor), since the browser reads it over the loopback interface, where deflating each chunk on R's one thread cost far more than the bytes it saved; files cellucid_prepare() writes are compressed. The browser reads it as it reads any Cellucid-ready AnnData, with range requests. Its cells are in priority order (seed 0, as cellucid_prepare() writes them by default), so a subsample the viewer opens is the first rows of every array and the first entries of every gene, and only its cells are sent; the original positions, which selections, highlights and labels name, are the ones cellucid_prepare() gives: an object read from a file in priority order whose record (misc$cellucid$priority, metadata(x)$cellucid$priority) still describes its cells is put back in its original order first; any other is taken in its own order, and a cellucid_original_position column without such a record names nothing (a warning says so). An object changed after the viewer started is not shown changed (R copies on change): view it again. A path is served from disk as it is, with exact Range answers; the browser reads the file itself. R has no HDF5 reader and no reader of Zarr chunks to serve a file in priority order, so for a file whose cells are not in priority order already (a Zarr store cellucid_prepare() wrote is) the viewer says so as it starts: a subsample then reads the storage blocks that hold its cells. To send only its cells, read the file into R and view the object, write it once with cellucid_prepare(), or serve it from Python (cellucid serve), which serves .h5ad, .h5mu, Loom, Cell Ranger and AnnData and MuData Zarr files in priority order. An .rds, .rda or MatrixMarket folder is read whole once by the browser. A file served as it is is listed to the viewer with its size and a strong ETag – its size, modification time, status-change time and identity (inode; on Windows NTFS's change time and file index), read from the file opened when the viewer asks – the validator its bytes are answered with: the browser keeps what it read of the file (its byte cache, gene indexes, an .rds converted once) under that version across visits, and a file rewritten while it is served, in place or replaced, is a new version, never read into what was kept of the old one.

The server runs in the background (httpuv): the R console stays usable while the viewer is open, and the viewer's requests are answered whenever R is idle. It answers only requests that name it (the Host check refuses DNS-rebinding pages with 421) – unless it listens on a non-loopback IP address (host = "0.0.0.0", say) with no allowed_hosts, an explicit request for network exposure whose names cannot be known – and opens the viewer channel – a WebSocket that carries commands to the viewer and its events back – only for a page of its own, a loopback or an allowed_hosts origin, and only with the viewer's token, which the printed address carries (so another user of the machine can read the served data but cannot drive the viewer without that address). The browser lets only the hosted app's pages (origins https://www.cellucid.com and https://cellucid.com) and pages on this machine read the data across origins. The channel is pinged every 20 seconds, so a proxy between the page and R (RStudio Server, Posit Workbench) does not close it for being idle, and a socket that does not say who it is within 60 seconds is closed. The web app itself is the published build of https://www.cellucid.com, downloaded once into web_cache_dir and checked file by file against its SHA-256 inventory before any byte is served; cellucid_clear_web_cache() removes it.

Several viewers run at once, each on its own port. cellucid_stop() tells an open page to freeze (it keeps its last view, as cellucid_freeze() does), closes the viewer's channel, waits for the page to answer the close (at most a second, so that the close reaches it) and frees its port, as cellucid-python's stop() does; cellucid_stop_all() stops every viewer of the session.

A Viewer pane that renders without WebGL stays blank: view it with pane = "browser".

In a Jupyter notebook

In a Jupyter notebook with the R kernel (IRkernel: JupyterLab, Jupyter Notebook 7, VS Code), cellucid_view() shows the viewer in the cell's output: a notebook widget – the one cellucid-python's show_anndata() shows – that frames the Cellucid app loaded from web_source_url and reaches this R session through the kernel's own connection (a Jupyter comm). No server is started and no port is opened, so it works the same on a laptop and on a remote JupyterHub. It needs the IRkernel and IRdisplay packages and the anywidget Python package where the notebook server runs (pip install anywidget), which shows the widget.

The kernel hands the widget the viewer's messages between cells: while a cell runs, the viewer waits for data it has not read yet, and a call that waits for the viewer (cellucid_wait_ready(), or cellucid_session_result() or cellucid_action_result() before the viewer answered) stops at once with what to do instead; actions never wait. Hooks registered with cellucid_on() run, and cellucid_selection() is current, whenever the kernel is idle. port, host, allowed_hosts, serve_web_ui and web_cache_dir do not apply.

A cell run again replaces its output: the viewer cellucid_view() showed in the cell's earlier run is stopped (cellucid_stop()), its object's matrices, memoised arrays and chunk cache let go, unless another cell shows it too. The cell is known by the id the front end sends with each run (JupyterLab, Notebook 7, VS Code). Where no cell id is sent, and outside a notebook, cellucid_view() of a dataset stops the earlier viewer of the same dataset id, and says so.

In Google Colab's R runtime (detected from its COLAB_RELEASE_TAG environment variable) the widget's display also names Colab's widget manager, as google.colab.output.enable_custom_widget_manager() does for Python: Colab shows third-party widgets only then. The widget's module is then loaded from jsDelivr.

A notebook front end that does not show widgets shows the widget's text instead. A server is no way around it inside IRkernel, which never runs R's event loop between cells: cellucid_serve() in a notebook answers nothing until a cell runs. Serve from an R session outside the notebook (R in a terminal, on the same machine) and open the viewer in a browser tab, or write the data with cellucid_prepare() and open it at https://www.cellucid.com or with the Python package's show_anndata().

Agent apps on this computer

While it runs, a viewer with a server (cellucid_view() outside a notebook, cellucid_serve()) is published to agent apps on this computer: a record readable by this user only in the agents folder (CELLUCID_AGENTS_DIR, else the user's cache folder), naming an endpoint of the viewer's own server, /_cellucid/agent/<viewer id>, that takes a secret token and answers this computer only, never a browser. Through it cellucid_mcp(), or cellucid-python's cellucid mcp, lets an app such as Claude Code work in the viewer under the person's permission settings. cellucid_stop() withdraws it (so does R's exit). A notebook widget's viewer has no server and is not published, and CELLUCID_AGENTS=off publishes none. The record names where the viewer is served, never its page address (which carries the viewer's token). See cellucid_mcp() for the folder's rules.

See also

cellucid_mcp() for agent apps on this computer; cellucid_act() (and one function per action, such as cellucid_highlight_cells()) and cellucid_on() to talk to the viewer, cellucid_prepare() to write the same store to share.

Examples

if (interactive() && requireNamespace("SeuratObject", quietly = TRUE)) {
  data("pbmc_small", package = "SeuratObject")
  viewer <- cellucid_view(SeuratObject::UpdateSeuratObject(pbmc_small))
  cellucid_highlight_cells(viewer, colnames(pbmc_small)[1:10])
  cellucid_stop(viewer)
}

# Serve the data without the web app, on a free port, and stop it.
if (requireNamespace("SeuratObject", quietly = TRUE)) {
  data("pbmc_small", package = "SeuratObject")
  viewer <- cellucid_serve(
    SeuratObject::UpdateSeuratObject(pbmc_small),
    port = 0L, serve_web_ui = FALSE, quiet = TRUE
  )
  viewer
  cellucid_stop(viewer)
}
#> Validating object structure
#> Updating object slots
#> Ensuring keys are in the proper structure
#> Updating matrix keys for DimReduc ‘pca’
#> Updating matrix keys for DimReduc ‘tsne’
#> Ensuring keys are in the proper structure
#> Ensuring feature names don't have underscores or pipes
#> Updating slots in RNA
#> Updating slots in RNA_snn
#> Setting default assay of RNA_snn to RNA
#> Updating slots in pca
#> Updating slots in tsne
#> Setting tsne DimReduc to global
#> Setting assay used for NormalizeData.RNA to RNA
#> Setting assay used for ScaleData.RNA to RNA
#> Setting assay used for RunPCA.RNA to RNA
#> Setting assay used for BuildSNN.RNA.pca to RNA
#> No assay information could be found for FindClusters
#> Warning: Adding a command log without an assay associated with it
#> Setting assay used for RunTSNE.pca to RNA
#> Setting assay used for JackStraw.RNA.pca to RNA
#> Setting assay used for ScoreJackStraw.pca to RNA
#> Setting assay used for ProjectDim.RNA.pca to RNA
#> Setting assay used for FindVariableFeatures.RNA to RNA
#> Validating object structure for Assay ‘RNA’
#> Validating object structure for Graph ‘RNA_snn’
#> Validating object structure for DimReduc ‘pca’
#> Validating object structure for DimReduc ‘tsne’
#> Object representation is consistent with the most current Seurat version