Single-CellSingle-Cell Omics

Build a Single-Cell Atlas in Your Browser — A 2026 Tutorial

End-to-end tutorial for analyzing single-cell data entirely in the browser using SciRouter APIs.

SciRouter Team
April 11, 2026
13 min read

A decade ago, building a single-cell atlas meant an HPC cluster, a pipeline of a dozen command-line tools, and weeks of curation. In 2026 you can do it in a browser tab. This tutorial walks through the full pipeline — from a raw counts file to an interactive scatter plot — using only the SciRouter Cell Atlas API and a handful of JavaScript libraries.

The endpoint does the heavy lifting. Your browser code handles the file parsing, the API calls, the 2D projection, and the plotting. Three API calls plus some glue code is all you need.

Note
This is meant as a reference architecture, not a production template. For a real app you would want proper auth, rate-limit handling, and loading states. The focus here is on the shape of the pipeline.

Architecture

Three API calls make up the pipeline, and each one does a distinct job:

  • POST /v1/singlecell/annotate — returns cell-type labels, confidence scores, and top marker genes per cell.
  • POST /v1/singlecell/embed — returns a 256- or 512-dimensional embedding per cell that you can feed into UMAP or PCA for 2D projection.
  • POST /v1/singlecell/marker-genes — returns a ranked marker gene list per cell-type label so you can build a legend or cluster annotation panel.

Everything else runs client-side: parsing the matrix, calling UMAP, drawing the scatter plot, handling hover and click events.

Step 1 — parsing the counts file

The simplest input format for a browser app is a gzipped CSV with rows as cells and columns as genes, plus a header row naming the genes. Something like Papa Parse plus pako for gzip works:

javascript
import Papa from "papaparse";
import pako from "pako";

async function loadCounts(file) {
  const compressed = new Uint8Array(await file.arrayBuffer());
  const csvText = new TextDecoder().decode(pako.ungzip(compressed));
  const parsed = Papa.parse(csvText, { header: true });
  const genes = parsed.meta.fields.slice(1); // first column is cell id
  const cells = parsed.data.map((row) => genes.map((g) => +row[g] || 0));
  return { genes, cells };
}

This yields a dense 2D array. For datasets larger than about 5,000 cells, build a CSR representation client-side before shipping to the API.

Step 2 — calling the annotation endpoint

The annotation endpoint returns cell-type labels, confidence scores, and top marker genes in a single response:

javascript
const API = "https://scirouter-gateway-production.up.railway.app/v1/singlecell";
const apiKey = "sk-sci-your-key";

async function annotate(genes, cells) {
  const res = await fetch(`${API}/annotate`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      model: "geneformer",
      genes,
      matrix: toCSR(cells),
      reference_atlas: "human-core-2026",
    }),
  });
  if (!res.ok) throw new Error(`annotate failed: ${res.status}`);
  return res.json();
}

The helper toCSR converts your 2D array to the indptr/indices/data representation described in the sparse-matrix tutorial on this blog.

Step 3 — extracting embeddings

The embedding endpoint returns a fixed-dimensional vector per cell. These vectors live in the foundation model's latent space and are what you feed to UMAP:

javascript
async function embed(genes, cells) {
  const res = await fetch(`${API}/embed`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      model: "geneformer",
      genes,
      matrix: toCSR(cells),
    }),
  });
  return (await res.json()).embeddings;
}

Step 4 — running UMAP in the browser

umap-js is a pure-JavaScript UMAP implementation that runs happily in the browser. It takes a couple of seconds for 10,000 cells:

javascript
import { UMAP } from "umap-js";

function runUmap(embeddings) {
  const umap = new UMAP({
    nComponents: 2,
    nNeighbors: 15,
    minDist: 0.1,
  });
  return umap.fit(embeddings);
}

The output is an array of [x, y] coordinates, one per cell. Feed that straight into your plotting library.

Step 5 — the scatter plot

DOM-based SVG plots choke above about 5,000 points. For a real atlas, use a WebGL-backed library. Here is the outline with regl-scatterplot:

javascript
import createScatterplot from "regl-scatterplot";

function drawAtlas(canvas, coords, labels) {
  const uniqueLabels = [...new Set(labels)];
  const colorMap = Object.fromEntries(
    uniqueLabels.map((l, i) => [l, hsl(i / uniqueLabels.length)])
  );
  const points = coords.map(([x, y], i) => [
    x, y, uniqueLabels.indexOf(labels[i])
  ]);
  const scatter = createScatterplot({ canvas, pointSize: 4 });
  scatter.draw(points);
}

Use a d3 color scale for the hsl helper and you have a serviceable atlas in under 200 lines of client code.

Step 6 — interactivity

The annotation response already contains everything you need for a click handler. Store the result array globally or in React state, and when a cell is clicked look up its entry:

javascript
scatter.subscribe("pointOver", ({ points }) => {
  const idx = points[0];
  const call = annotationResult.calls[idx];
  showTooltip({
    label: call.label,
    confidence: call.confidence,
    markers: call.markers.slice(0, 5),
  });
});

No additional API calls needed. The marker genes were fetched in step 2 and are already in memory.

Performance tips

  • Cap dataset size at 50k cells. Beyond that, browser memory and UMAP runtime both become painful. Cluster-level downsampling is a fine compromise.
  • Ship CSR, not dense. Sparse payloads are 10 to 100x smaller for typical scRNA-seq data.
  • Run UMAP in a web worker. Keeps the main thread responsive while the projection runs.
  • Cache embeddings in IndexedDB. Users re-opening the app should not pay for another API call unless the data changed.

Where to go from here

Once the basic atlas works, the interesting extensions are all straightforward:

  • Overlay gene expression on the scatter plot with a color ramp per gene.
  • Add a brush selection tool and run differential expression on the selected cells through a separate endpoint.
  • Compare two datasets side by side by embedding both with the same foundation model and plotting them in the same latent space.

All of these hang off the same three core endpoints. The Geneformer tool page documents the full request and response schemas if you want to go deeper.

Bottom line

A browser-based single-cell atlas used to be a research project. With a hosted foundation model API and a WebGL plotting library it is now a weekend project. The code above is the entire pipeline — parse, annotate, embed, project, plot. Everything else is polish.

Open the Cell Atlas workspace →

Frequently Asked Questions

Do I need a backend server for this?

No. The whole pipeline — annotation, embedding, marker extraction — runs through hosted HTTP endpoints. You can ship the entire thing as a static browser app that calls the API directly with an API key.

What file formats can I load in the browser?

The walkthrough uses gzipped CSV of counts plus a gene list, which is the simplest to parse in the browser. Loom and h5ad work too if you bring a WASM parser. Keep the dataset under 50,000 cells for a responsive browser experience.

Can I keep user data private?

Data leaves the browser only as part of the API call itself, over HTTPS. SciRouter does not persist the input matrix beyond the request lifetime. For stricter privacy, self-host the workers with the open-source gateway code.

What UMAP library should I use on the frontend?

umap-js is a pure-JavaScript UMAP implementation that runs in the browser. You pass it the embedding vectors returned by the API and it produces 2D coordinates for plotting.

How do I plot 50,000 cells without crashing the page?

Use a WebGL scatter plot library like deck.gl or regl-scatterplot. DOM-based SVG plots die above about 5,000 points. WebGL handles hundreds of thousands of cells smoothly.

Can I make the atlas interactive — clicking a cell to see markers?

Yes. The annotation endpoint returns top marker genes per cell, so a click handler can pop up a marker list instantly. No extra round trips needed.

Try this yourself

500 free credits. No credit card required.