Recipes for driving JBrowse from an agent
- Each recipe is a
run_javascriptbody for JBrowse Desktop over MCP. - In JBrowse Web the same code runs in the page, where the value is the last
expression rather than a
return, so wrap a body in(async () => { ... })(). - Every snippet runs against the
volvoxtest config or the hosted hg38 config in the Desktop conformance suite, and the values quoted are what came back. - Inside the app the same page is
docs topic:"recipes", one section at a time.
Find the tracks you can use
jb.listTracks answers { total, tracks }, not an array. Each entry carries
trackId, name, type, adapterType and assemblyNames, so a config with
several assemblies is filtered in code:
const { total, tracks } = jb.listTracks('vcf')
return {
total,
onVolvox: tracks
.filter(t => t.assemblyNames.includes('volvox'))
.map(t => `${t.trackId} (${t.type} / ${t.adapterType})`),
}
The search matches trackId and name, case-insensitively. Connection and hub
tracks are included, which is why this and not session.tracks is the catalog.
Open a hosted genome at a gene
- With nothing open, Desktop's
opentool takes the hosted config URL directly,https://jbrowse.org/ucsc/hg38/config.json; JBrowse Web takes it as?config=. Hosted genomes and tracks for agents has the URL for every UCSC database and GenArk accession. - Then a spec builds the view. A
locthat is a gene name goes through the config's text index. - Because the spec names its tracks, the track the hit was found in is not added on top of them.
return jb.loadSessionSpec({
views: [
{
type: 'LinearGenomeView',
assembly: 'hg38',
loc: 'BRCA1',
tracks: ['hg38-ncbiRefSeqCurated', 'hg38-clinvarMain'],
},
],
})
The result is the settle report plus a session summary naming the view, its visible region and each track's display and height. Here the report also says that ClinVar is over its fetch-size gate at a whole-gene zoom:
{
"settled": true,
"notReady": [
{
"trackId": "hg38-clinvarMain",
"display": "LinearBasicDisplay",
"height": 100,
"phase": "tooLarge",
"reason": "Requested too much data (8.52 Mb)"
}
],
"session": {
"views": [{ "visibleRegion": "chr17:43,019,038..43,195,484", "...": "..." }]
}
}
That track draws once the view is zoomed in, and a read over the same span is refused the same way, which is what the next recipe narrows.
Tabulate what is on screen
Features come back as live objects, so count in code and return the counts. Over
the default region of volvox:
const count = (feats, key) => {
const out = {}
for (const f of feats) {
const k = String(f.get(key) ?? 'none')
out[k] = (out[k] ?? 0) + 1
}
return out
}
const variants = await jb.getFeatures({ trackId: 'volvox_test_vcf' })
const genes = await jb.getFeatures({ trackId: 'gff3tabix_genes' })
return {
variants: count(variants, 'type'),
geneFeatures: count(genes, 'type'),
strands: count(genes, 'strand'),
}
{
"variants": {
"SNV": 106,
"deletion": 1,
"insertion": 1,
"sequence_variant": 1
},
"geneFeatures": { "gene": 2, "mRNA": 2, "CDS": 1, "match": 76, "...": "..." },
"strands": { "1": 79, "-1": 31, "0": 4 }
}
f.get('start')is zero-based andf.get('end')exclusive.- A VCF feature also answers
REF,ALT(an array),QUAL,FILTER,INFOandsamples; a GFF feature its column-nine attributes by name. Object.keys(f.toJSON())lists what one feature has, and is worth a look before filtering onname: the hosted RefSeq GFF names a gene byIDandgene_idwith noName, sonameisnullon every gene there andidis the symbol.
The same shape over ClinVar on the hosted hg38 config, where the field is the
clinical significance. The whole BRCA1 view is over the read's byte gate, so
loc narrows it to the gene's last exons:
const feats = await jb.getFeatures({
trackId: 'hg38-clinvarMain',
loc: 'chr17:43,044,000-43,060,000',
})
const by = {}
for (const f of feats) {
const k = f.get('clinSign') ?? 'unstated'
by[k] = (by[k] ?? 0) + 1
}
return { n: feats.length, by }
Without loc the call throws region too large for jb.getFeatures naming the
estimate and the limit, rather than returning a short answer that looks whole.
Pass byteLimit alongside trackId for a read you mean to be that big.
Which genes carry a variant
Two reads over the same region and a join in code. Overlap is the half-open
interval test, and the gene track is filtered to its top-level gene features
so exons are not counted again:
const genes = await jb.getFeatures({ trackId: 'gff3tabix_genes' })
const variants = await jb.getFeatures({ trackId: 'volvox_test_vcf' })
const overlaps = (a, b) =>
a.get('start') < b.get('end') && a.get('end') > b.get('start')
return genes
.filter(g => g.get('type') === 'gene')
.map(g => ({
gene: g.get('name') ?? g.get('id'),
variants: variants.filter(v => overlaps(v, g)).length,
}))
.filter(h => h.variants > 0)
.sort((a, b) => b.variants - a.variants)
Find the highest value in a quantitative track and go there
A bigWig answers one feature per interval with a score. Reduce with a loop
rather than Math.max(...scores), which overflows the call stack on a
base-resolution track, then navigate to what you found:
const view = jb.view()
const feats = await jb.getFeatures({
trackId: 'volvox_microarray',
loc: 'ctgA:1-50,000',
})
let best
for (const f of feats) {
if (!best || f.get('score') > best.get('score')) {
best = f
}
}
const peak = {
refName: best.get('refName'),
start: best.get('start'),
end: best.get('end'),
score: best.get('score'),
}
await view.navToLocString(
`${peak.refName}:${peak.start - 2000}-${peak.end + 2000}`,
)
const settle = await jb.waitReady(30000)
return { scanned: feats.length, peak, now: view.visibleLocStrings, ...settle }
{
"scanned": 500,
"peak": { "refName": "ctgA", "start": 24500, "end": 24600, "score": 899 },
"now": "ctgA:22,500..26,600",
"settled": true
}
Reads by strand and mapping quality
An alignment feature answers strand as 1 or -1, score as the mapping
quality, and flags, CIGAR, seq, qual, template_length and tags
besides:
const reads = await jb.getFeatures({ trackId: 'volvox_alignments' })
const byStrand = { forward: 0, reverse: 0 }
const mapq = {}
for (const r of reads) {
byStrand[r.get('strand') === -1 ? 'reverse' : 'forward'] += 1
const bin = Math.floor(r.get('score') / 10) * 10
mapq[bin] = (mapq[bin] ?? 0) + 1
}
return { n: reads.length, byStrand, mapq }
A track need not be shown to be read: jb.getFeatures takes any trackId in
the catalog. Reading a shown one shares the parsed index the display already
loaded.
Show a value you computed as a track
A FromConfigAdapter carries the features in the track config, so the derived
track saves and reopens with the session and needs no file. Variant density per
kilobase across the visible region:
const [region] = await jb.visibleRegions()
const BIN = 1000
const variants = await jb.getFeatures({
trackId: 'volvox_test_vcf',
regions: [region],
})
const first = Math.floor(region.start / BIN) * BIN
const counts = new Array(Math.ceil((region.end - first) / BIN)).fill(0)
for (const v of variants) {
const i = Math.floor((v.get('start') - first) / BIN)
if (i >= 0 && i < counts.length) {
counts[i] += 1
}
}
const trackId = `variant-density-${Date.now()}`
session.addSessionTrackConf({
type: 'QuantitativeTrack',
trackId,
name: 'variants per kb',
assemblyNames: [region.assemblyName],
adapter: {
type: 'FromConfigAdapter',
adapterId: trackId,
features: counts.map((score, i) => ({
uniqueId: `bin${i}`,
refName: region.refName,
start: first + i * BIN,
end: first + (i + 1) * BIN,
score,
})),
},
})
await jb.view().launchTrack(trackId)
return {
trackId,
bins: counts.length,
max: Math.max(...counts),
...(await jb.waitReady(30000)),
}
- A fresh
trackIdandadapterIdper computation is deliberate. Re-adding a knowntrackIdwith different content is refused, and the adapter cache is keyed onadapterId, so a recomputed track under the old ids keeps showing the first values it saw. - Plan for a few thousand features and no more; above that, write a real file
and load it with
jb.addTrack.
Restyle, and read back what landed
Settings keys come from the display's own schema. A key it does not declare is not an error, so read the report:
const track = jb.trackModel('gff3tabix_genes')
const slots = jb.describeSlots(track.activeDisplay.configuration)
const result = track.applyDisplaySettings({
displayMode: 'compact',
colour: 'red',
})
return {
knows: Object.keys(slots),
displayMode: slots.displayMode.description,
result,
}
{
"knows": [
"height",
"color",
"displayMode",
"heightMode",
"showLabels",
"..."
],
"result": {
"applied": ["displayMode"],
"unapplied": ["colour"],
"failed": []
}
}
unappliedis the misspelling;failedis a key the display knows and could not set.- Anything a slot does not cover is an action on the display itself, listed by
docs topic:"model:<modelType>" section:"Actions"with the type name fromjb.inspect('views.0.tracks.0.displays.0').modelType.
Reorder tracks with an action inspect found
MST attaches actions as non-enumerable properties, so Object.keys(view) shows
none of them and jb.inspect is how to see what a view can do. The move actions
take the track model's own id, not its trackId:
const view = jb.view()
const track = view.tracks.find(
t => t.configuration.trackId === 'volvox_test_vcf',
)
view.moveTrackToTop(track.id)
return {
moves: jb.inspect('views.0').actions.filter(a => a.startsWith('moveTrack')),
order: view.tracks.map(t => t.configuration.trackId),
}
Prove a track drew
A display that refuses to draw replaces its own subtree and raises no toast, so a screenshot of it looks fine. The settle report is where it shows:
await jb.view().launchTrack('volvox_bigwig_nonexist')
return jb.waitReady(20000)
{
"settled": true,
"notReady": [
{
"trackId": "volvox_bigwig_nonexist",
"display": "LinearWiggleDisplay",
"height": 100,
"phase": "error",
"error": "Error: ENOENT: no such file or directory, open '.../volvox.bw.nonexist'"
}
]
}
- A track over its fetch-size gate reports
phase: "tooLarge"with the reason the display painted. Zoom in, or raise the display'sfetchSizeLimitslot throughapplyDisplaySettingsif the size is meant. - For a track that settled clean, pair the empty
notReadywith a feature count over the visible region rather than looking for pixels: the canvases are offscreen and measure 0 by 0.
A figure per locus
The loop lives in the client, one navigate-and-settle call per locus and a
cropped screenshot after each. The settle call returns the view's element box so
rect or selector can crop to it:
const view = jb.view()
await view.navToLocString('ctgA:5,000-15,000')
const settle = await jb.waitReady(30000)
const el = document.querySelector(`[data-testid="view-container-${view.id}"]`)
return {
loc: view.visibleLocStrings,
rect: el.getBoundingClientRect().toJSON(),
...settle,
}
- Then
screenshotwithselector: '[data-testid="view-container-<view.id>"]', and readnotReadyin its text part before trusting the image. - On JBrowse Web the capture is the browser agent's own, taken after
jb.waitReadyresolves.
Add a file by URL, and check it lines up
jb.addTrack infers the track and adapter type from the extension, adds the
track to the session and shows it in a view on the right assembly. A GEO bigWig
on the open hg38 session:
const added = await jb.addTrack({
location:
'https://ftp.ncbi.nlm.nih.gov/geo/samples/GSM6703nnn/GSM6703858/suppl/GSM6703858_ATAC-DMSO-Human-1.bigwig',
name: 'ATAC-seq, DMSO rep 1',
})
const values = await jb.getFeatures({ trackId: added.trackId })
return { ...added, valuesInView: values.length }
- The result names the
trackIdit chose, the inferred types, the view it was shown in and the settle report. - A count of zero over a region that should have signal is the refName-mismatch
trap: read
getRefNames()off the adapter (the live model guide shows how) and compare against the assembly's names. - In JBrowse Web the location must be a URL the host serves with CORS headers; a local path is refused before anything is added.
The same data under another display
The display types a track can take are the ones registered for its track type,
and showTrack picks one by name. Read arcs over an alignments track, where the
default would be the pileup:
const view = jb.view()
const conf = session.getTrackById('volvox_cram')
const displayTypes = pluginManager
.getDisplayElements()
.filter(d => d.trackType === conf.type)
.map(d => d.name)
view.hideTrack('volvox_cram')
await view.showTrack('volvox_cram', {}, { type: 'LinearReadArcsDisplay' })
return { displayTypes, ...(await jb.waitReady(30000)) }
To show both at once, add the same file a second time with jb.addTrack under
another name; a trackId is shown once per view.
A second view without replacing the session
jb.loadSessionSpec replaces the whole session. To keep what is open and add a
view beside it, add one to the session and navigate it, naming the assembly
since a fresh view has none:
const second = session.addView('LinearGenomeView', {
displayName: 'second locus',
})
await second.navToLocString('ctgA:40,000-50,000', 'volvox')
await second.launchTrack('gff3tabix_genes')
return {
views: session.views.map(v => `${v.id}: ${v.displayName ?? v.type}`),
...(await jb.waitReady(30000)),
}
jb.sessionSummary() then lists both views with their ids, and every helper
that takes a viewId can be pointed at either.
Side by side
Views stack down the page until the session is arranged into panels.
session.layoutViews takes the same tree a session spec's layout does, with
view ids (or indexes into session.views) in its leaves, turns workspaces mode
on, and applies the order the leaves state. One panel per open view, left to
right:
const seated = session.layoutViews({
direction: 'horizontal',
children: session.views.map(v => ({ views: [v.id] })),
})
return { seated, ...(await jb.waitReady(30000)) }
- A
sizeon each child divides the space ({ views: [id], size: 70 }).direction: "vertical"stacks the panels;"tabs"puts the children in one cell as tabs; a leaf with several ids stacks those views in one tab. - A leaf spelled with any other key throws naming it, and a layout that seats no view throws rather than leaving a blank tab.
- Calling
session.applyLayoutSpecdirectly does neither of the two thingssession.layoutViewsadds, and the views stay stacked with nothing said.