JBrowse 2 · React App examples

Customizing the app

Theme, session state, sizing and the web worker.

On this page: Dark theme · Observe the session · Put the session in the URL · Fit the app to a container · Web worker RPC

Dark theme

palette.mode: dark.

Every palette option is in the theming guide.

View source — 33 lines
import { JBrowse } from '@jbrowse/react-app2'

const base = 'https://jbrowse.org/code/jb2/main/test_data/volvox'

const assemblies = [{ name: 'volvox', uri: `${base}/volvox.2bit` }]

const tracks = [
  {
    type: 'AlignmentsTrack',
    trackId: 'volvox_cram',
    name: 'volvox-sorted.cram',
    assemblyNames: ['volvox'],
    adapter: { type: 'CramAdapter', uri: `${base}/volvox-sorted.cram` },
  },
]

export default function DarkTheme() {
  return (
    <JBrowse
      assemblies={assemblies}
      tracks={tracks}
      configuration={{ theme: { palette: { mode: 'dark' } } }}
      views={[
        {
          type: 'LinearGenomeView',
          assembly: 'volvox',
          loc: 'ctgA:1..50000',
          tracks: ['volvox_cram'],
        },
      ]}
    />
  )
}

Observe the session

An observer reading the open views.

Anything marked #getter or #property on the session model or a view model is reactive. There is no change callback: to save the session, take getSnapshot(viewState.session) and pass it back as the session option.

View source — 60 lines
import { JBrowseApp, useCreateViewState } from '@jbrowse/react-app2'
import { observer } from 'mobx-react'

const base = 'https://jbrowse.org/code/jb2/main/test_data/volvox'

const config = {
  assemblies: [{ name: 'volvox', uri: `${base}/volvox.2bit` }],
  tracks: [
    {
      type: 'AlignmentsTrack',
      trackId: 'volvox_cram',
      name: 'volvox-sorted.cram',
      assemblyNames: ['volvox'],
      adapter: { type: 'CramAdapter', uri: `${base}/volvox-sorted.cram` },
    },
  ],
  defaultSession: {
    name: 'observe',
    views: [
      {
        id: 'view-0',
        type: 'LinearGenomeView',
        assembly: 'volvox',
        loc: 'ctgA:1..50000',
        tracks: ['volvox_cram'],
      },
    ],
  },
}

const SessionSummary = observer(function SessionSummary({
  viewState,
}: {
  viewState: NonNullable<ReturnType<typeof useCreateViewState>>
}) {
  const { views } = viewState.session
  return (
    <div style={{ padding: 8, fontFamily: 'monospace', fontSize: 12 }}>
      <div>{views.length} view(s) open</div>
      <ul style={{ margin: 0 }}>
        {views.map(view => (
          <li key={view.id}>
            {view.type} — {view.coarseVisibleLocStrings || 'no region'}
          </li>
        ))}
      </ul>
    </div>
  )
})

export default function ObserveSession() {
  const viewState = useCreateViewState({ config })

  return viewState ? (
    <div>
      <SessionSummary viewState={viewState} />
      <JBrowseApp viewState={viewState} />
    </div>
  ) : null
}

Put the session in the URL

encodeSession / decodeSession, for a sharable link.

The hash fragment never reaches the server, so a long session cannot fail with HTTP 414. Only the session travels: the receiving page supplies its own assemblies and tracks, and File → New session still returns to defaultSession.

View source — 114 lines
import { useEffect, useState } from 'react'

import {
  JBrowseApp,
  decodeSession,
  encodeSession,
  useCreateViewState,
} from '@jbrowse/react-app2'

import type { SessionSnapshot } from '@jbrowse/react-app2'

const base = 'https://jbrowse.org/code/jb2/main/test_data/volvox'

