Hooks, Selections and Sessions#
Audience: analysts who want the viewer and R to work together What you’ll do: act on an open viewer, react to what you do in it, and bring a selection or a whole session back to R.
Every viewer has a channel (a WebSocket) to the R session that opened it. Actions sent before the page has connected wait and go out in order as soon as it does.
Actions#
Everything a person can do in the viewer – and everything an agent reads
from it – is an action of one catalogue, and every action is a function of
this package, generated from the catalogue (inst/action_catalog.json, the
web app’s own, with its help page: ?cellucid_highlight_cells):
viewer <- cellucid_view(seu)
cellucid_color_by(viewer, field = list(source = "obs", key = "seurat_clusters"))
cellucid_highlight_cells(viewer, cells = WhichCells(seu, idents = "B"), color = "#377eb8")
cellucid_cells_visibility_set(viewer, cells = seu$nCount_RNA < 500, visible = FALSE)
cellucid_highlight_clear_all(viewer)
cellucid_camera_reset(viewer, view = "live")
cellucid_mapping_set(viewer, modality = "ADT") # a Seurat object with an ADT assay
Each is one step of the viewer’s history, labelled as R’s, undone like a
person’s click. cellucid_act(viewer, "color.by", field = ...) sends any
action named in data (its arguments are written .viewer and .action, so
no action parameter is partially matched to them). Fields, genes and
categories are named, never positions. Cells are given by name, by 1-based
position, or as TRUE/FALSE per cell, and travel as original positions in
one binary frame. Every action returns a request, invisibly, at once:
request <- cellucid_fields_list(viewer)
fields <- cellucid_action_result(request) # in a notebook: in a later cell
request$step # the history step a change made
cellucid_action_result(request, timeout = 60) returns the outcome (a read’s
result, as lists) or stops with the viewer’s reason when it did not apply the
action; a refusal nobody collects arrives as a command_error naming its
request (a warning unless a hook handles it). cellucid_mapping_set() changes
only the groups it names; the mapping the viewer then shows becomes
viewer$data_mapping. An object is served with one
matrix per assay (its data layer, else counts) and no layers, so the
mapping’s expression stays "X"; change the modality, the graph (knn) or
the embeddings instead. A file given by path may hold layers
(expression = "layer:counts").
cellucid_view() on a Seurat object of the Pancreas sample, opened in a
browser tab, after R ran cellucid_color_by() on clusters and
cellucid_highlight_cells() on the Ductal cells. The Dataset box reads Zarr v2
folder · R: the object is served from memory by this R session.#
The history#
Each action R sends is one step of the viewer’s history, labelled R in the viewer’s State › History list, beside the person’s own steps and an agent’s. R reads the history and moves it as the viewer’s own buttons do:
history <- cellucid_action_result(cellucid_history_get(viewer))
history$head # the step shown now
length(history$steps) # the steps from the first to the one shown
cellucid_history_undo(viewer) # back one step, as Undo does
cellucid_history_redo(viewer) # forward again
cellucid_history_jump(viewer, step = "s3") # show any step history_get() lists
cellucid_history_get(viewer, all_branches = TRUE) lists every branch, not
only the path to the step shown; each step says what it did (label), who
took it (origin, with agent or host), its pseudocode line (code), when,
what it changed (touches) and whether it no longer applies (status,
note). Undo,
redo and jumps record no step; their outcome is head (and moved, false
when there was nothing to move to). The history is changed through the
generated cellucid_history_*() functions (cellucid_history_note_add(),
cellucid_history_branch_switch(), cellucid_history_turn_undo(), …), the
same actions the State panel and agents use. How the history works in the
viewer: State and History.
Turns#
turn <- cellucid_turn(viewer, "Mark the B cells", {
cellucid_color_by(viewer, field = list(source = "obs", key = "seurat_clusters"))
cellucid_highlight_cells(viewer, cells = WhichCells(seu, idents = "B"))
})
cellucid_history_note_add(viewer, target = list(turn = turn), text = "B cells by cluster.")
cellucid_history_turn_undo(viewer, turn = turn) # every step of the turn, at once
cellucid_turn(viewer, label, code) makes every action sent while code
runs one turn of the history: the State panel shows its steps as one group,
labelled label and by R, and undoes them together, as it does an agent’s
turn. It returns the turn’s id ("r-" and 12 hexadecimal digits),
invisibly. Turns do not nest; a turn whose code stops on an error closes all
the same.
What changed, and a session pushed in#
cellucid_on(viewer, "history_changed", function(change) message(change$kind, " ", change$head))
cellucid_last_history_change(viewer) # list(kind, tracker, head, step, branch)
request <- cellucid_load_session(viewer, "pbmc.cellucid-session")
cellucid_load_session_result(request) # list(tracker = list(id, name), head)
The viewer tells every change of its history as a history_changed event:
kind is "step" (a step recorded; step is its id), "moved" (undo,
redo, a jump, a branch switched to), "edited" (a note, a bookmark, a branch
renamed, locked, archived or deleted), "tracker" (a tracker started, used,
renamed or removed) or "loaded" (a session loaded); tracker is the
tracker in use, head the step shown and branch the active branch.
cellucid_last_history_change() returns the latest (NULL before the
first).
cellucid_load_session(viewer, path) sends a .cellucid-session file saved
on the dataset the viewer shows: its trackers are added beside the person’s
own, and the viewer shows the file’s active tracker, as opening the file in
the viewer does. cellucid_load_session_result(request, timeout = 60)
returns what the viewer shows then, or stops with the viewer’s reason
(another dataset’s session, a damaged file); in a Jupyter notebook, collect
it in a later cell.
Events#
cellucid_on(viewer, "selection", function(event) {
message(length(event$cells), " cells selected with the ", event$source)
})
cellucid_on(viewer, "click", function(event) str(event))
The events are selection, hover, click, ready, data_mapping,
command_error, action_result (an action’s answer, which also completes its
request), pong, debug_snapshot, session_bundle, result,
result_removed and results (what the viewer made, as
cellucid_results() lists it), color_by (the field it colours by),
history_changed (every change of its history) and session_loaded (a
session cellucid_load_session() pushed in, loaded). A handler runs
whenever R is idle; one that fails gives a warning and the others still run.
cellucid_off(viewer, "hover") removes a kind’s handlers.
Cells are 1-based positions in every event, as the actions take them: a
selection gives cells and cell_names, a hover or click gives cell
and cell_name. (Python’s events are 0-based, as Python indexes.) pong and
debug_snapshot answer cellucid_debug_connection(viewer), the report to read
first when a viewer stays blank or does not react; the channel’s own
keep-alive pings, every 20 seconds, raise no event.
A selection back in R#
selected <- cellucid_selection(viewer)
selected$cells # 1-based positions
selected$cell_names # the object's cell names
sub <- subset(seu, cells = selected$cell_names)
A selection of any size arrives at once (millions of cells decode in a fraction of a second).
The session#
request <- cellucid_request_session(viewer)
session <- cellucid_session_result(request, file = "pbmc.cellucid-session")
cellucid_trackers(session)
tracker <- cellucid_tracker(session)
cellucid_steps(tracker)[, c("id", "branch", "label", "origin")]
cellucid_request_session() asks the viewer for its whole session – its
history: every step a person, an agent or R took, each tracker and branch,
its notes, bookmarks and turns, and the state each step leaves (highlights,
colours, filters, annotations, the camera) – and cellucid_session_result()
saves it as a .cellucid-session file (session format 4: the whole history,
every tracker and branch with its notes and Recently deleted, without the
conversations; the viewer’s Share… writes a chosen part of the same) and
returns it read and checked whole: the
one way, in an R console and in a Jupyter notebook (there, collect it in a
later cell: the viewer answers between cells). Open it again in the web app
with the same dataset, under the same dataset id.
cellucid_session_result() stops with what to do when the viewer is not open
or does not answer within timeout seconds; the request stays open, so it can
be collected again.
A saved file is read with cellucid_read_session(path), which checks it
whole first: a damaged file is refused with “This session file is damaged: “
and what is wrong; a file of format 3, saved by an earlier Cellucid, is
refused with the viewer’s own sentence; and a file written by a newer
Cellucid than this package with “This session was written by a newer
Cellucid (X) than this package reads (Y); update the cellucid package to
read it.” The history reads as data frames and records, function for
function cellucid-python’s cellucid.history:
Function |
What it returns |
|---|---|
|
Every tracker, one row each ( |
|
Every branch ( |
|
Every group, a named run of consecutive steps of one branch ( |
|
Every step in step order, or a branch’s path from the root to its tip: |
|
One step’s record (a squashed step’s |
|
What differs between two steps, one row per change ( |
|
The results stale at the head: the step, the later step that changed an input it read, and those inputs |
|
Every note; every turn (a notebook’s or an agent’s group of steps); every edit of the history itself |
|
A branch (main by default) as methods text and as pseudocode, as the web app writes them: the tracker’s first note opens the methods; groups are headings in the methods and |
|
The agent conversations the file kept with a tracker |
?cellucid_trackers and ?cellucid_read_session describe every column and
field. A file shared as its steps only (without the values they leave
behind) reads, but has no states: cellucid_step_state() refuses it.
Writing a step back onto the object#
session <- cellucid_read_session("pbmc.cellucid-session")
plan <- cellucid_apply_session(seu, session) # dry run: one row per change
plan
seu <- cellucid_apply_plan(plan)
cellucid_history_applied(seu)
# Another step, a branch's last step, or the viewer's session, applied at once:
seu <- cellucid_apply_session(seu, session, step = "s5", dry_run = FALSE)
seu <- cellucid_apply_session(seu, session, branch = "Without score", dry_run = FALSE)
seu <- cellucid_apply_session(seu, cellucid_request_session(viewer), dry_run = FALSE)
cellucid_apply_session(data, session, tracker = NULL, branch = NULL, step = NULL, ..., dry_run = TRUE, overwrite = FALSE, keep_session = FALSE, timeout = 60) applies the state one step left – by default the step the
viewer showed (the active tracker’s head); branch takes a branch’s last
step, step any step (on branch when both are given) – to the object it
was saved on. session is a session cellucid_read_session() read, a file,
or a cellucid_request_session() request (collected first; in a notebook, in
a later cell). The rules are cellucid-python’s
apply_cellucid_session_to_anndata(), row for row: both are held to one
shared expected plan.
A plan first. With dry_run = TRUE (the default) nothing is written:
the cellucid_apply_plan returned lists every change, one row each, in
domain order – column-renamed, categories-renamed, categories-merged,
field-deleted (recorded, never applied: the column stays), user-field (a
column per field the session made: a factor, or numbers), gene-set,
highlight (one TRUE/FALSE column per group,
cellucid_highlight__<page>__<group> from the page’s name and the group’s
label) and presentation – with its status:
Status |
Meaning |
|---|---|
|
The object lacks it; applying writes it |
|
The object holds exactly it already |
|
A column or gene set an earlier apply wrote holds other values, and |
|
Not written; |
cellucid_apply_plan(plan), or dry_run = FALSE, writes the plan all at
once: labels renamed in place, each user-defined field and highlight group a
column of the cell metadata (Seurat meta.data, colData), the gene sets in
cellucid$gene_sets (origin "session") and, each set written or replaced,
one TRUE/FALSE column of the feature metadata (below). R copies on change: the object
passed in is not changed, and the one returned carries the attribute
"cellucid_apply_report", list(plan, applied, provenance). A plan whose
object changed since it was made is refused: make it again.
Applying again changes nothing. The same step applied twice gives every row
unchangedornot-applied, andappliedisFALSE.A column the person made is never replaced, whatever
overwritesays: its row says so and the rest applies.Presentation is reported, never applied. Each other domain whose value at the step differs from the tracker’s opening (filters, colours, views, the camera, analyses) is one
presentationrow: how the data was shown, not the data.Each apply that writes is recorded. One provenance record is appended to
cellucid$history_applied(Seuratmisc$cellucid;metadata(x)$cellucid): the session’s SHA-256, the tracker, branch and step, when, the package and its version, the columns it wrote, what it renamed and what it did not apply.cellucid_history_applied(seu)reads them, oldest first;keep_session = TRUEalso keeps the session file itself (base64), so the history can be rebuilt from the object alone. cellucid-python’suns['cellucid']['history_applied']holds the same records.Gene sets mark their genes. Each set written or replaced is one
TRUE/FALSEcolumn,cellucid_gene_set__<name>, of the feature metadata of the gene axis the data mapping reads: the assay’s for a Seurat object (meta.features, or an Assay5’smeta.data),rowData()of the main experiment, or of thealtExp()the mapping names, for a SingleCellExperiment or SummarizedExperiment (a data frame has none).TRUEon each row whose gene, as the mapping names genes, the set lists; a gene the object does not have is left out, with a warning. The name is cellucid-python’s: the set’s name with each run of characters other than ASCII letters and digits one_, trimmed (gene_setwhen nothing is left),_2,_3on a clash, given over every set of the step in order. A mapping that reads the raw matrix or no expression gets no column (the sets stay in the record, with a warning). A set an earlier apply wrote and that is replaced (overwrite = TRUE) has its column replaced in place; a column of that name beside a set no apply wrote is the object’s own and is never replaced (a warning says so). A set the step no longer holds gets no row: its record and column stay. The columns are not rows of the plan; the shared fixture pins them (apply.sequence[].var_columns), as it pins the web app’s Save AnnData with this state….The priority-order column is never touched. A plan that would write, rename or replace the cell-metadata column
cellucid_original_position(each cell’s original position in a store written in priority order) is refused before anything is written, dry runs included.Only what the chosen step needs is checked, before anything is written: the cell count, the dataset id when the object’s cellucid record names one, the data mapping the session was saved with and its gene count, the cells’ order when the object has the embedding the session names (a cell-order digest of its stored values), and the obs columns and genes the step’s state uses.
A step’s state knows the cells the viewer had open then: a cell outside them
is NA in a highlight column and in a field’s, never “not selected”. A
session shared as its steps only cannot be applied. An object read from a
Cellucid-ready store in its stored priority order is refused until
cellucid_restore_original_order() puts its cells back. If cellucid_view()
was given arguments that change what is shown (embeddings, obs,
modalities, …), pass the same ones after the session.