Skip to main content

Plexora

About

This is an openseadragon based Cellular Image Viewing and Analysis Tool. It is built with a python Flask backend and a Node.js javascript frontend.

Images are read where they are, never converted or copied on import: OME-TIFF, TIFF, SVS and QPTIFF through the multichannel viewer, PNG and JPEG as a flat view-only image, and OME-Zarr / NGFF — either a standalone .ome.zarr store or an image inside a SpatialData .zarr, which can be the same store the cell table comes from. Point the Image field at the store and Plexora finds the image group inside it; a store that arrives with too few resolution levels to zoom out of gets the missing coarse ones derived once, into the project's own directory.

Install (for Users)

pip install plexora
plexora

That is the whole setup. plexora starts the server, prints the URL, and opens a browser when the environment looks interactive. On the first run it also prints where it will keep your projects.

plexora my_dataset        # open a project straight away
plexora --port 9000       # a specific port (8000 is the default; if it is
                          # busy, Plexora moves to a free one and says so)
plexora --version
python -m plexora         # same thing, when the console script is not on PATH

There are four ways to run Plexora, all of which end in the same viewer:

Where you are What to run
Your own machine, a terminal plexora
Your own machine, a notebook plexora.view("my_dataset")
A remote machine you can ssh into Settings → Remote servers → Connect — or plexora connect user@host
A hosted notebook or an HPC terminal plexora — it works out where it is running and prints a URL that works

Save a remote server once and reconnecting is a button: Plexora handles the SSH connection, the ports, the tunnel and the URL, and relays a password or 2FA prompt to the page if the server asks for one. It never stores the password.

docs/SETUP_GUIDE.md is the step-by-step version for people who would rather not learn about tunnels — organised by where your data is, with a compatibility matrix and a screenshot for every screen. DEPLOYMENT.md is the same ground for a technical reader, from a fresh conda environment to HPC job submission, with the real output of each command. The rest of this README is the short version.

Where your data lives

Plexora keeps projects, figures and settings in one directory, chosen in this order:

Rule Location
--data-dir / PLEXORA_DATA_PATH whatever you pass, for that one command
a recorded setting plexora config set data-dir <path>
a directory suggested by a connection the saved profile that launched the session, used only if nothing above answered
default %LOCALAPPDATA%\plexora, ~/Library/Application Support/plexora, or ~/.local/share/plexora

It never depends on the directory you started from, and never lives inside the installed package.

plexora where                          # which directory, and which rule chose it
plexora config set data-dir /scratch/me/plexora

Moving it matters on HPC and on Windows machines with a small system drive: derived image pyramids are large, and the default sits on your home or system volume. plexora config set data-dir records the choice permanently; the environment variable overrides it for one run.

Datasets: grouping projects that belong together

One image is one project. A dataset is the folder above that -- a patient cohort, a TMA series, one imaging run -- and it is nothing more than a grouping: it holds project names, not data. Deleting a dataset releases the projects in it and touches no file of theirs, and taking a project out of one is not deleting the project.

On the Open Project page, datasets appear as folders. Open one to see what is in it, drag cards onto one to move them, and drag them onto All projects to take them out again. Select several with the tick in a card's corner, or with ctrl-click and shift-click, and move them together. Searching looks everywhere and tells you which dataset each result lives in.

The same thing from Python or the terminal:

import plexora

plexora.create_dataset("Melanoma Cohort", images=[
    "sample1.ome.tif", "sample2.ome.tif", "sample3.ome.tif",
])
plexora dataset create melanoma_cohort --images sample1.tif sample2.tif
plexora dataset list
plexora dataset show melanoma_cohort

Register first, configure as you go

An image is the only thing a project must have. Everything else -- a segmentation mask, a feature table, which table inside a .zarr store, which column holds the cell id -- is optional at registration, and Plexora asks for what it needs at the moment something needs it rather than up front:

plexora.create_project("slide.ome.tif")                       # complete already
plexora.create_project("slide.ome.tif", data="store.zarr")    # table undecided

The second is a valid project: it opens, it shows the image, and the first tool that wants a table asks which one to load, with the path already filled in. Answer once and nothing asks again.

Anything you do know can be said at registration, and is then recorded as an answer rather than a guess -- so nothing asks you to confirm it later:

plexora.create_dataset("Melanoma Cohort", projects=[
    {"image": "sample1.ome.tif", "segmentation": "sample1_mask.tif",
     "data": "sample1.csv", "cell_id": "CellID",
     "x": "X_centroid", "y": "Y_centroid"},
    {"image": "sample2.ome.tif"},
])

