JBrowse 2 · Linear Genome View examples

Multiple views

React to the view from your own companion panels, and render several independent views on one page.

Observe the visible view

The view model is a MobX-state-tree node, so anything outside the view can wrap in mobx-react’s observer and re-render when the fields it reads change — coordinate readouts, feature inspectors, summary tables — with no event wiring:

const VisibleRegions = observer(function VisibleRegions({ viewState }) {
  return <div>{viewState.session.view.coarseVisibleLocStrings}</div>
})

Reading the visible regions is synchronous: view.dynamicBlocks updates every pan/zoom frame, view.coarseDynamicBlocks is its debounced twin. Reading actual feature data goes through the RPC manager, so key that query off the coarse blocks in an autorun inside an effect, or you fire a fetch per animation frame of a drag.

Anything marked #getter or #property in the state model is reactive and safe to read. That is the whole read API: there is no change callback to subscribe to, because observer already re-renders exactly the components that read what changed.

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

import { getConf } from '@jbrowse/core/configuration'
import { getRpcSessionId } from '@jbrowse/core/util/tracks'
import {
  JBrowseLinearGenomeView,
  useCreateViewState,
} from '@jbrowse/react-linear-genome-view2'
import { autorun } from 'mobx'
import { observer } from 'mobx-react'

import type { Feature } from '@jbrowse/core/util'
import type { ViewModel } from '@jbrowse/react-linear-genome-view2'

// reading the visible regions is synchronous observable state.
// coarseVisibleLocStrings is the debounced twin of visibleLocStrings, and it
// already assembles the locstring for you — including the [rev] marker on a
// flipped region and the assembly prefix when several are on screen
const VisibleRegions = observer(function VisibleRegions({
  viewState,
}: {
  viewState: ViewModel
}) {
  const view = viewState.session.view
  return view.initialized ? (
    <p>Visible region {view.coarseVisibleLocStrings}</p>
  ) : null
})

// reading actual feature data needs an RPC round-trip, keyed off the debounced
// coarseDynamicBlocks so a drag doesn't fire a fetch per frame
const VisibleFeatures = observer(function VisibleFeatures({
  viewState,
}: {
  viewState: ViewModel
}) {
  const [features, setFeatures] = useState<Feature[]>()
  const [error, setError] = useState<unknown>()
  const { rpcManager, view } = viewState.session

  useEffect(() => {
    // each run supersedes the one before it. Two pans in quick succession leave
    // two calls in flight, and they can come back in either order — without the
    // generation check the slower, older one lands last and the table shows the
    // region you already left
    let latest = 0
    return autorun(() => {
      if (view.initialized) {
        const track = view.tracks[0]
        if (track) {
          const adapterConfig = getConf(track, 'adapter')
          const sessionId = getRpcSessionId(track)
          const generation = ++latest
          void rpcManager
            // No handles here on purpose: this page's whole subject is the
            // generation check below, and the source is published verbatim as a
            // `?raw` block, so a stop token and a status sink would be two more
            // mechanisms to read past before reaching the one it teaches. A real
            // display gets both off `ctx` — see FetchMixin.
            // eslint-disable-next-line no-restricted-syntax
            .call(sessionId, 'CoreGetFeatures', {
              adapterConfig,
              regions: view.coarseDynamicBlocks,
            })
            .then(feats => {
              if (generation === latest) {
                setFeatures(feats)
              }
            })
            .catch((e: unknown) => {
              if (generation === latest) {
                setError(e)
              }
            })
        }
      }
    })
  }, [rpcManager, view])

  return error ? (
    <div>Error: {String(error)}</div>
  ) : !features ? (
    <div>Loading...</div>
  ) : (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Location</th>
        </tr>
      </thead>
      <tbody>
        {features.map(f => (
          <tr key={f.id()}>
            <td>{f.get('name')}</td>
            <td>
              {f.get('refName')}:{f.get('start')}-{f.get('end')}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  )
})

export default function ObserveVisible() {
  const state = useCreateViewState({
    assembly: {
      name: 'volvox',
      uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
    },
    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',
        },
      },
    ],
    // open the track (via a default session) so it appears in view.tracks — the
    // feature table below reads its adapter to fetch the visible features
    defaultSession: {
      name: 'Observe visible',
      view: {
        type: 'LinearGenomeView',
        init: {
          assembly: 'volvox',
          loc: 'ctgA:1105..1221',
          tracks: ['volvox_gff3'],
        },
      },
    },
  })
  return (
    <div>
      <JBrowseLinearGenomeView viewState={state} />
      <VisibleRegions viewState={state} />
      <VisibleFeatures viewState={state} />
    </div>
  )
}

Observe the selected feature

Clicking a feature opens its details widget and sets session.selection. That is plain observable state, so an observer panel outside the view mirrors it into your app — highlight a table row, fetch related records, update a URL — with no click handler registered.

selection is typed unknown, because it can hold whatever the app selected (a feature, a view, a region). Guard it with isFeature from @jbrowse/core/util before reading get('name'). It is the same reactive pattern as watching the visible region.

To change what the details panel itself shows, pass a formatDetails block in createViewState’s configuration — see customizing feature details.

View source — 65 lines
import { isFeature } from '@jbrowse/core/util'
import {
  JBrowseLinearGenomeView,
  useCreateViewState,
} from '@jbrowse/react-linear-genome-view2'
import { observer } from 'mobx-react'

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

// session.selection is set to a Feature whenever the user clicks one (the same
// path that opens the feature-details widget). An observer re-renders when it
// changes, so a companion panel stays in sync with no click handler wiring.
const SelectedFeature = observer(function SelectedFeature({
  viewState,
}: {
  viewState: ViewModel
}) {
  const { selection } = viewState.session
  return isFeature(selection) ? (
    <p>
      Selected <b>{selection.get('name') || selection.id()}</b> at{' '}
      {selection.get('refName')}:{selection.get('start')}-{selection.get('end')}
    </p>
  ) : (
    <p>Click a feature to select it.</p>
  )
})

export default function ObserveSelection() {
  const state = useCreateViewState({
    assembly: {
      name: 'volvox',
      uri: 'https://jbrowse.org/genomes/volvox/volvox.2bit',
    },
    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',
        },
      },
    ],
    defaultSession: {
      name: 'Observe selection',
      view: {
        type: 'LinearGenomeView',
        init: {
          assembly: 'volvox',
          loc: 'ctgA:1..50,000',
          tracks: ['volvox_gff3'],
        },
      },
    },
  })
  return (
    <div>
      <JBrowseLinearGenomeView viewState={state} />
      <SelectedFeature viewState={state} />
    </div>
  )
}

Two linear genome views

Two or more views coexist on a page. Each gets its own viewState, and they share no navigation, tracks or session state.

To make them track each other — linked panning, shared zoom — wire it through the state tree: wrap a sibling in observer (the pattern), read one view’s bpPerPx/offsetPx, and call the matching actions on the other.

View source — 41 lines
import {
  JBrowseLinearGenomeView,
  useCreateViewState,
} 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',
    },
  },
]

export default function WithTwoLinearGenomeViews() {
  const state1 = useCreateViewState({
    assembly,
    tracks,
    location: 'ctgA:1105..1221',
  })
  const state2 = useCreateViewState({
    assembly,
    tracks,
    location: 'ctgA:5560..30589',
  })
  return (
    <div>
      <JBrowseLinearGenomeView viewState={state1} />
      <JBrowseLinearGenomeView viewState={state2} />
    </div>
  )
}