JBrowse 2 · Build Your Own examples

Controlling the view

Navigate, zoom and toggle tracks from your own UI, and read clicks back.

A location box, zoom buttons and a track list

View source — 151 lines
import { SessionPaletteProvider } from '@jbrowse/core/ui/PaletteContext'
import { DisplayUIProvider } from '@jbrowse/display-ui'
import { LocationBox, RegionSeams, TrackStack } from '@jbrowse/display-ui/embed'
import { useCreateViewState } from '@jbrowse/react-linear-genome-view2'
import { observer } from 'mobx-react'

import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'

const catalogue = [
  { id: 'hg38_phylop', label: 'Conservation' },
  { id: 'hg38_genes', label: 'Genes' },
  { id: 'na12878_exome', label: 'Reads' },
]

const bookmarks = [
  { label: 'BRCA1', loc: 'chr17:43,044,295..43,125,364' },
  { label: 'A whole chromosome', loc: 'chr17' },
  {
    label: 'Two regions at once',
    loc: 'chr17:43,044,295..43,060,000 chr17:43,100,000..43,125,364',
  },
  {
    label: 'Two BRCA genes',
    loc: 'chr17:43,044,295..43,125,364 chr13:32,315,474..32,400,266',
  },
]

const Toolbar = observer(function Toolbar({
  view,
}: {
  view: LinearGenomeViewModel
}) {
  const shown = new Set(view.tracks.map(t => t.configuration.trackId))
  return (
    <div
      style={{
        display: 'flex',
        flexWrap: 'wrap',
        alignItems: 'center',
        gap: 12,
        paddingBottom: 8,
        fontSize: '0.85rem',
      }}
    >
      <LocationBox view={view} />
      <span>
        <button
          type="button"
          aria-label="Zoom out"
          onClick={() => {
            view.zoom(view.bpPerPx * 2)
          }}
        >

        </button>
        <button
          type="button"
          aria-label="Zoom in"
          onClick={() => {
            view.zoom(view.bpPerPx / 2)
          }}
        >
          +
        </button>
      </span>
      {bookmarks.map(({ label, loc }) => (
        <button
          key={label}
          type="button"
          onClick={() => {
            view.navToLocString(loc).catch((e: unknown) => {
              console.error(e)
            })
          }}
        >
          {label}
        </button>
      ))}
      {catalogue.map(({ id, label }) => (
        <label key={id}>
          <input
            type="checkbox"
            checked={shown.has(id)}
            onChange={() => {
              if (shown.has(id)) {
                view.hideTrack(id)
              } else {
                void view.launchTrack(id)
              }
            }}
          />
          {label}
        </label>
      ))}
    </div>
  )
})

const DriveItFromYourApp = observer(function DriveItFromYourApp() {
  const state = useCreateViewState({
    assembly: {
      name: 'hg38',
      uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
      refNameAliases: {
        uri: 'https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
      },
      geneticCodes: { chrM: 2 },
    },
    tracks: [
      {
        trackId: 'hg38_phylop',
        name: 'phyloP 100-way conservation',
        uri: 'https://hgdownload.soe.ucsc.edu/goldenpath/hg38/phyloP100way/hg38.phyloP100way.bw',
        displayDefaults: { height: 100, color: '#3a7ca5' },
      },
      {
        trackId: 'hg38_genes',
        name: 'RefSeq curated genes',
        uri: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz',
        index: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz.csi',
        displayDefaults: { height: 120 },
      },
      {
        trackId: 'na12878_exome',
        name: 'NA12878 exome reads',
        uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/alignments/NA12878/NA12878.alt_bwamem_GRCh38DH.20150826.CEU.exome.cram',
        displayDefaults: { height: 150 },
      },
    ],
    init: {
      loc: 'chr17:43,044,295..43,125,364',
      tracks: ['hg38_phylop', 'hg38_genes'],
    },
  })
  if (!state) {
    return null
  }
  const { session } = state
  return (
    <SessionPaletteProvider session={session}>
      <DisplayUIProvider>
        <Toolbar view={session.view} />
        <TrackStack view={session.view}>
          <RegionSeams view={session.view} />
        </TrackStack>
      </DisplayUIProvider>
    </SessionPaletteProvider>
  )
})