plexora.configure_project("sample2", data="sample2.csv", cell_id="CellID")
plexora.project_manifest("sample2")   # what it has, and what is still open

Files do not all have to be on the same machine. node names a data node you are connected to, and any field can say otherwise for itself -- so a cohort whose slides sit on a cluster and whose quantification sits on your laptop is one document:

plexora.create_dataset("Melanoma Cohort", node="hms-o2", projects=[
    {"image": "/n/scratch/reg/sample1.ome.tif",
     "segmentation": "/n/scratch/seg/sample1.tif",
     "data": {"path": "C:/quant/sample1.csv", "node": None}},
    {"image": "/n/scratch/reg/sample2.ome.tif"},
])

The projects are registered here, so they appear in the Samples page like any other; the bytes stay where they are and are read through the node.

The same options exist as flags:

plexora project create slide.ome.tif --data cells.csv --cell-id CellID \
    --markers CD3 CD8 --dataset melanoma_cohort
plexora project show slide            # every question, and whether it is answered

plexora dataset create "Melanoma Cohort" --from projects.json --node hms-o2

Shared projects

Several people on one workstation or login node can share a directory of common datasets while keeping their own work private:

export PLEXORA_SHARED_PATH=/srv/plexora/common   # or: plexora config set shared-dirs ...
plexora

Shared projects appear in Open Project marked Shared. They can be opened and explored but not edited or deleted, and everything you produce while exploring one — gates, ROIs, figures, cached results — is written to your data directory, not the shared one. A project of your own with the same name takes precedence over the shared copy.

Plexora has no user accounts. For a multi-user deployment, run one process per user behind a reverse proxy that maps the authenticated user to their own --data-dir, and keep the server bound to loopback (the default). The one place Plexora authenticates at all is where it cannot use loopback — the Open OnDemand routes, which mint a per-server token — and that protects a single-user server rather than telling several users apart.

Running on a remote machine over SSH

Plexora binds to loopback and has no authentication, so the way to reach one running on a server is an SSH tunnel rather than an open port. There are two ways to get one, and they do the same thing.

Have Plexora set it up. Run this on your own computer:

plexora connect user@server.lab.edu                 # starts, tunnels, opens a browser
plexora connect user@server.lab.edu my_dataset      # …straight into a project

It stays in the foreground; Ctrl+C closes the tunnel and stops the remote server. It uses your system ssh, so whatever ~/.ssh/config, an agent, a ProxyJump or a hardware token already do for ssh user@host happens here too.

If the remote plexora is not on a non-interactive PATH — which is common with conda — name it explicitly:

plexora connect user@server.lab.edu --remote-command /home/you/miniconda3/envs/imaging

The environment path is enough — bin/plexora is filled in for you.

Or do it by hand. On the remote machine:

plexora --remote

It prints the exact ssh -N -L … command to paste into a terminal on your own computer, and the http://localhost:<port>/ address to open afterwards.

HPC clusters with compute nodes

On a cluster you usually may not run anything heavy on the login node, so Plexora belongs in a job. --srun submits one and tunnels to whichever node the scheduler grants — the target is the login node:

plexora connect user@o2.hms.harvard.edu --srun "-p interactive -t 4:00:00 -c 16 --mem 128G"

Allocation may queue; it says so while it waits. Ctrl+C ends the job.

By hand, the same thing is two steps: start an interactive job (srun --pty -p interactive -t 4:00:00 -c 16 --mem 128G bash), then run plexora --remote inside it. It detects the job, works out which compute node it is on and which login node you came through, and prints the two-hop ssh -J command for it.

Some sites refuse SSH into a compute node. Add --bind-node at either end for a login-node forward instead — note that this makes the port reachable from the cluster's internal network while it runs.

A lab's shared reference data pairs naturally with this: point PLEXORA_SHARED_PATH (or plexora config set shared-dirs) at a read-only directory on the cluster filesystem, and --data-dir or plexora config set data-dir at your own scratch space.

Running as a Docker container

Note: When running on an ARM machine (e.g. M1 Macbook), build the image with docker build --platform linux/amd64 -t plexora .

  • Build image: docker build -t plexora .
  • Run image with mounted path: docker run --rm -dp 8000:8000 -v [source path]:/[target path] plexora

where

  • --rm cleans up the container after it finishes executing
  • -v mounts the "present working directory" (containing your data) to be /data inside the container. This is necessary in order to import your data via the import page.
  • -dp forwards the port 8000

Once the container is running, go to http://localhost:8000/ in your web browser. To import your imaging files in the import gui type in the mounted /data/..

Inside the image, projects are written to /app/data (PLEXORA_DATA_PATH), so mount a volume there to keep them between runs:

