Screenshot a live view (@jbrowse/capture)
Drive a live JBrowse 2 instance with Puppeteer and screenshot it once it has actually finished rendering.
npx @jbrowse/capture --hub hg38 --loc BRCA1 \
--track hg38-ncbiRefSeqCurated --track hg38-phyloP100way -o brca1.png--hub names an assembly on genomes.jbrowse.org,
which hosts a ready-made JBrowse config per UCSC and GenArk genome, and --loc
takes a gene name because those configs ship a text index. --track takes a
trackId, the id without its assembly prefix (clinvarMain) or the track's
display name; capture checks each one and the assembly against the config before
launching a browser, and a miss fails at once with suggestions.
Knowing when JBrowse has finished rendering
Knowing when a genome browser is done is the entire problem. JBrowse loads its config, builds a session, resolves an assembly, fetches each track, and then draws to a canvas — and a screenshot taken at any point before the last step is a picture of an empty browser that looks like a successful run.
Nearly every signal a browser publishes is negative — no loading overlay, no display in its loading phase, no unpainted canvas — and an absence is equally true of a page whose JavaScript has not started. Measured against a released build with two remote tracks:
networkidle2resolves at ~350ms- the session appears at ~880ms
- the assembly and tracks land at ~2500ms
- the loading overlay only goes up after that
A wait chain built from those signals alone finishes in under a second and reports success.
So this package waits on the two positive signals instead, and nothing else decides the answer:
- The census.
AppReadyMarkerpublishes what is open asdata-app-views,data-app-assembliesanddata-app-tracks, and the gate reads that one element: at least one view, your assembly among them, every trackId you named actually open. A trackId or assembly that capture could not check against the config before launching (a session spec's, or a config it could not fetch) fails here if it never opens. A config, session or plugin the app cannot load fails here at once, with the app's own message, which it publishes asdata-app-error. [data-app-phase="ready"], held. The session renders it when no view is resolving an assembly and no display is fetching. It has to hold for a beat, not merely be true once: a display drops toreadyin the gap between one fetch finishing and the debounced next one starting. The hold also waits out a view body still loading its code and a display drawing a morph between two layouts (data-display-animating="true"), neither of which the marker can see.
Then one check on what the frame shows, which the marker does not answer. The
marker is about WORK, so it reads ready over a display whose fetch failed or
that the user canceled. Capture names every display that is unpainted, canceled
or showing an error banner, and fails. It fails too on an error toast or error
box, which no display carries: a track whose config fails to load never becomes
a display, so the frame lacks it while every display present has painted.
The instance has to be JBrowse v5 or later, the first release to publish the
marker. Capture reads the instance's version.txt before launching a browser
and refuses an older build there, rather than waiting out a timeout on a page
that will never say it has finished.
Library
import {
captureJBrowse,
launchBrowser,
openJBrowse,
waitForJBrowseReady,
} from '@jbrowse/capture'
// one call: launch, wait, shoot, close
const { pending, unsettled } = await captureJBrowse({
hub: 'hg38',
loc: 'BRCA1',
tracks: ['hg38-ncbiRefSeqCurated'],
out: 'brca1.png',
})
// or keep the page, to click things, read state back, or shoot one element
const { browser, page } = await openJBrowse({ hub: 'mm39', loc: 'Sox2' })
const tracks = await page.evaluate(
() => window.JBrowseSession.views[0].tracks.length,
)
const view = await page.$('[data-testid^="view-container-"]')
await view.screenshot({ path: 'view-only.png' })
await browser.close()
// or a page of your own that embeds a JBrowse component
const own = await launchBrowser()
const ownPage = await own.newPage()
await ownPage.goto('http://localhost:8000/my-widget.html')
await waitForJBrowseReady(ownPage)
await ownPage.screenshot({ path: 'embedded.png' })
await own.close()The CLI covers the common capture and has no flag for the rest. A crop, a click
before the shot or a hidden element is a line of Puppeteer on the page
openJBrowse returns, and a view or display setting goes in a --spec.
Two waits, depending on what you did:
waitForJBrowseReady(page)is the wait on its own, for a page you navigated yourself.waitForFrame(page)is its second half alone, for a page whose session you already gated, after a resize or a click.waitForSession, the first half, is exported too.waitForAppSettled(page)is the wait after you CLICK something. A page that is loading starts outloadingand the transition intoreadyis it finishing; a page you just clicked is alreadyreadyand stays that way until the click's work registers, so waiting forreadythere returns on the pre-click frame.waitForAppSettledrequires it to hold, and throws on a build with no marker rather than falling back to a wait that cannot fail.
openJBrowse opens a JBrowse Web instance. A page of your own that embeds a
JBrowse component, such as an htmlwidget or a notebook widget, publishes the
same marker: launchBrowser, navigate, then waitForJBrowseReady(page), as the
last example above does.
Timeouts and unsettled waits
Puppeteer waits are usually written .catch(() => {}) so a slow page is not
failed for being slow. The cost is that "everything settled" and "we gave up"
become the same void — the run ends with an image and an exit code of 0 either
way, which is the vacuous-gate problem again, one step later.
Here every stage reports its outcome, and an unsettled one throws by default, naming the gate:
gave up waiting after 60000ms: the app never held itself ready; display(s)
never painted: LinearAlignmentsDisplay (volvox_cram-LinearAlignmentsDisplay) is
loading. Raise the timeout, or pass allowUnsettled (--allowUnsettled) to capture
the frame as it stands.allowUnsettled (--allowUnsettled) takes the frame as it stands instead, and
still tells you what did not settle. It does not cover the session gate: a
trackId or assembly that never opens throws either way, since that frame lacks
the thing you asked to capture.
Reading the result
Three fields on a successful capture:
unsettled— stages that hit their timeout. Empty unless you asked to proceed anyway.pending— displays not showing their data when the shutter fired, each with its own phase:loadingis a slow fetch,errorandrenderErrora banner,canceleda user's stop, andreadya display claiming it finished without drawing. Each one lands inunsettledtoo, so a capture returns them only underallowUnsettled.tooLarge— displays showing "Requested too much data" instead of their features, because the region is wider than the track fetches without being asked. The capture still succeeds, since that is the app working as designed, and the CLI prints a warning naming them. Narrow the location to draw them.
Callouts
--annotations draws arrows, boxes, labels, numbered badges, legends and
lineage trapezoids over the view once it has settled, from a JSON array (inline,
a path, or - for stdin). These are the callouts on the JBrowse documentation's
figures, drawn by the same code, so a figure's Open or rebuild this view
dialog hands you its callouts as a file to pass here.
cat > callouts.json <<'JSON'
[
{ "type": "box", "anchor": { "loc": "chr17:43,044,295-43,125,483" } },
{
"type": "text",
"text": "BRCA1",
"leader": true,
"anchor": { "trackId": "hg38-ncbiRefSeqCurated", "loc": "chr17:43,044,295", "fracY": 0.5 },
"dx": -60,
"dy": -30
}
]
JSON
npx @jbrowse/capture --hub hg38 --loc BRCA1 --track hg38-ncbiRefSeqCurated \
--annotations callouts.json -o brca1.pngEach callout places itself through an anchor that resolves against the running
session, so none of them is a pixel measured off an earlier capture:
loc, with an optionaltrackIdandfracY(0 is the track's top, 1 its bottom), lands on a genomic position in a linear genome view. Aviewindex picks the view, and an array ([0, 1]) descends into a synteny view's rows.graphNodenames a GFA segment in a graph view or graph track.hLocandvLocname a dotplot's cell, as a whole chromosome (chr4) or a span (chr4:1-5,000,000).selectorandtextname an element of the page, such as a menu item.
dx and dy nudge a callout off its anchor, and fromAnchor anchors an
arrow's tail. The full vocabulary is Annotation in annotationOverlay.ts,
exported as types from this package. An anchor that resolves to nothing, or a
callout that lands outside the image, fails the run rather than writing a
picture with the callout missing or parked in a corner.
In a script, pass the same array as annotations to captureJBrowse, or call
drawAnnotations(page, annotations) on the page openJBrowse returns.
Many screenshots from one browser
jb2capture batch takes a JSON array with one object per image and draws them
all from one browser. Each image gets a window of its own, closed after its
screenshot, so no session state passes from one image to the next.
jb2capture batch captures.json --hub hg38 --track hg38-ncbiRefSeqCurated[
{ "loc": "BRCA1", "out": "brca1.png" },
{ "loc": "TP53", "out": "tp53.png", "tracks": ["hg38-clinvarMain"] }
]An object names its out and may set hub, config, assembly, loc,
tracks, spec, session, width, height, dpr, fullPage and
annotations. The command-line flags are the defaults for what an object leaves
out. --concurrency sets how many windows are open at once (default 4).
Each image is reported on a line of its own with the time it took. A failed
image is reported and the rest carry on, a browser that died is relaunched for
the images left, and the exit code is 1 if any image failed. Chrome flags and
--headed apply to the whole batch, so images that need different ones go in
separate batches.
In a script, captureBatch(captures, { concurrency }) takes an array of
captureJBrowse options and returns one result per capture, in order.
CLI
jb2capture --help for the full list. Also:
jb2capture list hg38 conservation # trackIds matching a filter
jb2capture url --hub hg38 --loc BRCA1 # just print the link, no browserSee also
- Docs: Using JBrowse with AI agents
- @jbrowse/img renders SVG/PNG with no browser at all, via server-side React. Prefer it for a static figure; use this when you need the real app — canvas and WebGPU rendering, dialogs, menus, or state read back out of a running session.