JBrowse 2 · Linear Genome View examples

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:

  • session vs defaultSession fill the same slot but are checked differently. defaultSession is 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 in session and 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 assembly and tracks; the snapshot names them. The encoding is JBrowse Web’s own, so links open in either.
  • encodeSession does more than getSnapshot: 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
}