UI modules map#
This page is a map of the UI layer: which modules exist, what DOM they own, and how they communicate with state/rendering without creating tight coupling.
It is especially useful when:
you want to add a new sidebar module,
you want to find where a UI behavior lives,
you are debugging “why did this update twice?” issues.
At a glance#
Audience
Computational users: skim “UI coordinator” and “Which module owns what?”
Developers: read fully; use the module table as a navigation map.
Time
20–40 minutes
Prerequisites
UI coordinator (the orchestrator)#
The UI layer is orchestrated by:
cellucid/assets/js/app/ui/core/ui-coordinator.js(initUI)
initUI is intentionally thin:
collects DOM references once (
collectDOMReferences)initializes feature modules (
initXfunctions)wires cross-module callbacks (e.g. “visibility changed → refresh legend + badges”)
subscribes to a small set of
DataStateevents
This separation exists to prevent:
a single monolithic UI file that becomes unmaintainable
accidental performance regressions from repeated DOM queries
DOM cache (single source of truth for selectors)#
Instead of every module calling document.getElementById(...) repeatedly, Cellucid uses:
cellucid/assets/js/app/ui/core/dom-cache.js
collectDOMReferences(document) returns a nested object keyed by UI domains:
dom.datasetdom.fieldSelectordom.filterdom.highlightdom.renderdom.cameradom.viewdom.sessiondom.communityAnnotation…
Design rule:
When adding new UI elements, prefer adding them to the DOM cache rather than scattering selectors across modules.
Module index (what exists today)#
Most UI modules live under:
cellucid/assets/js/app/ui/modules/
Most — not all — are imported and initialized by:
cellucid/assets/js/app/ui/core/ui-coordinator.js
Important
“Lives under ui/modules/” and “is initialized by the coordinator” are two
different facts, and four of the modules below are initialized by a sibling
module rather than by the coordinator:
dataset-connections.js← initialized bydataset-controls.jsdataset-drop.js← initialized bydataset-connections.jsfield-selector-gene-expression.js← initialized byfield-selector.jsfield-selector-deleted-fields.js← initialized byfield-selector.js
Two more are not initialized by the coordinator at all:
sidebar-controls.js and select-bar-placement.js are wired by
ui/core/sidebar-layout.js, which main.js calls right after the viewer is
created. The sidebar toggle and, on a phone, the panel’s peek bar
(#phone-sheet-bar: the dataset’s name and a handle; a tap or an upward swipe
opens the panel) are painted with the first frame, so they work before the
first dataset has loaded, which is when the coordinator runs. A phone starts
with the bar: ui/core/viewport-init.js marks the root before the stylesheet
applies (data-phone-sheet="peek"), unless this tab last left the panel open
(cellucid:phone-sheet in session storage, ui/core/phone-sheet-memory.js).
If you are tracing why a control is inert, check which of these paths owns it before assuming the coordinator does.
The ui/ layer also has directories this page does not otherwise cover:
ui/components/ (shared widgets — the coordinator initializes
components/info-popovers.js directly), ui/category-builder/,
ui/onboarding/ (welcome modal and the global keyboard shortcuts), and
ui/keyboard-move.js.
Core “shell” modules#
Module |
File |
Owns |
|---|---|---|
Sidebar toggle/resize |
|
sidebar visibility ( |
Selection bar placement |
|
centres |
Stats display |
|
top stats line (dataset/field/cell counts) |
Cell info |
|
the card describing the cell under the pointer (Show cell info) |
Cite dialog |
|
the footer’s Cite Cellucid dialog, read from the deployed |
Place names |
|
the names of the panels and blocks a message sends someone to ( |
Dataset + session modules#
Module |
File |
Owns |
|---|---|---|
Dataset controls |
|
dataset dropdown (the samples, or a connected catalog’s datasets as “Dataset from |
Dataset box |
|
the Dataset box ( |
Open… menu |
|
the Open… button’s two-item menu (File…, Folder…) and its keyboard handling |
Address routing |
|
what the Link or server bar opens: a |
Dataset connections |
|
local file and folder pickers, the Link or server bar (server and link connectors), the Server / Link connection lines with Disconnect, the sign-in step |
Dataset drop |
|
a file or folder dropped anywhere on the page, opened as the matching picker would (initialized by |
Share dialog |
|
the Dataset box’s Share: link, badge and embed snippet for a dataset a URL can reopen |
Session controls |
|
Loading a |
My datasets |
|
Session › Open data › My datasets: the five most recent datasets opened in this browser ( |
Session sections |
|
the Session panel’s collapsible sections, Subsampling ( |
Cells to open |
|
Session › Subsampling › Cells to open: the share slider, method and seed, Apply, one status line and its Details table |
Genes read for |
|
Session › Subsampling › Genes read for: the slider of the share of the open cells a gene is read for (steps: the share now, then 1, 5, 10, 25, 50, 100 % up to what the gene index fits, |
Stored line |
|
Session › Stored: what this browser keeps, one line beside Manage… ( |
Data mapping controls |
|
Session › Data mapping (its summary through |
State panel#
ui/modules/state/ (its README.md is the full map). Header
History (i) 🔍 ↶ ↷ ⚙ ⋯, a status line, a virtualised branch graph; the
session’s Share… and Load… sit above it.
Module |
File |
Owns |
|---|---|---|
State section |
|
The history of the dataset shown, drawn from the page’s current history ( |
The seam to the history |
|
|
Branch graph |
|
Rows, lanes and segments (path, fork stubs, expanded branches, turns, groups, folded runs, the paused marker); only the rows in view are in the DOM. Click selects, Shift+click extends, double-click (or a double tap) shows, |
Details, notes, search |
|
A step’s, branch’s, turn’s, group’s or run’s details and actions (Run again only on a stale step of an action marked |
Branches |
|
The branch picker (trackers, branches, archived, New branch); the branch map (every branch as a lane graph and a table; archive or delete several); two branches side by side through the views’ own actions ( |
Share and Load |
|
Share this history (History / Current state only / Steps only, and what goes in; |
Recently deleted, banners, replays |
|
Every undoable edit of the history with Restore and Empty; the line above the graph for a loaded, shared-state or replayed tracker; a replay’s report |
Compare two steps |
|
What differs between two steps ( |
Tour, copy, report |
|
The guided tour (each step shown by |
Undo keys, menus, words |
|
Ctrl/⌘+Z and Ctrl/⌘+Shift+Z (left to text fields, dialogs, menus and a selection in progress); menus at a row or the pointer; the words of rows and the status line ( |
The panel keeps no history of its own: it reads the current history and
redraws when it tells of a change, at most once a frame and only while it can
be seen; it calls no history mutator, and its controls are not session
controls (data-state-serializer-skip on the section). See
Action layer, state domains and history.
Agents#
The agent dock at the foot of the plot (the activity lines, the confirm
cards, the chat bar) and the Agents panel are drawn by app/agent/, not by
ui/modules/: app/agent/index.js (initAgents, from the coordinator),
app/agent/presence/ (cards, lines, the permission settings dialog),
app/agent/chat/ (the chat bar), app/agent/connect/ (Agents › Claude and
ChatGPT: the connector’s status line, address, approval dialog and
Manage…). ui/modules/dataset-open-action.js provides datasets.list and
dataset.open for agents and hosts over the Open data controls. See
Agents in the tab: chat bar, permissions, data guard and relay.
Actions installed by the page#
These modules own no DOM of their own; they provide the actions of the controls they stand behind and record what a person changes by hand:
Module |
File |
Provides |
|---|---|---|
Settings actions |
|
|
Layout actions |
|
The |
Field actions |
|
The state’s own actions: fields, categories, colours, filters, cells hidden by hand, gene sets, the data mapping |
Field/filter/highlight modules#
Module |
File |
Owns |
|---|---|---|
Field selector |
|
categorical/continuous/gene selectors + copy/rename/delete |
Gene expression helper |
|
gene dropdown/search behavior |
Deleted fields panel |
|
restore/purge UI |
Filter controls |
|
filter stack UI + count |
Legend renderer |
|
legend DOM + outlier UI container; a continuous field’s Gray for zero (genes with no negative value), Log color scale and Rescale to range switches, a gene sample’s one-line layer note and its link to Genes read for ( |
Outlier filter control |
|
the legend’s Outlier filter: Choose latent space without a latent space, else the slider with at most one line, progress and Cancel |
Highlight controls |
|
highlight pages + groups + selection mode tools |
View/render/camera modules#
Module |
File |
Owns |
|---|---|---|
View controls |
|
multiview (“Keep view”), layout mode, Link cameras, one row per view (what it shows, field, navigation mode, linked), the Navigation “Applies to” row |
Representation controls |
|
the |
Render controls |
|
points vs smoke, smoke sliders (scales in |
Velocity overlay controls |
|
vector field overlay settings + field selection |
Camera controls |
|
the Navigation accordion: mode select of the focused view, pointer capture, projectiles |
Navigation settings |
|
per-mode settings boxes, speed sliders and option checkboxes |
“Large feature” modules#
These are bigger subsystems but still treated as modules:
Module |
File |
Owns |
|---|---|---|
Figure export |
|
export UI + engine + SVG/PNG renderers |
Community annotation |
|
auth + pull/publish + voting/moderation UI |
Community annotation (split parts) |
|
the controls module’s own submodules (browser integration, connection flow, consensus column, modal shell, auto-pull storage) |
Community annotation dialogs |
|
the voting dialog and its ownership/lifecycle |
Cinematic camera |
|
camera-path authoring, transport bar, playback, and the |
Benchmark |
|
synthetic data generation contract + worker, the configuration-matrix harness, GL upload counters, frame recorder, camera path. The panel’s wiring lives in |
Highlight subsystem |
|
the four selection tools, their copy, and the highlight-page UI |
Legend subsystem |
|
categorical and continuous legend rendering |
Visualization reset |
|
reset actions (clear filters/highlights/etc.), and the global |
Field interaction owner |
|
arbitrates which module owns a field interaction |
Camera input contract |
|
the validated shape of camera control input |
Note
ui/modules/cross-highlight/ exists but contains only a .gitkeep. There is no
cross-highlight UI module today — cross-view highlight synchronisation is
handled in the state layer (app/state/managers/highlight-manager.js) and in
ui/modules/highlight/. A reader who trusts a complete module map would go
looking for a module that is not there.
Note
Analysis (plots / DE / correlation / etc.) is initialized directly in cellucid/assets/js/app/main.js (not via ui-coordinator.js).
Its UI lives in cellucid/assets/js/app/analysis/ui/.
See Analysis architecture.
How modules communicate (actions + state events + callbacks)#
Most modules follow this pattern:
Inputs (DOM events) dispatch the action the control stands for (
dispatchFromControl,app/actions/control-dispatch.js); the action’s implementation calls theDataState(or viewer) methods. A control calls no mutating state method itself, so every change is one history step, undone the same way whoever made it (Action layer, state domains and history). Settings controls and camera gestures change their owner first and are recorded after (recordObserved).DataStateemits events (field:changed,visibility:changed, etc.).The UI coordinator reacts to those events and triggers targeted UI updates (legend/stats/badges).
Why this matters:
It prevents “UI module A reaches into UI module B’s internal state”.
It makes it easier to reason about what needs to update when.
Example: visibility changes
Filtering module dispatches a
filter.*action → its handler updates the filter → state recomputes visibility → state emitsvisibility:changed.UI coordinator debounces a visibility UI update and triggers:
filterControls render
highlight summary render
view badge render
The control inventory (which UI state is a setting)#
Every input and select with a stable DOM id and every accordion section of
the sidebar and its floating panels is in the control inventory
(app/state-serializer/ui-controls.js, read from the markup), and
controlDomainOf is the one map that says which state domain holds each:
the accordion sections and the theme are the layout domain’s, the centroid
toggles the views domain’s, every other control is a setting (the settings
domain), set by setting.set and recorded by observation. A control added to
the markup is a setting the history records, a session saves and an agent can
list and set, with no code. A pressed-button group
(data-state-serializer-pressed-group, the selection tool) is not in it: it
is page chrome, kept by a save and the autosave as the session’s chrome and
put back after a load, never a step (capturePageChrome / applyPageChrome).
Some UI subtrees are intentionally excluded with
data-state-serializer-skip="true": what they show is held by another domain,
or is not state at all.
A second attribute, data-state-serializer-after-embedding="true", marks a
subtree whose controls stay in the inventory but are restored only after the
embeddings, kept views and active view are back, because their choices follow
the embedding the active view shows (the vector-field block: a field is bound
to one embedding). See cellucid/assets/js/app/state-serializer/README.md.
Examples:
The Figure export section is excluded from the inventory; its settings are one record of the
settingsdomain (figureExport,exportSessionSettings()/importSessionSettings()of the panel).The docked analysis panels and the floating analysis windows are excluded; they are the
analysisdomain.The Camera path is excluded; it is the
camera-pathdomain.The State section and My datasets are excluded (they show the history and the library, which are not state of the dataset).
#hp-antialiasis never captured (a device capability); the theme is.Benchmark section is excluded.
Dataset connection controls are excluded (sessions assume the dataset is already loaded).
Community annotation UI state is excluded (auth/network-driven).
Code pointers:
cellucid/assets/js/app/state-serializer/ui-controls.jscellucid/assets/js/app/state-serializer/README.mdcellucid/assets/js/app/state/domains/settings.js,layout.js
Troubleshooting (UI module boundary issues)#
Symptom: “One click triggers multiple UI updates”#
Likely causes:
Two modules subscribed to the same
DataStateevent and both re-render the same DOM region.A module is initialized twice (usually due to calling
initUItwice on reload paths).
How to confirm:
Set a breakpoint in the module init function and reload.
Log subscriptions by temporarily instrumenting
EventEmitter.on.
Fix:
Keep DOM ownership crisp: one module “owns” a DOM subtree.
Prefer coordinator wiring over cross-module subscriptions.
Next: Data loading pipeline and caching (data sources) or Extension points overview (how to add modules safely).