export default DriveItFromYourApp

The whole genome at once

The demo spells out the chromosome list on purpose. view.showAllRegionsInAssembly() would show all 455 sequences in hg38, and every _alt, _random and chrUn_ scaffold elides into a grey smear. No field in the FASTA says which sequences are the chromosomes.

View source — 52 lines
import { SessionPaletteProvider } from '@jbrowse/core/ui/PaletteContext'
import { DisplayUIProvider } from '@jbrowse/display-ui'
import { RegionSeams, Scalebar, TrackStack } from '@jbrowse/display-ui/embed'
import { useCreateViewState } from '@jbrowse/react-linear-genome-view2'
import { observer } from 'mobx-react'

const chromosomes = [
  ...Array.from({ length: 22 }, (_, i) => `chr${i + 1}`),
  'chrX',
  'chrY',
]

const EveryChromosome = observer(function EveryChromosome() {
  const state = useCreateViewState({
    assembly: {
      name: 'hg38',
      uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
      refNameAliases: {
        uri: 'https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
      },
      geneticCodes: { chrM: 2 },
    },
    tracks: [
      {
        trackId: 'hg38_phylop',
        name: 'phyloP 100-way conservation',
        uri: 'https://hgdownload.soe.ucsc.edu/goldenpath/hg38/phyloP100way/hg38.phyloP100way.bw',
        displayDefaults: { height: 120, color: '#3a7ca5' },
      },
    ],
    init: {
      loc: chromosomes.join(' '),
      tracks: ['hg38_phylop'],
    },
  })
  if (!state) {
    return null
  }
  const { session } = state
  return (
    <SessionPaletteProvider session={session}>
      <DisplayUIProvider>
        <TrackStack view={session.view}>
          <Scalebar view={session.view} />
          <RegionSeams view={session.view} />
        </TrackStack>
      </DisplayUIProvider>
    </SessionPaletteProvider>
  )
})

export default EveryChromosome

Feature details on click

View source — 118 lines
import { SessionPaletteProvider } from '@jbrowse/core/ui/PaletteContext'
import { isFeature } from '@jbrowse/core/util/simpleFeature'
import { DisplayUIProvider } from '@jbrowse/display-ui'
import { TrackStack } from '@jbrowse/display-ui/embed'
import { useCreateViewState } from '@jbrowse/react-linear-genome-view2'
import { observer } from 'mobx-react'

const hidden = new Set([
  'refName',
  'start',
  'end',
  'strand',
  'type',
  'name',
  'uniqueId',
])

