Skip to content

Commit

Permalink
Added variants and features GA4GH API
Browse files Browse the repository at this point in the history
  • Loading branch information
akmorrow13 committed Jul 12, 2017
1 parent 3859912 commit 0a8a47e
Show file tree
Hide file tree
Showing 35 changed files with 965 additions and 72 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[![Build Status](https://travis-ci.org/hammerlab/pileup.js.svg?branch=travis-flow)](https://travis-ci.org/hammerlab/pileup.js) [![Coverage Status](https://coveralls.io/repos/hammerlab/pileup.js/badge.svg?branch=master)](https://coveralls.io/r/hammerlab/pileup.js?branch=master) [![NPM version](http://img.shields.io/npm/v/pileup.svg)](https://www.npmjs.org/package/pileup) [![Dependency Status](https://david-dm.org/hammerlab/pileup.js.svg?theme=shields.io)](https://david-dm.org/hammerlab/pileup.js) [![devDependency Status](https://david-dm.org/hammerlab/pileup.js/dev-status.svg?theme=shields.io)](https://david-dm.org/hammerlab/pileup.js#info=devDependencies) [![DOI](https://zenodo.org/badge/8220/hammerlab/pileup.js.svg)](https://zenodo.org/badge/latestdoi/8220/hammerlab/pileup.js)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hammerlab/pileup.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hammerlab/pileup.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)

# pileup.js
pileup.js is an interactive in-browser track viewer. [**Try a demo**][demo]!
Expand Down Expand Up @@ -98,6 +98,8 @@ To play with the demo, start an [http-server][hs]:

Then open [http://localhost:8080/examples/index.html](http://localhost:8080/examples/index.html) in your browser of choice.

To view integration with GA4GH schemas, view [http://localhost:8080/examples/ga4gh-example.html](http://localhost:8080/examples/ga4gh-example.html).

## Testing

Run the tests from the command line:
Expand Down
74 changes: 74 additions & 0 deletions examples/data-ga4gh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Some data for the demo.

// We are going to use the same data source for multiple tracks
var bamSource = pileup.formats.bam({
url: '/test-data/synth3.normal.17.7500000-7515000.bam',
indexUrl: '/test-data/synth3.normal.17.7500000-7515000.bam.bai'
});

var sources = [
{
viz: pileup.viz.genome(),
isReference: true,
data: pileup.formats.twoBit({
url: 'http://www.biodalliance.org/datasets/hg19.2bit'
}),
name: 'Reference'
},
{
viz: pileup.viz.scale(),
name: 'Scale'
},
{
viz: pileup.viz.location(),
name: 'Location'
},
{
viz: pileup.viz.genes(),
data: pileup.formats.bigBed({
url: 'http://www.biodalliance.org/datasets/ensGene.bb'
}),
name: 'Genes'
},
{
viz: pileup.viz.variants(),
data: pileup.formats.GAVariant({
endpoint: 'http://1kgenomes.ga4gh.org',
variantSetId: "WyIxa2dlbm9tZXMiLCJ2cyIsInBoYXNlMy1yZWxlYXNlIl0",
callSetIds: ["WyIxa2dlbm9tZXMiLCJ2cyIsInBoYXNlMy1yZWxlYXNlIiwiSEcwMDA5NiJd"],
killChr: true
}),
options: {
onVariantClicked: function(data) {
var content = "Variants:\n";
for (var i =0;i< data.length;i++) {
content += `${data[i].id}: Ref: ${data[i].ref}, Alt: `;
data[i].alt.forEach(alt => {
content += `${alt} `;
})
content += '\n';
}
alert(content);
},
},
name: 'Variants'
},
{
viz: pileup.viz.features(),
data: pileup.formats.GAFeature({
endpoint: 'http://1kgenomes.ga4gh.org',
featureSetId: "WyIxa2dlbm9tZXMiLCJnZW5jb2RlX3YyNGxpZnQzNyJd",
}),
name: 'Features'
},
// { TODO
// viz: pileup.viz.pileup(),
// data: pileup.formats.GAReadAlignment({
// endpoint: 'http://1kgenomes.ga4gh.org',
// readGroupId: "WyIxa2dlbm9tZXMiLCJyZ3MiLCJIRzAzMjcwIiwiRVJSMTgxMzI5Il0",
// }),
// name: 'Alignments'
// }
];

var range = {contig: 'chr1', start: 120000, stop: 125000};
20 changes: 20 additions & 0 deletions examples/ga4gh-example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" href="../style/pileup.css" />
<link rel="stylesheet" href="demo.css" />
</head>

<body>
<button id="jiggle">FPS test</button>
<div id="pileup"></div>
</body>

<script src="../node_modules/stats.js/build/stats.min.js"></script>
<script src="../dist/pileup.js"></script>
<!-- or:
<script src="../dist/pileup.min.js"></script>
-->

<script src="data-ga4gh.js"></script>
<script src="playground.js"></script>
5 changes: 5 additions & 0 deletions src/main/ContigInterval.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ class ContigInterval<T: (number|string)> {
};
}

expand(size: number, zeroBased: boolean): ContigInterval<T> {
var newInterval = this.interval.expand(size, zeroBased);
return new ContigInterval(this.contig, newInterval.start, newInterval.stop);
}

// Comparator for use with Array.prototype.sort
static compare(a: ContigInterval, b: ContigInterval): number {
if (a.contig > b.contig) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/GA4GHAlignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class GA4GHAlignment /* implements Alignment */ {
}
}

// This is exposed as a static method to facilitate an optimization in GA4GHDataSource.
// This is exposed as a static method to facilitate an optimization in GA4GHAlignmentSource.
static keyFromGA4GHResponse(alignment: Object): string {
// this.alignment.id would be appealing here, but it's not actually unique!
return alignment.fragmentName + ':' + alignment.readNumber;
Expand Down
10 changes: 10 additions & 0 deletions src/main/Interval.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ class Interval {
return this.contains(other.start) && this.contains(other.stop);
}

// Expand range to begin and end on multiples of size
expand(size: number, zeroBased: boolean): Interval {
var minimum = zeroBased ? 0 : 1;
var roundDown = x => x - x % size;
var newStart = Math.max(minimum, roundDown(this.start)),
newStop = roundDown(this.stop + size - 1);

return new Interval(newStart, newStop);
}

clone(): Interval {
return new Interval(this.start, this.stop);
}
Expand Down
7 changes: 6 additions & 1 deletion src/main/RemoteRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import Q from 'q';
import ContigInterval from './ContigInterval';

var MONSTER_REQUEST = 5000000;

class RemoteRequest {
url: string;
cache: Object;
Expand Down Expand Up @@ -85,4 +87,7 @@ class RemoteRequest {
}
}

module.exports = RemoteRequest;
module.exports = {
RemoteRequest,
MONSTER_REQUEST: MONSTER_REQUEST
};
6 changes: 4 additions & 2 deletions src/main/data/vcf.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ function extractVariant(vcfLine: string): Variant {
}
}
}
var contig = parts[0];
var position = Number(parts[1]);

return {
contig: parts[0],
position: Number(parts[1]),
contig: contig,
position: position,
id: parts[2],
ref: parts[3],
alt: parts[4],
Expand Down
12 changes: 10 additions & 2 deletions src/main/pileup.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@ import TwoBitDataSource from './sources/TwoBitDataSource';
import BigBedDataSource from './sources/BigBedDataSource';
import VcfDataSource from './sources/VcfDataSource';
import BamDataSource from './sources/BamDataSource';
import GA4GHDataSource from './sources/GA4GHDataSource';
import EmptySource from './sources/EmptySource';

// Data sources from json
import GA4GHJson from './json/GA4GHJson';

// GA4GH sources
import GA4GHAlignmentSource from './sources/GA4GHAlignmentSource';
import GA4GHVariantSource from './sources/GA4GHVariantSource';
import GA4GHFeatureSource from './sources/GA4GHFeatureSource';

// Visualizations
import CoverageTrack from './viz/CoverageTrack';
import GenomeTrack from './viz/GenomeTrack';
import GeneTrack from './viz/GeneTrack';
import FeatureTrack from './viz/FeatureTrack';
import LocationTrack from './viz/LocationTrack';
import PileupTrack from './viz/PileupTrack';
import ScaleTrack from './viz/ScaleTrack';
Expand Down Expand Up @@ -165,17 +170,20 @@ var pileup = {
create: create,
formats: {
bam: BamDataSource.create,
ga4gh: GA4GHDataSource.create,
json: GA4GHJson.create,
vcf: VcfDataSource.create,
twoBit: TwoBitDataSource.create,
bigBed: BigBedDataSource.create,
GAReadAlignment: GA4GHAlignmentSource.create,
GAVariant: GA4GHVariantSource.create,
GAFeature: GA4GHFeatureSource.create,
empty: EmptySource.create
},
viz: {
coverage: makeVizObject(CoverageTrack),
genome: makeVizObject(GenomeTrack),
genes: makeVizObject(GeneTrack),
features: makeVizObject(FeatureTrack),
location: makeVizObject(LocationTrack),
scale: makeVizObject(ScaleTrack),
variants: makeVizObject(VariantTrack),
Expand Down
13 changes: 3 additions & 10 deletions src/main/sources/BamDataSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,7 @@ import type {Alignment, AlignmentDataSource} from '../Alignment';
// This reduces network activity while fetching.
// TODO: tune this value
var BASE_PAIRS_PER_FETCH = 100;

function expandRange(range: ContigInterval<string>) {
var roundDown = x => x - x % BASE_PAIRS_PER_FETCH;
var newStart = Math.max(1, roundDown(range.start())),
newStop = roundDown(range.stop() + BASE_PAIRS_PER_FETCH - 1);

return new ContigInterval(range.contig, newStart, newStop);
}
var ZERO_BASED = false;


function createFromBamFile(remoteSource: BamFile): AlignmentDataSource {
Expand Down Expand Up @@ -53,7 +46,7 @@ function createFromBamFile(remoteSource: BamFile): AlignmentDataSource {
}

function fetch(range: GenomeRange) {
var refsPromise = !_.isEmpty(contigNames) ? Q.when() :
var refsPromise = !_.isEmpty(contigNames) ? Q.when() :
remoteSource.header.then(saveContigMapping);

// For BAMs without index chunks, we need to fetch the entire BAI file
Expand All @@ -77,7 +70,7 @@ function createFromBamFile(remoteSource: BamFile): AlignmentDataSource {
return Q.when();
}

interval = expandRange(interval);
interval = interval.expand(BASE_PAIRS_PER_FETCH, ZERO_BASED);
var newRanges = interval.complementIntervals(coveredRanges);
coveredRanges.push(interval);
coveredRanges = ContigInterval.coalesce(coveredRanges);
Expand Down
24 changes: 21 additions & 3 deletions src/main/sources/BigBedDataSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,28 @@ export type Gene = {
name: string; // human-readable name, e.g. "TP53"
}

export type Feature = {
id: string;
featureType: string;
contig: string;
start: number;
stop: number;
score: number;
}

// Flow type for export.
export type FeatureDataSource = {
rangeChanged: (newRange: GenomeRange) => void;
getFeaturesInRange: (range: ContigInterval<string>) => Feature[];
on: (event: string, handler: Function) => void;
off: (event: string) => void;
trigger: (event: string, ...args:any) => void;
}

// Flow type for export.
export type BigBedSource = {
rangeChanged: (newRange: GenomeRange) => void;
getGenesInRange: (range: ContigInterval<string>) => Gene[];
getFeaturesInRange: (range: ContigInterval<string>) => Gene[];
on: (event: string, handler: Function) => void;
off: (event: string) => void;
trigger: (event: string, ...args:any) => void;
Expand Down Expand Up @@ -68,7 +86,7 @@ function createFromBigBedFile(remoteSource: BigBed): BigBedSource {
}
}

function getGenesInRange(range: ContigInterval<string>): Gene[] {
function getFeaturesInRange(range: ContigInterval<string>): Gene[] {
if (!range) return [];
var results = [];
_.each(genes, gene => {
Expand Down Expand Up @@ -106,7 +124,7 @@ function createFromBigBedFile(remoteSource: BigBed): BigBedSource {
rangeChanged: function(newRange: GenomeRange) {
fetch(newRange).done();
},
getGenesInRange,
getFeaturesInRange,

// These are here to make Flow happy.
on: () => {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ContigInterval from '../ContigInterval';
import GA4GHAlignment from '../GA4GHAlignment';

var ALIGNMENTS_PER_REQUEST = 200; // TODO: explain this choice.
var ZERO_BASED = false;


// Genome ranges are rounded to multiples of this for fetching.
Expand All @@ -23,14 +24,6 @@ var ALIGNMENTS_PER_REQUEST = 200; // TODO: explain this choice.
// bulkier requests.
var BASE_PAIRS_PER_FETCH = 100;

function expandRange(range: ContigInterval<string>) {
var roundDown = x => x - x % BASE_PAIRS_PER_FETCH;
var newStart = Math.max(1, roundDown(range.start())),
newStop = roundDown(range.stop() + BASE_PAIRS_PER_FETCH - 1);

return new ContigInterval(range.contig, newStart, newStop);
}

type GA4GHSpec = {
endpoint: string;
readGroupId: string;
Expand All @@ -40,10 +33,6 @@ type GA4GHSpec = {
};

function create(spec: GA4GHSpec): AlignmentDataSource {
if (spec.endpoint.slice(-6) != 'v0.5.1') {
throw new Error('Only v0.5.1 of the GA4GH API is supported by pileup.js');
}

var url = spec.endpoint + '/reads/search';

var reads: {[key:string]: Alignment} = {};
Expand All @@ -52,6 +41,9 @@ function create(spec: GA4GHSpec): AlignmentDataSource {
var coveredRanges: ContigInterval<string>[] = [];

function addReadsFromResponse(response: Object) {
if (response.alignments == undefined) {
return;
}
response.alignments.forEach(alignment => {
// optimization: don't bother constructing a GA4GHAlignment unless it's new.
var key = GA4GHAlignment.keyFromGA4GHResponse(alignment);
Expand All @@ -68,7 +60,7 @@ function create(spec: GA4GHSpec): AlignmentDataSource {
var interval = new ContigInterval(contig, newRange.start, newRange.stop);
if (interval.isCoveredBy(coveredRanges)) return;

interval = expandRange(interval);
interval = interval.expand(BASE_PAIRS_PER_FETCH, ZERO_BASED);

// select only intervals not yet loaded into coveredRangesß
var intervals = interval.complementIntervals(coveredRanges);
Expand Down
Loading

0 comments on commit 0a8a47e

Please sign in to comment.