JBrowse 2 · Linear Genome View examples

Files from your host process

Open a track on bytes your host already holds — a notebook kernel, an R session — with no web server and no CORS.

localFiles is name -> bytes, and a track then refers to that name as if it were a URL. It exists for hosts whose data lives in a process rather than at a URL — a Jupyter kernel, an R session, a desktop app. On an ordinary web page you would serve the file; the reason to reach for this is that there is no server to serve it from, and no CORS or public bucket to arrange.

The bytes become a Blob, which JBrowse reads by byte range through Blob.slice(). So an indexed file stays indexed — a bgzipped+tabixed table, a BAM with its .bai, a bigWig — and only the region on screen is ever touched. That is what makes this the answer for a result too big to inline as features, which the notebooks below measure.

Register an index under its conventional sibling name (peaks.bed.gz + peaks.bed.gz.tbi). The adapter asks for it by that name, derived from the data file’s, so nothing on your side has to know which adapter wanted which file.

To add files to a view that is already up, rather than rebuilding it, use createLinearGenomeView’s addLocalFiles.

In a notebook

jbrowse-anywidget wraps this as add_local_file(path), which reads the file and its sibling index out of the kernel:

  • Large results — every NCBI RefSeq exon in the human genome, as tabix rather than as JSON.
  • Large signal — the same comparison for quantitative data, via bigWig.
View source — 103 lines
import { useEffect, useState } from 'react'

import {
  JBrowseLinearGenomeView,
  useCreateViewState,
} from '@jbrowse/react-linear-genome-view2'

import type { LocalFileInput } from '@jbrowse/react-linear-genome-view2'

const assembly = {
  name: 'volvox',
  uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
}

// The track refers to the file by the name it was registered under, exactly as
// it would a URL — so the `.bam` extension still picks the adapter, and the
// adapter still derives its `.bai` sibling by name. Nothing here knows the file
// is local.
const tracks = [
  {
    type: 'AlignmentsTrack',
    trackId: 'local_bam',
    name: 'volvox-sorted.bam (in memory)',
    assemblyNames: ['volvox'],
    adapter: { type: 'BamAdapter', uri: 'volvox-sorted.bam' },
  },
]

// `localFiles` is read once, when the engine is built, like every other option.
// So a new set of files is a new engine, and this component is remounted on a
// `key` below to get one. The engine it leaves behind is useCreateViewState's
// to destroy — React owns it, and unmounting takes the RPC worker threads and
// the autoruns with it.
function LocalFileView({ localFiles }: { localFiles: LocalFileInput }) {
  const state = useCreateViewState({
    assembly,
    tracks,
    localFiles,
    init: { loc: 'ctgA:1..20000', tracks: ['local_bam'] },
  })
  return <JBrowseLinearGenomeView viewState={state} />
}

export default function WithLocalFiles() {
  const [files, setFiles] = useState<LocalFileInput>()
  const [error, setError] = useState<unknown>()

  // Standing in for whatever puts bytes in your host's memory. In a Jupyter
  // kernel that is `path.read_bytes()` and anywidget's binary channel; in an R
  // session, `readBin`. A web page has no such thing, so this demo fetches —
  // which is the one part of it you would not write.
  useEffect(() => {
    const mount = { unmounted: false }
    void (async () => {
      try {
        const [bam, bai] = await Promise.all(
          ['volvox-sorted.bam', 'volvox-sorted.bam.bai'].map(async name => {
            const res = await fetch(
              `https://jbrowse.org/code/jb2/main/test_data/volvox/${name}`,
            )
            if (!res.ok) {
              throw new Error(`HTTP ${res.status} fetching ${name}`)
            }
            return new Uint8Array(await res.arrayBuffer())
          }),
        )
        if (!mount.unmounted) {
          // the whole API: a name, and the bytes behind it. Registering the
          // index under its conventional sibling name is what keeps the file
          // indexed — without it the adapter has no index to seek with
          setFiles({
            'volvox-sorted.bam': bam!,
            'volvox-sorted.bam.bai': bai!,
          })
        }
      } catch (e) {
        console.error(e)
        setError(e)
      }
    })()
    return () => {
      mount.unmounted = true
    }
  }, [])

  const bytes = files?.['volvox-sorted.bam'] as Uint8Array | undefined

  return (
    <div>
      <div style={{ padding: 8, fontSize: 13, background: '#8881' }}>
        {error
          ? `could not read the file: ${error}`
          : bytes
            ? `${bytes.length.toLocaleString()} bytes of BAM held in this page's memory — the pileup below is read out of it by byte range, so panning touches only the bytes for the region on screen`
            : 'reading the file into memory…'}
      </div>
      {files ? (
        // keyed on the names, so a different set of files rebuilds the engine
        <LocalFileView key={Object.keys(files).join(',')} localFiles={files} />
      ) : null}
    </div>
  )
}