Init & persistence
A richer initial view with advanced init and highlights, then persisting or sharing the live session.
Advanced init
init accepts more than trackId strings. A tracks entry can be an object
carrying a displaySnapshot (initial display state — type, height, score range,
colors) or a trackSnapshot (e.g. pinned: true), and the view itself takes
tracklist, nav and highlight:
init: {
loc: 'chr1:11,106,077-11,261,675',
tracklist: true, // open the track selector
nav: true, // keep the nav bar visible
highlight: ['chr1:11,170,000-11,190,000'],
tracks: [{ trackId: 'my-track', displaySnapshot: { height: 200 } }],
}
This is the embedded form of the “advanced track configuration” session spec
JBrowse Web puts in its URL params.
What a displaySnapshot accepts is per display type in the state-model
reference, e.g.
LinearBasicDisplay
and
LinearWiggleDisplay.
View source — 39 lines
import { LinearGenomeView } from '@jbrowse/react-linear-genome-view2'
// managed API: the `init` blob is the component's whole declarative input —
// loc, which tracks to open (with per-display snapshots), tracklist/nav
// visibility, and highlights
export default function WithInitAdvanced() {
return (
<LinearGenomeView
assembly={{
name: 'hg38',
uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
refNameAliases: {
uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
},
}}
tracks={[
{
type: 'FeatureTrack',
trackId: 'ncbi-refseq-genes',
name: 'NCBI RefSeq Genes',
assemblyNames: ['hg38'],
adapter: {
type: 'Gff3TabixAdapter',
uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/ncbi_refseq/GCA_000001405.15_GRCh38_full_analysis_set.refseq_annotation.sorted.gff.gz',
},
},
]}
init={{
loc: 'chr1:11,106,077-11,261,675',
tracklist: true,
nav: true,
tracks: [
{ trackId: 'ncbi-refseq-genes', displaySnapshot: { height: 200 } },
],
highlight: ['chr1:11,170,000-11,190,000'],
}}
/>
)
}Session highlights
Highlights paint a region over the genome — a locus of interest, a search hit, a variant. They live on the session, so they round-trip through saved sessions.
Authoring them on the view snapshot lets each one carry its own color and label:
highlight: [
{
assemblyName: 'hg38',
refName: 'chr1',
start: 11_130_000,
end: 11_145_000,
color: 'rgba(255, 0, 0, 0.25)',
label: 'Region of interest',
},
]
There is also init.highlight, which
takes plain locstrings and so has nowhere to put a color or a label.
addToHighlights / removeHighlight are in the
state model.
View source — 67 lines
import {
JBrowseLinearGenomeView,
useCreateViewState,
} from '@jbrowse/react-linear-genome-view2'
export default function WithSessionHighlights() {
const state = useCreateViewState({
assembly: {
name: 'hg38',
aliases: ['GRCh38'],
uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
refNameAliases: {
uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
},
},
tracks: [
{
type: 'FeatureTrack',
trackId: 'ncbi-refseq-genes',
name: 'NCBI RefSeq Genes',
assemblyNames: ['hg38'],
adapter: {
type: 'Gff3TabixAdapter',
uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/ncbi_refseq/GCA_000001405.15_GRCh38_full_analysis_set.refseq_annotation.sorted.gff.gz',
},
},
],
defaultSession: {
name: 'Session highlights',
view: {
type: 'LinearGenomeView',
// highlights authored on the view snapshot carry per-highlight color
// and label, and round-trip through saved sessions. compare with
// init.highlight, which only accepts plain loc-strings
highlight: [
{
assemblyName: 'hg38',
refName: 'chr1',
start: 11_130_000,
end: 11_145_000,
color: 'rgba(255, 0, 0, 0.25)',
label: 'Region of interest',
},
{
assemblyName: 'hg38',
refName: 'chr1',
start: 11_200_000,
end: 11_220_000,
color: 'rgba(0, 128, 255, 0.25)',
label: 'Promoter',
},
],
init: {
loc: 'chr1:11,106,077-11,261,675',
assembly: 'hg38',
tracks: [
{
trackId: 'ncbi-refseq-genes',
displaySnapshot: { height: 200 },
},
],
},
},
},
})
return <JBrowseLinearGenomeView viewState={state} />
}Persist & restore the session
A defaultSession restores state on first paint; to keep
it current, mirror the live session back out. The session is a MobX-state-tree
node, so onSnapshot hands you a serializable snapshot after every change:
import { onSnapshot } from '@jbrowse/mobx-state-tree'
onSnapshot(state.session, snap =>
localStorage.setItem(KEY, JSON.stringify(snap)),
)
The snapshot references tracks by trackId and the assembly by name, so it
restores against the same assembly/tracks config you pass on every load.
Swap localStorage for a server call to persist per user.
This keeps a session for one browser. To hand one to someone else, use the session in the URL.
View source — 99 lines
import { useEffect, useState } from 'react'
import { getSnapshot, onSnapshot } from '@jbrowse/mobx-state-tree'
import {
JBrowseLinearGenomeView,
createViewState,
} from '@jbrowse/react-linear-genome-view2'
const STORAGE_KEY = 'jbrowse-lgv-example-session'
const assembly = {
name: 'volvox',
uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
}
const tracks = [
{
type: 'FeatureTrack',
trackId: 'volvox_gff3',
name: 'Volvox genes',
assemblyNames: ['volvox'],
adapter: {
type: 'Gff3TabixAdapter',
uri: 'https://jbrowse.org/code/jb2/main/test_data/volvox/volvox.sort.gff3.gz',
},
},
]
const freshSession = {
name: 'Persisted session',
view: {
type: 'LinearGenomeView',
init: {
assembly: 'volvox',
loc: 'ctgA:1105..1221',
tracks: ['volvox_gff3'],
},
},
}
export default function WithSessionPersistence() {
const [state] = useState(() => {
// the session snapshot references trackIds/assembly by name, so it restores
// against the same `assembly`/`tracks` config passed on every load. It goes
// in `session` rather than `defaultSession`: what comes back out of storage
// is only known at runtime, and that is the slot MST validates
const saved = localStorage.getItem(STORAGE_KEY)
// and it is dropped *before* being used, because a snapshot this build can
// no longer open takes the render down with it — the reset button below
// included — and with the entry still in storage every reload after it
// fails the same way. The effect below puts it back once a render has
// actually committed, so the reload the user was going to try is the fix.
localStorage.removeItem(STORAGE_KEY)
try {
return createViewState({
assembly,
tracks,
// `session`, not `defaultSession`: what comes back out of storage is
// only known at runtime, and that is the slot MST validates
session: saved ? JSON.parse(saved) : undefined,
defaultSession: freshSession,
})
} catch (e) {
// unparseable, or a snapshot MST rejected outright
console.error(e)
return createViewState({ assembly, tracks, defaultSession: freshSession })
}
})
// an effect, so this only runs on a render that committed. onSnapshot fires
// after each action — mirror the live session into localStorage so pans,
// zooms and track toggles survive a reload — but write once up front too,
// since a load where nothing happens still has a session worth keeping
useEffect(() => {
const save = (snap: unknown) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(snap))
}
save(getSnapshot(state.session))
return onSnapshot(state.session, save)
}, [state])
return (
<div>
<p>
Pan, zoom, or toggle tracks, then reload the page — the view comes back
where you left it.{' '}
<button
onClick={() => {
localStorage.removeItem(STORAGE_KEY)
location.reload()
}}
>
Reset saved session
</button>
</p>
<JBrowseLinearGenomeView viewState={state} />
</div>
)
}Put the session in the URL
Persistence keeps a session for
one browser; putting it in the URL makes it shareable. encodeSession
serializes the live session into a compact URL-safe string and decodeSession
turns one back into a snapshot:
const param = new URLSearchParams(location.hash.slice(1)).get('session')
const state = createViewState({
assembly,
tracks,
session: param ? await decodeSession(param) : undefined,
defaultSession: param ? undefined : freshSession,
})
Four things worth knowing:
sessionvsdefaultSessionfill the same slot but are checked differently.defaultSessionis validated against the session model’s shape, which suits one you wrote; a decoded session’s shape is only known at runtime, so it goes insessionand is checked as it is applied.- Use the hash fragment, not the query string. The fragment never reaches the server, so a long session can’t come back as an HTTP 414.
- Only the session travels. The receiving page supplies its own
assemblyandtracks; the snapshot names them. The encoding is JBrowse Web’s own, so links open in either. encodeSessiondoes more thangetSnapshot: display settings a user picked up from a promoted display-type default live in their browser rather than in the session, and it folds those in first — otherwise the link renders differently for whoever opens it.
View source — 150 lines
import { useEffect, useState } from 'react'
import {
JBrowseLinearGenomeView,
createViewState,
decodeSession,
destroyViewState,
encodeSession,
} from '@jbrowse/react-linear-genome-view2'
import type {
SessionSnapshot,
ViewModel,
} from '@jbrowse/react-linear-genome-view2'
const assembly = {
name: 'volvox',
uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
}
const tracks = [
{
type: 'FeatureTrack',
trackId: 'volvox_gff3',
name: 'Volvox genes',
assemblyNames: ['volvox'],
adapter: {
type: 'Gff3TabixAdapter',
uri: 'https://jbrowse.org/code/jb2/main/test_data/volvox/volvox.sort.gff3.gz',
},
},
]
// what opens when the URL carries no session
const freshSession = {
name: 'Session in URL',
view: {
type: 'LinearGenomeView',
init: {
assembly: 'volvox',
loc: 'ctgA:1..50000',
tracks: ['volvox_gff3'],
},
},
}
// The session goes in the hash fragment rather than the query string. The
// fragment is never sent to the server, so a long session can't overflow the
// request line (HTTP 414) — the same reason JBrowse Web keeps its own there.
function readSessionParam() {
return (
new URLSearchParams(window.location.hash.slice(1)).get('session') ??
undefined
)
}
function writeSessionParam(value: string) {
const params = new URLSearchParams(window.location.hash.slice(1))
params.set('session', value)
window.history.replaceState(null, '', `#${params.toString()}`)
}
function build(session?: SessionSnapshot) {
return createViewState({
assembly,
tracks,
// `session` is the slot for a snapshot whose shape is only known at
// runtime; `defaultSession` is for one you author and want checked
session,
defaultSession: session ? undefined : freshSession,
})
}
export default function SessionInUrl() {
// undefined while the URL is being decoded, so the view isn't built with an
// empty session first and then replaced. With no session to decode there is
// nothing to wait for, so build the normal starting state right here rather
// than rendering nothing and setting it from the effect below.
const [state, setState] = useState<ViewModel | undefined>(() =>
readSessionParam() ? undefined : build(),
)
const [status, setStatus] = useState('')
useEffect(() => {
const param = readSessionParam()
if (!param) {
return
}
// The engine is not owned by React, so unmounting alone leaves its RPC
// worker threads and its autoruns running — and the decode below can land
// after this effect was torn down (in StrictMode it usually does), which
// would build one that nothing ever destroys. One box rather than two
// `let`s, because the compiler's narrowing doesn't see through the cleanup.
const mount = {
unmounted: false,
engine: undefined as ViewModel | undefined,
}
const open = (session?: SessionSnapshot) => {
if (!mount.unmounted) {
mount.engine = build(session)
setState(mount.engine)
}
}
decodeSession(param)
.then(session => {
open(session)
setStatus(`restored "${session.name}" from the URL`)
})
.catch((e: unknown) => {
// a truncated or hand-edited link shouldn't strand the user on a
// blank view: fall back to the normal starting state and say so
console.error(e)
open()
setStatus(`could not restore the session in the URL: ${e}`)
})
return () => {
mount.unmounted = true
if (mount.engine) {
destroyViewState(mount.engine)
}
}
}, [])
return state ? (
<div>
<div style={{ padding: 8, fontSize: 13, background: '#8881' }}>
<button
type="button"
onClick={() => {
void encodeSession(state)
.then(encoded => {
writeSessionParam(encoded)
setStatus(
`saved to the URL (${encoded.length} chars) — copy the address bar, or reload to restore it`,
)
})
.catch((e: unknown) => {
console.error(e)
setStatus(`could not save the session to the URL: ${e}`)
})
}}
>
Save this view to the URL
</button>{' '}
{status || 'navigate or toggle a track, then save'}
</div>
<JBrowseLinearGenomeView viewState={state} />
</div>
) : null
}