JBrowse 2 · Linear Genome View examples

Pan-UKB GWAS

Pan-UK Biobank GWAS summary statistics across phenotypes.

Pan-UKBB columns are already −log₁₀(p). For a raw p-value column set scoreTransform: 'negLog10', or 'negLog10FromLn' for a natural-log one.

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

import {
  JBrowseLinearGenomeView,
  useCreateViewState,
} from '@jbrowse/react-linear-genome-view2'

const BASE = 'https://pan-ukb-us-east-1.s3.amazonaws.com/sumstats_flat_files'

const MANIFEST_URL = 'https://jbrowse.org/demos/panukbb/panukbPhenotypes.json'

interface Phenotype {
  id: string
  traitType: string
  description: string
  category: string
  n: number
  pops: string[]
  popsQc: string[]
}

const POP_LABELS: Record<string, string> = {
  EUR: 'European',
  AFR: 'African',
  AMR: 'Admixed American',
  CSA: 'Central/South Asian',
  EAS: 'East Asian',
  MID: 'Middle Eastern',
}

const FEATURED: Record<string, string> = {
  'continuous-50-both_sexes-irnt': 'chr12:64,000,000..67,000,000',
  'continuous-21001-both_sexes-irnt': 'chr16:53,000,000..55,000,000',
  'continuous-4079-both_sexes-irnt': 'chr4:81,000,000..82,500,000',
  'biomarkers-30690-both_sexes-irnt': 'chr1:54,500,000..56,000,000',
  'biomarkers-30780-both_sexes-irnt': 'chr19:11,000,000..11,500,000',
  'icd10-E11-both_sexes': 'chr10:112,500,000..113,500,000',
  'icd10-I25-both_sexes': 'chr9:21,500,000..22,500,000',
  'icd10-J45-both_sexes': 'chr17:37,500,000..38,500,000',
}

function popOptions(p: Phenotype) {
  return [
    ...(p.popsQc.length
      ? [{ col: 'neglog10_pval_meta_hq', label: 'Meta-analysis (QC pass)' }]
      : []),
    { col: 'neglog10_pval_meta', label: 'Meta-analysis (all)' },
    ...p.pops.map(pop => ({
      col: `neglog10_pval_${pop}`,
      label: POP_LABELS[pop] ?? pop,
    })),
  ]
}

const assembly = {
  name: 'hg38',
  aliases: ['GRCh38'],
  uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
  refNameAliases: {
    uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
  },
  cytobands: {
    uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/cytoBand.txt',
  },
  geneticCodes: { chrM: 2 },
}

const NCBI_REFSEQ_TRACK = {
  type: 'FeatureTrack',
  trackId: 'ncbi_refseq_hg38',
  name: 'NCBI RefSeq genes',
  assemblyNames: ['hg38'],
  adapter: {
    type: 'Gff3TabixAdapter',
    uri: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeq.gff.gz',
    csi: true,
  },
  displayDefaults: {
    height: 200,
    labels: {
      name: "jexl:get(feature,'gene_id') || get(feature,'name') || get(feature,'id')",
    },
  },
}

function makeTrack(p: Phenotype, scoreColumn: string, label: string) {
  return {
    type: 'GWASTrack',
    trackId: 'panukb_gwas',
    name: `${p.description} — ${label}`,
    assemblyNames: ['hg38'],
    adapter: {
      type: 'GWASAdapter',
      scoreColumn,
      uri: `${BASE}/${p.id}.tsv.bgz`,
    },
    displayDefaults: { height: 250 },
  }
}

function GenomeView({
  phenotype,
  scoreColumn,
  label,
}: {
  phenotype: Phenotype
  scoreColumn: string
  label: string
}) {
  const state = useCreateViewState({
    assembly,
    tracks: [makeTrack(phenotype, scoreColumn, label), NCBI_REFSEQ_TRACK],
    defaultSession: {
      name: 'Pan-UKB GWAS',
      view: {
        type: 'LinearGenomeView',
        assembly: assembly.name,
        loc: FEATURED[phenotype.id] ?? 'chr1',
        tracks: ['panukb_gwas', 'ncbi_refseq_hg38'],
      },
    },
  })
  return state ? <JBrowseLinearGenomeView viewState={state} /> : null
}

export default function PanUKBGWAS() {
  const [phenotypes, setPhenotypes] = useState<Phenotype[]>()
  const [query, setQuery] = useState('')
  const [selected, setSelected] = useState<Phenotype>()
  const [scoreColumn, setScoreColumn] = useState('neglog10_pval_meta_hq')

  useEffect(() => {
    const ac = new AbortController()
    fetch(MANIFEST_URL, { signal: ac.signal })
      .then(res => res.json() as Promise<Phenotype[]>)
      .then(data => {
        setPhenotypes(data)
        setSelected(
          data.find(p => p.id === 'continuous-50-both_sexes-irnt') ?? data[0],
        )
      })
      .catch((e: unknown) => {
        if (!ac.signal.aborted) {
          console.error(e)
        }
      })
    return () => {
      ac.abort()
    }
  }, [])

  const results = useMemo(() => {
    if (!phenotypes) {
      return []
    }
    const q = query.trim().toLowerCase()
    const matches = q
      ? phenotypes.filter(
          p =>
            p.description.toLowerCase().includes(q) ||
            p.category.toLowerCase().includes(q) ||
            p.id.toLowerCase().includes(q),
        )
      : phenotypes
    return matches.slice(0, 100)
  }, [phenotypes, query])

  const options = selected ? popOptions(selected) : []
  const activeCol = options.some(o => o.col === scoreColumn)
    ? scoreColumn
    : (options[0]?.col ?? 'neglog10_pval_meta')
  const activeLabel = options.find(o => o.col === activeCol)?.label ?? activeCol

  return (
    <div>
      <div style={{ display: 'flex', gap: 16, marginBottom: 8 }}>
        <div style={{ flex: 1 }}>
          <input
            type="text"
            placeholder="Search ~7,200 phenotypes (e.g. height, diabetes, LDL)…"
            value={query}
            onChange={e => {
              setQuery(e.target.value)
            }}
            style={{ width: '100%', padding: 4, boxSizing: 'border-box' }}
          />
          <select
            size={6}
            value={selected?.id ?? ''}
            onChange={e => {
              const p = phenotypes?.find(x => x.id === e.target.value)
              if (p) {
                setSelected(p)
              }
            }}
            style={{ width: '100%', marginTop: 4 }}
          >
            {results.map(p => (
              <option key={p.id} value={p.id}>
                {p.description} [{p.traitType}, n={p.n.toLocaleString()}]
              </option>
            ))}
          </select>
          <div style={{ fontSize: '0.8em', color: 'GrayText' }}>
            {!phenotypes
              ? 'Loading phenotype catalog…'
              : query
                ? `${results.length}${results.length === 100 ? '+' : ''} match`
                : `${phenotypes.length.toLocaleString()} phenotypes`}
          </div>
        </div>
        <label>
          Population:{' '}
          <select
            value={activeCol}
            onChange={e => {
              setScoreColumn(e.target.value)
            }}
          >
            {options.map(o => (
              <option key={o.col} value={o.col}>
                {o.label}
              </option>
            ))}
          </select>
        </label>
      </div>
      {selected ? (
        <GenomeView
          key={`${selected.id}-${activeCol}`}
          phenotype={selected}
          scoreColumn={activeCol}
          label={activeLabel}
        />
      ) : null}
    </div>
  )
}