docker run --rm -dp 8000:8000 -v ~/plexora-data:/app/data -v /my/images:/data plexora

The image also sets PLEXORA_HOST=0.0.0.0 — published ports would never reach it otherwise — and PLEXORA_DOCKER=1, which switches the import page to container-shaped path hints.

Data on more than one machine

Sometimes the image and the cell table are not on the same computer — the slide is on cluster scratch and the .h5ad came back to your laptop. Start a data node where the data is:

plexora node serve --serve image:tumor=/scratch/me/tumor.ome.tif

It prints a token. Register the node in the viewer under Settings → Data nodes, then point a project at it from that project's Edit page, under Where the data lives.

A node is a Plexora with the viewer switched off: it holds files and answers questions about them. Your projects, ROIs, gates and figures all stay on the machine you are looking at, so a node can restart or disappear without any of your work being at risk — the project still opens, and whatever came from that node is absent and says so.

See DEPLOYMENT.md for the tunnel recipes, what actually crosses the network, and the limits.

Clone and Run Codebase (for Developers)

git clone https://github.com/nirmallab/plexora.git
cd plexora
python -m venv .venv && source .venv/bin/activate   # or conda create -n plexora python=3.13
pip install -e ".[dev,jupyter]"

Any Python 3.12 or 3.13 environment works — conda, venv, uv, whatever you already use. The editable install pulls every runtime dependency plus the test and notebook extras; there is no separate environment file to keep in sync.

Then start the server with python run.py (or plexora) and open http://localhost:8000/.

Running in Jupyter notebooks

Install the package into the same environment as Jupyter:

pip install "plexora[jupyter]"

Then, in a cell:

import plexora

plexora.view("my_dataset")

That is the whole thing, in every kind of notebook. Plexora starts a small server beside your kernel and shows it in the cell.

data_dir is optional everywhere below: leaving it out uses the same directory plexora where reports, so a notebook and a terminal see the same projects. Pass it to work against a different one.

Hosted notebooks — JupyterHub, Open OnDemand, Colab

The same call. When your kernel is not on the machine with your browser, a 127.0.0.1 address would point at your own laptop, so Plexora detects the situation and builds the proxied URL your host actually serves it on.

On a JupyterHub server, this needs jupyter-server-proxy installed in the environment running the Jupyter server (not necessarily the one running your kernel):

pip install jupyter-server-proxy

Open OnDemand needs nothing installed. Plexora recognises the portal and mounts itself under /rnode/<node>/<port>, the door OnDemand provides for apps that serve at the root. That door is reached from the portal over the network, so the viewer binds 0.0.0.0 and protects itself with a token carried in the URL — it says so, once, when it starts. From a terminal in the same session, plexora --ood my_dataset does the same thing for the standalone app.

Colab needs nothing extra either. Neither does local Jupyter or VS Code Remote, which keep the direct localhost address they always used.

Override the detection if you need to:

plexora.view("my_dataset", proxy=True)     # always proxy
plexora.view("my_dataset", proxy=False)    # always use 127.0.0.1
plexora.view("my_dataset", base_url="/user/me/")   # name the prefix yourself

On a hub, viewer.url is a path rather than a full address — open it under your notebook server's own address, which is where your session is authenticated.

Datasets can also be registered directly from notebook-visible files:

from plexora.jupyter import PlexoraViewer

viewer = PlexoraViewer.from_files(
    name="my_dataset",
    image="/path/to/image.ome.tif",
    segmentation="/path/to/segmentation.ome.tif",
    features="/path/to/cells.csv",
    x="X_centroid",
    y="Y_centroid",
    id_column="CellID",
)
viewer

Looking at what is already in your kernel

plexora.view takes data as well as a name, and each piece may be a path or an object you already have:

import plexora, scanpy as sc

sc.tl.leiden(adata)

plexora.view(
    "my_dataset",
    image="/path/to/slide.ome.tif",   # a path, read from disk as always
    segmentation=mask,                # a numpy label array
    adata=adata,                      # served straight out of this kernel
    tool="cell_explorer",             # open with this panel showing
    overlay="leiden",                 # ...drawn by this column
    channels=["DAPI", "CD3"],         # ...over these channels
)

Nothing is written to disk. The kernel serves its own objects to the viewer over Plexora's data-node API, on a loopback port that only the viewer beside it can reach — so this works the same in local Jupyter, JupyterHub, Open OnDemand and Colab, with no extra port exposed anywhere.

Annotate again and show the result without rebuilding anything:

adata.obs["phenotype"] = classify(adata)
viewer.refresh(adata)