const config = {
  assemblies: [{ name: 'volvox', uri: `${base}/volvox.2bit` }],
  tracks: [
    {
      type: 'FeatureTrack',
      trackId: 'volvox_gff3',
      name: 'Volvox genes',
      assemblyNames: ['volvox'],
      adapter: { type: 'Gff3TabixAdapter', uri: `${base}/volvox.sort.gff3.gz` },
    },
  ],
  defaultSession: {
    name: 'Session in URL',
    views: [
      {
        id: 'view-0',
        type: 'LinearGenomeView',
        assembly: 'volvox',
        loc: 'ctgA:1..50000',
        tracks: ['volvox_gff3'],
      },
    ],
  },
}

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 App({ session, note }: { session?: SessionSnapshot; note: string }) {
  const viewState = useCreateViewState({ config, session })
  const [status, setStatus] = useState(note)

  return viewState ? (
    <div>
      <div style={{ padding: 8, fontSize: 13, background: '#8881' }}>
        {status || 'navigate or open a track, then save from the app toolbar'}
      </div>
      <JBrowseApp
        viewState={viewState}
        headerButtons={
          <button
            type="button"
            onClick={() => {
              void encodeSession(viewState)
                .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 to URL
          </button>
        }
      />
    </div>
  ) : null
}

export default function SessionInUrl() {
  const [session, setSession] = useState<SessionSnapshot | null | undefined>(
    () => (readSessionParam() ? undefined : null),
  )
  const [note, setNote] = useState('')

  useEffect(() => {
    const param = readSessionParam()
    if (!param) {
      return
    }
    decodeSession(param)
      .then(snap => {
        setSession(snap)
        setNote(`restored "${snap.name}" from the URL`)
      })
      .catch((e: unknown) => {
        console.error(e)
        setSession(null)
        setNote(`could not restore the session in the URL: ${e}`)
      })
  }, [])

  return session === undefined ? null : (
    <App session={session ?? undefined} note={note} />
  )
}

Fit the app to a container

The --jbrowse-app-height CSS variable.

--jbrowse-app-height defaults to 100vh. A percentage resolves only inside a container with a definite height, which here is the flex child with minHeight: 0.

View source — 74 lines
import { JBrowse } from '@jbrowse/react-app2'

const assemblies = [
  {
    name: 'volvox',
    sequence: {
      adapter: {
        type: 'TwoBitAdapter',
        uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
      },
    },
    refNameAliases: {
      adapter: {
        type: 'FromConfigAdapter',
        adapterId: 'W6DyPGJ0UU',
        features: [
          { refName: 'ctgA', uniqueId: 'alias1', aliases: ['A'] },
          { refName: 'ctgB', uniqueId: 'alias2', aliases: ['B'] },
        ],
      },
    },
  },
]

const tracks = [
  {
    type: 'AlignmentsTrack',
    trackId: 'volvox_cram',
    name: 'volvox-sorted.cram',
    assemblyNames: ['volvox'],
    category: ['Alignments'],
    adapter: {
      type: 'CramAdapter',
      uri: 'https://jbrowse.org/code/jb2/main/test_data/volvox/volvox-sorted.cram',
    },
  },
]

export default function FitToContainer() {
  return (
    <>
      <style>{`.jbrowseFitDemo { --jbrowse-app-height: 100%; }`}</style>
      <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
        <div
          style={{
            padding: '8px 12px',
            fontSize: 14,
            background: 'color-mix(in srgb, CanvasText 8%, Canvas)',
            borderBottom:
              '1px solid color-mix(in srgb, CanvasText 20%, Canvas)',
          }}
        >
          Your own app chrome lives here. The embedded JBrowse below fills the
          remaining space instead of forcing the full viewport height.
        </div>
        <div className="jbrowseFitDemo" style={{ flex: 1, minHeight: 0 }}>
          <JBrowse
            assemblies={assemblies}
            tracks={tracks}
            views={[
              {
                type: 'LinearGenomeView',
                assembly: 'volvox',
                loc: 'ctgA:1..50000',
                tracks: ['volvox_cram'],
                tracklist: true,
              },
            ]}
          />
        </div>
      </div>
    </>
  )
}

Web worker RPC

Move parsing and rendering off the main thread.

Under webpack, import @jbrowse/react-app2/esm/makeWorkerInstance instead of the ?worker entry, and set output.publicPath: 'auto' so the worker finds its own URL. A module worker, which Vite builds here, cannot load a UMD plugin; a classic worker can.

View source — 55 lines
import { JBrowse } from '@jbrowse/react-app2'
import RpcWorker from '@jbrowse/react-app2/esm/rpcWorker?worker'

const assemblies = [
  {
    name: 'volvox',
    sequence: {
      adapter: {
        type: 'TwoBitAdapter',
        uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
      },
    },
    refNameAliases: {
      adapter: {
        type: 'FromConfigAdapter',
        adapterId: 'W6DyPGJ0UU',
        features: [
          { refName: 'ctgA', uniqueId: 'alias1', aliases: ['A'] },
          { refName: 'ctgB', uniqueId: 'alias2', aliases: ['B'] },
        ],
      },
    },
  },
]

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',
    },
  },
]

export default function WithWebWorker() {
  return (
    <JBrowse
      assemblies={assemblies}
      tracks={tracks}
      makeWorkerInstance={() => new RpcWorker()}
      views={[
        {
          type: 'LinearGenomeView',
          assembly: 'volvox',
          loc: 'ctgA:1..50000',
          tracks: ['volvox_gff3'],
        },
      ]}
    />
  )
}