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>
)
}