A refresh re-reads the table's shape, so a column that did not exist a cell ago is immediately available as an overlay. Anything you do not name is left alone — a whole-slide image is not re-read and the tiles your browser is holding stay valid.

tool, overlay and channels belong to that one viewer. They are carried in its URL and never overwrite the channels, colours or overlay the project has saved, so a notebook can open the same project a dozen ways without disturbing what you last set up by hand.

A few things worth knowing:

  • table= takes a pandas DataFrame wherever adata= takes an AnnData. sdata= takes a SpatialData object and pulls its image, labels and table apart for you (name them with sdata_image=/sdata_labels=/sdata_table= when the store holds more than one of a kind).
  • Very large images belong on disk. A path costs nothing here; an array is given the coarse pyramid levels a viewer needs, which is about a third of it again in memory. Plexora says so if the array is large.
  • A table snapshot is a copy of the elements the viewer reads — obs, var, and the one matrix you chose. For an imaging table that is tens of megabytes. to_disk=True restores the old behaviour of writing an .h5ad, which is the better trade when the project should outlive the kernel.
  • The other matrices are not copied, so a memory-served project does not offer them as a read spec to switch to. Reading a different layer means calling plexora.view again saying so.

Baseline smoke test

Before upgrading dependencies or changing the viewer/server boundary, run the local orion2 baseline:

python -m tests.baseline_orion2

The test checks Flask app import, /config, the viewer page, metadata JSON, channel metadata, and one image tile plus one segmentation tile. It skips with a clear message if the local orion2 datasource or exemplar files are not available.

(4. Node.js installation and packages)

This step is only needed when you plan to edit js code. The codebase already included bundled js files.

  • Install Node.js, then navigate to /plexora/client and run npm install to install all packages listed in package.json.
  • Run npm run start to package the Javascript, or run npm run watch if you plan on editing dependencies

Packaging/Bundling Code as Executable (for Developers)

Any tagged commit to a branch will trigger a build, where tag == commit message. This will appear under releases. Note building may take ~10 min.

Tagging Conventions: All release tags should look like v{version_number}_{branch_name}.

License

Plexora is released under the Plexora Academic License 1.0 (see LICENSE). It is not an open source license. In short:

Academic research, teaching, personal study ✅ Free
Use by a nonprofit or government research institution ✅ Free (whatever the funding source)
Redistributing Plexora unmodified, with the license attached ✅ Allowed
Patching your own copy to fix a bug or a compatibility problem ✅ Allowed
Publishing a fork, a patched build, or a renamed version ❌ Not allowed
Commercial use of any kind ❌ Requires a paid license

Plugins are a deliberate exception. Anything you build against the documented extension interfaces — the plexora.plugins entry point group and the plexora.api package — is yours. You may distribute and sell your plugin under whatever license you like, and you do not need our permission. Extending Plexora through the plugin API is the supported way to change what it does; editing its source is not.

For a commercial license, contact Ajit Johnson Nirmal ajitjohnson.n@gmail.com.

Some bundled components carry their own licenses, which are unaffected by the above — see section 8 of LICENSE.

Release files for plexora 0.0.23

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for plexora 0.0.23
File Size Uploaded
plexora-0.0.23.tar.gz 9.8 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for plexora 0.0.23
File Interpreter ABI Platform
plexora-0.0.23-py3-none-any.whl Python 3 none any Details

Total release size: 19.2 MB

Release files / plexora-0.0.23.tar.gz

Download URL plexora-0.0.23.tar.gz
Size 9.8 MB
Tags Source
SHA-256 checksum
How to use checksums
e0352ce3eb8300cd8a3127f96096539dca01d18a555309a68ba7aefbf1f98847
BLAKE2b-256 checksum
How to use checksums
3b924d31d1576b1a06c722130573903758cf7399e957c636343339315d44f213
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.13

Release files / plexora-0.0.23-py3-none-any.whl

Download URL plexora-0.0.23-py3-none-any.whl
Size 9.4 MB
Tags Python 3
SHA-256 checksum
How to use checksums
b233b40c39a0f1167280b850c81a3b8cf47bd85632705b98adf3f0a5ebf098a2
BLAKE2b-256 checksum
How to use checksums
6c93d012b0be231ec0608c404269fcdaca645680f7fc823ad41d463991e958bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.13

Release history Release notifications | RSS feed

This release

0.0.23 This release

2 release files

0.0.22

2 release files

0.0.21

2 release files

0.0.20

2 release files

0.0.19

2 release files

0.0.17

2 release files

0.0.16

2 release files

0.0.13

2 release files

0.0.12

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page