Skip to content

Latest commit

 

History

490 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@gmod/tabix

NPM version Build Status

Read Tabix-indexed files using either .tbi or .csi indexes.

Install

npm install @gmod/tabix

Usage

import { TabixIndexedFile } from '@gmod/tabix'

// Local file — TBI index assumed at path + '.tbi'
const file = new TabixIndexedFile({ path: 'file.vcf.gz' })

// CSI index
const csi = new TabixIndexedFile({
  path: 'file.vcf.gz',
  csiPath: 'file.vcf.gz.csi',
})

// Remote files
const remote = new TabixIndexedFile({
  url: 'https://example.com/file.vcf.gz',
  tbiUrl: 'https://example.com/file.vcf.gz.tbi',
})

// Or with a filehandle from generic-filehandle2
import { RemoteFile } from 'generic-filehandle2'

const custom = new TabixIndexedFile({
  filehandle: new RemoteFile('https://example.com/file.vcf.gz'),
  tbiFilehandle: new RemoteFile('https://example.com/file.vcf.gz.tbi'),
})

Over HTTP it is worth swapping in @gmod/range-cache-filehandle. A query fetches the index once, then reads the BGZF blocks it points at as byte ranges spread through the file. Overlapping queries re-read the same blocks: panning twenty half-overlapping windows across the 3.4 MB test BED file reads 11 MB. The cache serves those reads out of 256 KiB chunks, so neighboring blocks share a request and each byte is fetched once.

import { RemoteFileWithRangeCache } from '@gmod/range-cache-filehandle'

const cached = new TabixIndexedFile({
  filehandle: new RemoteFileWithRangeCache('https://example.com/file.vcf.gz'),
  tbiFilehandle: new RemoteFileWithRangeCache(
    'https://example.com/file.vcf.gz.tbi',
  ),
})

getLines

Fetches lines overlapping a region. start/end are 0-based half-open coordinates (unlike the tabix CLI which uses 1-based closed).

const lines: string[] = []
await file.getLines('chr1', 200, 300, line => lines.push(line))

The callback also receives the virtual file offset and parsed coordinates for the line:

await file.getLines('chr1', 200, 300, (line, fileOffset, start, end) => {
  lines.push(line)
})

Pass an options object instead of a bare callback to abort the query or track download progress:

const aborter = new AbortController()
await file.getLines('chr1', 200, 300, {
  lineCallback: (line, fileOffset, start, end) => lines.push(line),
  signal: aborter.signal,
  onProgress: (bytesDownloaded, totalBytes) => {
    console.log(`${bytesDownloaded}/${totalBytes}`)
  },
})

onProgress ticks once per chunk — the run of BGZF blocks the index resolves a query to — including instant ticks for chunks already cached, and the index supplies totalBytes up front, which is enough for a determinate progress bar.

Notes:

  • The scan skips meta/comment lines
  • Line strings have no trailing whitespace
  • Pass undefined for end to read to the end of the contig
  • A refName that is not in the index yields no lines and no error, so a chr1/1 naming mismatch looks like an empty region. Check against getReferenceSequenceNames if a query comes back unexpectedly empty
  • start > end throws a TypeError; start === end returns without reading

Without NPM (CDN)

<script src="https://unpkg.com/@gmod/tabix/dist/tabix-bundle.js"></script>

See example/index.html for a working demo. It fetches the VCF over HTTP, so serve the directory (e.g. npx serve example) rather than opening the file directly.

How a query flows

getLines turns a region into BGZF chunks through the index and decompresses each one in wasm — index reads included, since .tbi and .csi are bgzipped too. The rest is ordinary JS: it matches lines as bytes and decodes only the ones you asked for. docs/dataflow.md has the diagram and walks it through.

The file then holds on to those decompressed chunks, so overlapping and adjacent queries reuse them instead of inflating again — up to 1GB per file, dropped after three idle minutes. A consumer holding one file per track should bound them together with a shared chunkCacheBudget rather than shrinking each file's own ceiling: docs/caching.md.

Decompressing on a worker pool

BGZF blocks inflate independently, so that decompression can spread across threads.

import { getSharedWorkerPool } from '@gmod/bgzf-filehandle'

const file = new TabixIndexedFile({
  url: 'https://example.com/yourfile.vcf.gz',
  // the pending promise is fine — it is awaited at the point of use
  bgzfWorkerPool: getSharedWorkerPool(),
})

Safe to pass unconditionally: getSharedWorkerPool() returns undefined under node, or anywhere the host forbids Workers, which keeps the in-process path. No cross-origin isolation needed. tabix-js never creates a pool on its own — the thread budget belongs to the consumer.

Worth about 1.4x here, against the 1.95x a BAM reader reports. Measured in jbrowse-components on test/data/1kg.chr1.subset.vcf.gz — 213MB of 1000 Genomes, headless Chrome, real HTTP, four workers, arms interleaved, both returning the same record count: 1.34-1.46x across five window sizes and a twelve-step pan.

The decompression itself moves 1.83x. What holds the end-to-end figure below that is a 28% floor of per-line byte scanning and string decoding, which no worker count reaches — and that floor is at its worst on multi-sample VCF, whose records carry a genotype field per sample and run to ~60KB a line. A format with narrower lines sits closer to BAM. If you want more than ~1.5x on a multi-sample VCF, the scan is what is left to attack, not the decompression.

Worker counts, lifecycle and benchmarks: bgzf-filehandle's worker pool docs; the end-to-end numbers above, and how to confirm a pool is really engaging in production rather than quietly falling back, are in jbrowse-components' BGZF_WORKER_POOL.md.

Docs

Academic Use

Written with NHGRI funding as part of JBrowse. If you use this in a publication, please cite the most recent JBrowse paper at jbrowse.org.

License

MIT © Robert Buels

About

Read Tabix-indexed files, either with .tbi or .csi indexes, in node or the browser

Resources

Contributing

Stars

14 stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages