JBrowse 2 · Build Your Own examples

Your own data source

A plugin in the page adds an adapter that reads genes from Ensembl's REST API.

An adapter answers one region at a time, so any server that takes a range can feed a track. Ensembl names a chromosome 17 where the hg38 FASTA has chr17, and the assembly’s refNameAliases file maps them. A region over the server’s limit throws regionTooLarge, which the track shows without a retry button. Creating adapters covers the full API.

View source — 160 lines
import Plugin from '@jbrowse/core/Plugin'
import { ConfigurationSchema } from '@jbrowse/core/configuration'
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType'
import { ObservableCreate } from '@jbrowse/core/util/rxjs'
import SimpleFeature from '@jbrowse/core/util/simpleFeature'
import {
  EmbedProvider,
  Legend,
  LocationBox,
  Toolbar,
  TrackStack,
} from '@jbrowse/display-ui/embed'
import { useCreateViewState } from '@jbrowse/react-linear-genome-view2'
import { observer } from 'mobx-react'

import type PluginManager from '@jbrowse/core/PluginManager'
import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter'
import type { Feature } from '@jbrowse/core/util/simpleFeature'
import type { NoAssemblyRegion } from '@jbrowse/core/util/types'

const ensembl = 'https://rest.ensembl.org'
const MAX_SPAN = 5_000_000

interface EnsemblGene {
  id: string
  start: number
  end: number
  strand: number
  biotype: string
  external_name?: string
  description?: string
}

async function getJson<T>(path: string, signal?: AbortSignal) {
  const url = new URL(path, ensembl)
  url.searchParams.set('content-type', 'application/json')
  const response = await fetch(url, { signal })
  if (!response.ok) {
    throw new Error(`Ensembl answered ${response.status} for ${url}`)
  }
  return (await response.json()) as T
}

const configSchema = ConfigurationSchema(
  'EnsemblGenesAdapter',
  { species: { type: 'string', defaultValue: 'homo_sapiens' } },
  { explicitlyTyped: true },
)

class EnsemblGenesAdapter extends BaseFeatureDataAdapter {
  async getRefNames(opts?: BaseOptions) {
    const species = this.getConf('species') as string
    const { karyotype } = await getJson<{ karyotype: string[] }>(
      `/info/assembly/${species}`,
      opts?.signal,
    )
    return karyotype
  }

  getFeatures(region: NoAssemblyRegion, opts?: BaseOptions) {
    return ObservableCreate<Feature>(async observer => {
      const { refName, start, end } = region
      if (end - start > MAX_SPAN) {
        throw Object.assign(
          new Error('Ensembl serves 5 Mb at a time; zoom in to see genes'),
          { regionTooLarge: true },
        )
      }
      const species = this.getConf('species') as string
      const genes = await getJson<EnsemblGene[]>(
        `/overlap/region/${species}/${refName}:${start + 1}-${end}?feature=gene`,
        opts?.signal,
      )
      for (const gene of genes) {
        observer.next(
          new SimpleFeature({
            uniqueId: `${this.id}-${gene.id}`,
            refName,
            start: gene.start - 1,
            end: gene.end,
            strand: gene.strand,
            type: 'gene',
            name: gene.external_name ?? gene.id,
            biotype: gene.biotype,
            description: gene.description,
          }),
        )
      }
      observer.complete()
    }, opts?.signal)
  }
}

class EnsemblPlugin extends Plugin {
  name = 'EnsemblPlugin'

  install(pluginManager: PluginManager) {
    pluginManager.addAdapterType(
      () =>
        new AdapterType({
          name: 'EnsemblGenesAdapter',
          configSchema,
          AdapterClass: EnsemblGenesAdapter,
        }),
    )
  }
}

const InlinePlugin = observer(function InlinePlugin() {
  const state = useCreateViewState({
    plugins: [EnsemblPlugin],
    assembly: {
      name: 'hg38',
      uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
      refNameAliases: {
        uri: 'https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
      },
    },
    tracks: [
      {
        type: 'FeatureTrack',
        trackId: 'ensembl_genes',
        name: 'Ensembl genes',
        assemblyNames: ['hg38'],
        adapter: { type: 'EnsemblGenesAdapter' },
        displays: [
          {
            type: 'LinearBasicDisplay',
            displayId: 'ensembl_genes-LinearBasicDisplay',
            height: 160,
            // skips the density probe, which samples the server window by window
            forceLoad: true,
            encoding: { color: 'biotype' },
            legend: { show: false },
          },
        ],
      },
    ],
    view: {
      loc: 'chr17:42,950,000..43,250,000',
      tracks: ['ensembl_genes'],
    },
  })
  if (!state) {
    return null
  }
  const { view } = state.session
  return (
    <EmbedProvider session={state.session}>
      <Toolbar>
        <LocationBox view={view} />
        <Legend view={view} trackId="ensembl_genes" />
      </Toolbar>
      <TrackStack view={view} />
    </EmbedProvider>
  )
})

export default InlinePlugin