const FeatureDetails = observer(function FeatureDetails({
  session,
}: {
  session: { selection: unknown; clearSelection: () => void }
}) {
  const { selection } = session
  if (!isFeature(selection)) {
    return <div style={{ padding: 12, opacity: 0.7 }}>Click a gene.</div>
  }
  const data = selection.toJSON()
  const rows = Object.entries(data).filter(
    ([key, value]) =>
      !hidden.has(key) && value !== null && typeof value !== 'object',
  )
  return (
    <div style={{ padding: 12, fontSize: '0.8rem' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
        <strong>{data.name ?? data.type ?? 'Feature'}</strong>
        <button
          type="button"
          onClick={() => {
            session.clearSelection()
          }}
        >
          Clear
        </button>
      </div>
      <div style={{ opacity: 0.75 }}>
        {data.refName}:{data.start.toLocaleString()}-{data.end.toLocaleString()}
      </div>
      <dl
        style={{
          display: 'grid',
          gridTemplateColumns: 'auto 1fr',
          gap: '2px 10px',
        }}
      >
        {rows.map(([key, value]) => (
          <div key={key} style={{ display: 'contents' }}>
            <dt style={{ opacity: 0.7 }}>{key}</dt>
            <dd style={{ margin: 0, wordBreak: 'break-word' }}>
              {String(value)}
            </dd>
          </div>
        ))}
      </dl>
    </div>
  )
})

const YourOwnFeatureDetails = observer(function YourOwnFeatureDetails() {
  const state = useCreateViewState({
    assembly: {
      name: 'hg38',
      uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
      refNameAliases: {
        uri: 'https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
      },
      geneticCodes: { chrM: 2 },
    },
    tracks: [
      {
        trackId: 'hg38_genes',
        name: 'RefSeq curated genes',
        uri: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz',
        index: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz.csi',
        displayDefaults: { height: 180 },
      },
    ],
    init: {
      loc: 'chr17:43,044,295..43,125,364',
      tracks: ['hg38_genes'],
    },
  })
  if (!state) {
    return null
  }
  const { session } = state
  return (
    <SessionPaletteProvider session={session}>
      <DisplayUIProvider>
        <div style={{ display: 'flex' }}>
          <TrackStack view={session.view} style={{ flex: 1, minWidth: 0 }} />
          <div
            style={{
              width: 260,
              flex: 'none',
              overflow: 'auto',
              borderLeft:
                '1px solid color-mix(in srgb, currentColor 25%, transparent)',
            }}
          >
            <FeatureDetails session={session} />
          </div>
        </div>
      </DisplayUIProvider>
    </SessionPaletteProvider>
  )
})

export default YourOwnFeatureDetails

A track selector sidebar

View source — 193 lines
import { useState } from 'react'

import { readConfObject } from '@jbrowse/core/configuration'
import { SessionPaletteProvider } from '@jbrowse/core/ui/PaletteContext'
import { DisplayUIProvider } from '@jbrowse/display-ui'
import { TrackStack } from '@jbrowse/display-ui/embed'
import { useCreateViewState } from '@jbrowse/react-linear-genome-view2'
import { observer } from 'mobx-react'

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

type Session = ViewModel['session']

const variantTrack = {
  trackId: 'thousand_genomes_sv',
  name: 'Structural variants',
  category: ['Variants'],
  assemblyNames: ['hg38'],
  uri: 'https://ftp.1000genomes.ebi.ac.uk/vol1/ftp/data_collections/1000G_2504_high_coverage/working/20210124.SV_Illumina_Integration/1KGP_3202.gatksv_svtools_novelins.freeze_V3.wAF.vcf.gz',
  displayDefaults: { height: 90 },
}

function listTracks(session: Session) {
  return session.tracks.map(conf => {
    const trackId: string = readConfObject(conf, 'trackId')
    const [category = 'Uncategorized'] = (readConfObject(conf, 'category') ??
      []) as string[]
    return {
      trackId,
      category,
      name: (readConfObject(conf, 'name') as string) || trackId,
    }
  })
}

const TrackSelector = observer(function TrackSelector({
  session,
}: {
  session: Session
}) {
  const [filter, setFilter] = useState('')
  const { view } = session
  const shown = new Set(view.tracks.map(t => t.configuration.trackId))
  const needle = filter.trim().toLowerCase()
  const groups = Map.groupBy(
    listTracks(session).filter(e =>
      `${e.name} ${e.category}`.toLowerCase().includes(needle),
    ),
    e => e.category,
  )
  return (
    <div
      style={{
        width: 220,
        flex: 'none',
        display: 'flex',
        flexDirection: 'column',
        gap: 8,
        padding: 10,
        fontSize: '0.8rem',
      }}
    >
      <input
        aria-label="Filter tracks"
        placeholder="Filter tracks"
        value={filter}
        onChange={event => {
          setFilter(event.target.value)
        }}
      />
      {[...groups].map(([category, entries]) => (
        <div key={category}>
          <strong>{category}</strong>
          {entries.map(({ trackId, name }) => (
            <label key={trackId} style={{ display: 'block' }}>
              <input
                type="checkbox"
                checked={shown.has(trackId)}
                onChange={() => {
                  if (shown.has(trackId)) {
                    view.hideTrack(trackId)
                  } else {
                    void view.launchTrack(trackId)
                  }
                }}
              />
              {name}
            </label>
          ))}
        </div>
      ))}
      <button
        type="button"
        disabled={!!session.getTrackById(variantTrack.trackId)}
        style={{ marginTop: 'auto' }}
        onClick={() => {
          session.addSessionTrackConf(variantTrack)
        }}
      >
        Add a variant track
      </button>
    </div>
  )
})

const TrackSelectorSidebar = observer(function TrackSelectorSidebar() {
  const state = useCreateViewState({
    assembly: {
      name: 'hg38',
      uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
      refNameAliases: {
        uri: 'https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
      },
      geneticCodes: { chrM: 2 },
    },
    tracks: [
      {
        trackId: 'hg38_phylop',
        name: 'phyloP conservation',
        category: ['Signal'],
        uri: 'https://jbrowse.org/demos/phylop/hg38.phyloP100way.brca1.bw',
        displayDefaults: { height: 80, color: '#3a7ca5' },
      },
      {
        trackId: 'hg38_gnomad_genome_coverage',
        name: 'gnomAD genome coverage',
        category: ['Signal'],
        uri: 'https://hgdownload.soe.ucsc.edu/gbdb/hg38/gnomAD/coverage/v3-genome/gnomad.coverage.mean.bw',
        displayDefaults: { height: 80 },
      },
      {
        trackId: 'hg38_gnomad_exome_coverage',
        name: 'gnomAD exome coverage',
        category: ['Signal'],
        uri: 'https://hgdownload.soe.ucsc.edu/gbdb/hg38/gnomAD/coverage/v4-exome/gnomad.coverage.mean.bw',
        displayDefaults: { height: 80 },
      },
      {
        trackId: 'hg38_genes',
        name: 'RefSeq curated genes',
        category: ['Annotation'],
        uri: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz',
        index: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz.csi',
        displayDefaults: { height: 120 },
      },
      {
        trackId: 'hg38_segmental_dups',
        name: 'Segmental duplications',
        category: ['Annotation'],
        uri: 'https://jbrowse.org/ucsc/hg38/genomicSuperDups.bed.gz',
        index: 'https://jbrowse.org/ucsc/hg38/genomicSuperDups.bed.gz.csi',
        displayDefaults: { height: 100 },
      },
      {
        trackId: 'na12878_exome',
        name: 'NA12878 exome reads',
        category: ['Alignments'],
        uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/alignments/NA12878/NA12878.alt_bwamem_GRCh38DH.20150826.CEU.exome.cram',
        displayDefaults: { height: 150 },
      },
      {
        trackId: 'thousand_genomes_snvindels',
        name: '1000 Genomes variants',
        uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/variants/ALL.wgs.shapeit2_integrated_snvindels_v2a.GRCh38.27022019.sites.vcf.gz',
        displayDefaults: { height: 80 },
      },
    ],
    init: {
      loc: 'chr17:43,044,295..43,125,364',
      tracks: ['hg38_phylop', 'hg38_genes'],
    },
  })
  if (!state) {
    return null
  }
  const { session } = state
  return (
    <SessionPaletteProvider session={session}>
      <DisplayUIProvider>
        <div style={{ display: 'flex', minHeight: 330 }}>
          <TrackSelector session={session} />
          <TrackStack
            view={session.view}
            trackIds={listTracks(session).map(e => e.trackId)}
            style={{ flex: 1, minWidth: 0 }}
          />
        </div>
      </DisplayUIProvider>
    </SessionPaletteProvider>
  )
})

export default TrackSelectorSidebar