From 2a45d8bffe7d208827217e2f28945a9da535f413 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Tue, 9 Jun 2020 16:17:33 -0400 Subject: [PATCH 01/13] change apply filters dialog --- .../js/View/Track/AppliedFiltersEditor.js | 172 ++++++++++++++++++ gdc-viewer/js/View/Track/CommonTrack.js | 123 +------------ 2 files changed, 182 insertions(+), 113 deletions(-) create mode 100644 gdc-viewer/js/View/Track/AppliedFiltersEditor.js diff --git a/gdc-viewer/js/View/Track/AppliedFiltersEditor.js b/gdc-viewer/js/View/Track/AppliedFiltersEditor.js new file mode 100644 index 0000000..574a7a8 --- /dev/null +++ b/gdc-viewer/js/View/Track/AppliedFiltersEditor.js @@ -0,0 +1,172 @@ +/** + * Create dialog showing applied filters for the given track. + * User can apply new filters here too. + */ +define([ + 'dojo/_base/declare', + 'dojo/aspect', + 'dojo/json', + 'dojo/dom-construct', + 'dijit/Dialog', + 'dijit/form/Button', + 'dijit/form/NumberSpinner', + 'dijit/form/ValidationTextBox', + './ValidationTextArea', +], +function( + declare, + aspect, + JSON, + dom, + Dialog, + Button, + NumberSpinner, + ValidationTextBox, + ValidationTextArea +) { + +return declare( null, { + + constructor: function( track ) { + this.track = track; + }, + + show: function( editCallback, cancelCallback ) { + var dialog = this.dialog = new Dialog( + { title: "Track Filters", className: 'appliedFiltersEditor' } + ); + + var content = [ + this._makeEditControls().domNode + ]; + dialog.set( 'content', content ); + dialog.show(); + + aspect.after( dialog, 'hide', dojo.hitch( this, function() { + setTimeout( function() { + dialog.destroyRecursive(); + }, 500 ); + })); + }, + + _makeEditControls: function() { + var details = dom.create('div', { className: 'detail', style: 'display: flex; flex-direction: column; align-items: center; justify-content: center;' }); + + // Create header text + var headerString = '

View and Update Filters Applied To The Current Track

'; + var headerElement = dom.toDom(headerString); + dom.place(headerElement, details); + + // Create filter help text + var helpString = 'The following filters have been applied to the track. You can update the filters here, though only basic validation is done on the input.'; + var helpElement = dom.toDom(helpString); + dom.place(helpElement, details); + + var filterString = '

Filters

Filters narrow down the features displayed on the track. We use the same format as the GDC GraphQL API.
'; + var filterElement = dom.toDom(filterString); + dom.place(filterElement, details); + + // Display filtered text + var filteredText = JSON.stringify(this.track.store.filters, null, 2) + + var textArea = new ValidationTextArea({ + rows: 20, + value: filteredText, + style: "width: 80%", + readOnly: false, + id: "filterTextArea", + invalidMessage: "Invalid JSON filter - must be a valid JSON", + isValid: function() { + var value = this.attr('value') + try { + JSON.parse(value) + } catch (e) { + return false; + } + return true; + } + }).placeAt(details); + + // Display cases + var caseString = '

Case UUIDs

Comma separated unique identifiers for the case, expressed as UUIDs.
'; + var caseElement = dom.toDom(caseString); + dom.place(caseElement, details); + + var caseIdTextBox = new ValidationTextBox({ + value: this.track.store.config.case, + style: "width: 80%", + id: "caseTextBox", + regExp: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(,[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})*$", + invalidMessage: "Invalid Case UUID - Must be version 4 UUID", + trim: true + }).placeAt(details); + + // Display size + var sizeHeader = '

Size

This is the maximum number of results to return per panel.
'; + var sizeElement = dom.toDom(sizeHeader); + dom.place(sizeElement, details); + + var sizeTextBox = new NumberSpinner({ + value: this.track.store.size, + style: "width: 80%", + id: "sizeTextBox", + constraints: { min: 1, max: 2000, places: 0 }, + smallDelta: 10 + }).placeAt(details); + + var actionBar = dom.create( + 'div', { + className: 'dijitDialogPaneActionBar' + }, details); + + + var closeButton = new Button({ + iconClass: 'dijitIconDelete', + label: 'Cancel', + onClick: dojo.hitch( this, function() { + this.dialog.hide(); + }) + }) + .placeAt( actionBar ); + + // Add button to update the track with changed content + var updateTrackButton = new Button({ + label: 'Apply Filters', + iconClass: 'dijitIconSave', + onClick: dojo.hitch( this, function() { + const trackString = document.getElementById("filterTextArea").value; + const caseString = document.getElementById("caseTextBox").value; + const sizeString = document.getElementById("sizeTextBox").value; + var storeConf = { + browser: this.track.browser, + refSeq: this.track.browser.refSeq, + type: this.track.store.config.type, + case: caseString, + size: sizeString, + filters: trackString + }; + var storeName = this.track.browser.addStoreConfig(null, storeConf); + this.track.config.store = storeName; + this.track.browser.publish( '/jbrowse/v1/v/tracks/replace', [this.track.config] ); + this.dialog.hide(); + }) + }).placeAt(actionBar); + + // Change listeners for text boxes + caseIdTextBox.on('change', function(e) { + updateTrackButton.set('disabled', !caseIdTextBox.validate() || !sizeTextBox.validate() || !textArea.validate()) + }); + + sizeTextBox.on('change', function(e) { + updateTrackButton.set('disabled', !caseIdTextBox.validate() || !sizeTextBox.validate() || !textArea.validate()) + }); + + textArea.on('change', function(e) { + updateTrackButton.set('disabled', !caseIdTextBox.validate() || !sizeTextBox.validate() || !textArea.validate()) + }); + + return { domNode: details }; + }, + +}); +}); diff --git a/gdc-viewer/js/View/Track/CommonTrack.js b/gdc-viewer/js/View/Track/CommonTrack.js index 4108ebb..93838ef 100644 --- a/gdc-viewer/js/View/Track/CommonTrack.js +++ b/gdc-viewer/js/View/Track/CommonTrack.js @@ -8,7 +8,8 @@ define( 'dijit/form/Button', 'dijit/form/NumberSpinner', 'dijit/form/ValidationTextBox', - './ValidationTextArea' + './ValidationTextArea', + './AppliedFiltersEditor' ], function( declare, @@ -16,7 +17,8 @@ define( Button, NumberSpinner, ValidationTextBox, - ValidationTextArea) { + ValidationTextArea, + AppliedFiltersEditor) { return declare(null, { /** @@ -31,121 +33,16 @@ define( title: 'Share Track as URL', content: dojo.hitch(this,'_shareableLinkContent') }); + var track = this; options.push({ label: 'View Applied Filters', - action: "contentDialog", title: 'View Applied Filters', - content: dojo.hitch(this,'_appliedFilters') - }); - return options; - }, - - /** - * Create dialog showing applied filters for the given track. - * User can apply new filters here too. - */ - _appliedFilters: function() { - var track = this; - var details = domConstruct.create('div', { className: 'detail', style: 'display: flex; flex-direction: column; align-items: center; justify-content: center;' }); - - // Create header text - var headerString = '

View and Update Filters Applied To The Current Track

'; - var headerElement = domConstruct.toDom(headerString); - domConstruct.place(headerElement, details); - - // Create filter help text - var helpString = 'The following filters have been applied to the track. You can update the filters here, though only basic validation is done on the input.'; - var helpElement = domConstruct.toDom(helpString); - domConstruct.place(helpElement, details); - - var filterString = '

Filters

Filters narrow down the features displayed on the track. We use the same format as the GDC GraphQL API.
'; - var filterElement = domConstruct.toDom(filterString); - domConstruct.place(filterElement, details); - - // Display filtered text - var filteredText = JSON.stringify(track.store.filters, null, 2) - - var textArea = new ValidationTextArea({ - rows: 20, - value: filteredText, - style: "width: 80%", - readOnly: false, - id: "filterTextArea", - invalidMessage: "Invalid JSON filter - must be a valid JSON", - isValid: function() { - var value = this.attr('value') - try { - JSON.parse(value) - } catch (e) { - return false; - } - return true; - } - }).placeAt(details); - - // Display cases - var caseString = '

Case UUIDs

Comma separated unique identifiers for the case, expressed as UUIDs.
'; - var caseElement = domConstruct.toDom(caseString); - domConstruct.place(caseElement, details); - - var caseIdTextBox = new ValidationTextBox({ - value: track.store.config.case, - style: "width: 80%", - id: "caseTextBox", - regExp: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(,[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})*$", - invalidMessage: "Invalid Case UUID - Must be version 4 UUID", - trim: true - }).placeAt(details); - - // Display size - var sizeHeader = '

Size

This is the maximum number of results to return per panel.
'; - var sizeElement = domConstruct.toDom(sizeHeader); - domConstruct.place(sizeElement, details); - - var sizeTextBox = new NumberSpinner({ - value: track.store.size, - style: "width: 80%", - id: "sizeTextBox", - constraints: { min: 1, max: 2000, places: 0 }, - smallDelta: 10 - }).placeAt(details); - - // Add button to update the track with changed content - var updateTrackButton = new Button({ - label: 'Apply Filters', - iconClass: 'dijitIconSave', - onClick: function() { - const trackString = document.getElementById("filterTextArea").value; - const caseString = document.getElementById("caseTextBox").value; - const sizeString = document.getElementById("sizeTextBox").value; - var storeConf = { - browser: track.browser, - refSeq: track.browser.refSeq, - type: track.store.config.type, - case: caseString, - size: sizeString, - filters: trackString - }; - var storeName = track.browser.addStoreConfig(null, storeConf); - track.config.store = storeName; - track.browser.publish( '/jbrowse/v1/v/tracks/replace', [track.config] ); + action: function() { + new AppliedFiltersEditor(track) + .show(); } - }).placeAt(details); - - // Change listeners for text boxes - caseIdTextBox.on('change', function(e) { - updateTrackButton.set('disabled', !caseIdTextBox.validate() || !sizeTextBox.validate() || !textArea.validate()) - }); - - sizeTextBox.on('change', function(e) { - updateTrackButton.set('disabled', !caseIdTextBox.validate() || !sizeTextBox.validate() || !textArea.validate()) - }); - - textArea.on('change', function(e) { - updateTrackButton.set('disabled', !caseIdTextBox.validate() || !sizeTextBox.validate() || !textArea.validate()) - }); - - return details; + }) + return options; }, /** From 316b8d3e9eed8bd20f2ddbfa4094e7005a8acba2 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Tue, 9 Jun 2020 16:18:32 -0400 Subject: [PATCH 02/13] change apply filters to apply --- gdc-viewer/js/View/Track/AppliedFiltersEditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdc-viewer/js/View/Track/AppliedFiltersEditor.js b/gdc-viewer/js/View/Track/AppliedFiltersEditor.js index 574a7a8..684fb1f 100644 --- a/gdc-viewer/js/View/Track/AppliedFiltersEditor.js +++ b/gdc-viewer/js/View/Track/AppliedFiltersEditor.js @@ -131,7 +131,7 @@ return declare( null, { // Add button to update the track with changed content var updateTrackButton = new Button({ - label: 'Apply Filters', + label: 'Apply', iconClass: 'dijitIconSave', onClick: dojo.hitch( this, function() { const trackString = document.getElementById("filterTextArea").value; From 872cbb931bc38d2570d112fe688ed008d5e87974 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Tue, 9 Jun 2020 16:29:04 -0400 Subject: [PATCH 03/13] removed unused dependencies --- gdc-viewer/js/View/Track/CommonTrack.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gdc-viewer/js/View/Track/CommonTrack.js b/gdc-viewer/js/View/Track/CommonTrack.js index 93838ef..ec93eb5 100644 --- a/gdc-viewer/js/View/Track/CommonTrack.js +++ b/gdc-viewer/js/View/Track/CommonTrack.js @@ -6,18 +6,12 @@ define( "dojo/_base/declare", 'dojo/dom-construct', 'dijit/form/Button', - 'dijit/form/NumberSpinner', - 'dijit/form/ValidationTextBox', - './ValidationTextArea', './AppliedFiltersEditor' ], function( declare, domConstruct, Button, - NumberSpinner, - ValidationTextBox, - ValidationTextArea, AppliedFiltersEditor) { return declare(null, { From b01c92f608c348a2375b4a0fe9899b71f6cc78d6 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Tue, 23 Jun 2020 14:11:34 -0400 Subject: [PATCH 04/13] basic IEQ implementation --- .../IsoformExpressionQuantification.js | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js diff --git a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js new file mode 100644 index 0000000..41804ec --- /dev/null +++ b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js @@ -0,0 +1,115 @@ +/** + * Base class for some Store SeqFeature for GDC + */ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + 'dojo/_base/lang', + 'JBrowse/Store/SeqFeature/BED', + 'JBrowse/Store/SeqFeature/BED/Parser', + 'JBrowse/Store/SeqFeature', + 'JBrowse/Model/XHRBlob', + 'JBrowse/Model/BlobFilehandleWrapper' + +], +function ( + declare, + array, + lang, + BED, + Parser, + SeqFeatureStore, + XHRBlob, + BlobFilehandleWrapper +) { + return declare([ SeqFeatureStore, BED ], { + downloadUrl: 'https://api.gdc.cancer.gov/data/', + + /** + * Constructor + * @param {*} args + */ + constructor: function (args) { + // Unique file ID + this.fileId = args.fileId; + + this.data = new BlobFilehandleWrapper( + new XHRBlob( + this.resolveUrl( + this.downloadUrl + this.fileId + ) + ) + ); + }, + + _loadFeatures: function () { + var thisB = this; + var features = this.bareFeatures = []; + + var parser = new Parser( + { + featureCallback: function (fs) { + array.forEach(fs, function (feature) { + console.log(feature) + if (thisB.config.featureCallback) { + features.push(thisB.config.featureCallback(feature, thisB)); + } else { + features.push(feature); + } + }); + }, + endCallback: function () { + thisB._estimateGlobalStats() + .then(function (stats) { + thisB.globalStats = stats; + thisB._deferred.stats.resolve(); + }); + + thisB._deferred.features.resolve(features); + }, + commentCallback: (this.config.commentCallback || function (i) { }), + store: this + }); + + var fail = lang.hitch(this, '_failAllDeferred'); + // parse the whole file and store it + let i = 0; + this.data.fetchLines( + function (line) { + try { + if (i !== 0) { + splitLine = line.split('\t') + // Second column contains location in the format refseq:chr:start-end:strand + // This needs to be redistributed to form BED format + featureLocation = splitLine[1] + splitLocation = featureLocation.split(':') + splitPos = splitLocation[2].split('-') + locationArray = [splitLocation[1], splitPos[0], splitPos[1]] + splitLine.splice(1, 1) + splitLine = locationArray.concat(splitLine) + splitLine.splice(5, 0, splitLocation[3]) + if (i > 0) { + parser.addLine(splitLine.join('\t')); + } + } + i++; + } catch (e) { + fail('Error parsing IsoFormExpressionQuantification.', e); + throw e; + } + }, + lang.hitch(parser, 'finish'), + fail + ); + }, + + /** + * Stub for getParser + */ + getParser: function() { + return new Promise(function(resolve, reject) { + resolve({'getMetadata': function() {}}); + }); + }, + }); +}); \ No newline at end of file From e2f76470a5c07f4ebe59e9658a253265c15b7910 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Tue, 23 Jun 2020 15:57:30 -0400 Subject: [PATCH 05/13] fix IEQ track --- .../IsoformExpressionQuantification.js | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js index 41804ec..28d5c5e 100644 --- a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js +++ b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js @@ -46,11 +46,19 @@ function ( var thisB = this; var features = this.bareFeatures = []; + var featuresSorted = true; + var seenRefs = this.refSeqs = {}; var parser = new Parser( { featureCallback: function (fs) { array.forEach(fs, function (feature) { - console.log(feature) + var prevFeature = features[ features.length - 1 ]; + var regRefName = thisB.browser.regularizeReferenceName(feature.seq_id); + if (regRefName in seenRefs && prevFeature && prevFeature.seq_id != feature.seq_id) {featuresSorted = false;} + if (prevFeature && prevFeature.seq_id == feature.seq_id && feature.start < prevFeature.start) {featuresSorted = false;} + + if (!(regRefName in seenRefs)) {seenRefs[ regRefName ] = features.length;} + if (thisB.config.featureCallback) { features.push(thisB.config.featureCallback(feature, thisB)); } else { @@ -59,6 +67,12 @@ function ( }); }, endCallback: function () { + if (!featuresSorted) { + features.sort(thisB._compareFeatureData); + // need to rebuild the refseq index if changing the sort order + thisB._rebuildRefSeqs(features); + } + thisB._estimateGlobalStats() .then(function (stats) { thisB.globalStats = stats; @@ -80,11 +94,11 @@ function ( if (i !== 0) { splitLine = line.split('\t') // Second column contains location in the format refseq:chr:start-end:strand - // This needs to be redistributed to form BED format + // This needs to be redistributed to form BED format featureLocation = splitLine[1] splitLocation = featureLocation.split(':') splitPos = splitLocation[2].split('-') - locationArray = [splitLocation[1], splitPos[0], splitPos[1]] + locationArray = [splitLocation[1].replace('chr', ''), splitPos[0], splitPos[1]] splitLine.splice(1, 1) splitLine = locationArray.concat(splitLine) splitLine.splice(5, 0, splitLocation[3]) From 23598bbad82e63b2f7586eec6965c13c5fdcd4fe Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Thu, 25 Jun 2020 10:22:24 -0400 Subject: [PATCH 06/13] use base class for bed like files --- .../js/Store/SeqFeature/BaseBEDLikeFeature.js | 116 +++++++++++++++ .../IsoformExpressionQuantification.js | 134 +++--------------- 2 files changed, 134 insertions(+), 116 deletions(-) create mode 100644 gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js diff --git a/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js b/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js new file mode 100644 index 0000000..fcdf488 --- /dev/null +++ b/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js @@ -0,0 +1,116 @@ +/** + * Base class for some Store SeqFeature for GDC + */ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + 'dojo/_base/lang', + 'JBrowse/Store/SeqFeature/BED', + 'JBrowse/Store/SeqFeature/BED/Parser', + 'JBrowse/Store/SeqFeature', + 'JBrowse/Model/XHRBlob', + 'JBrowse/Model/BlobFilehandleWrapper' +], +function( + declare, + array, + lang, + BED, + Parser, + SeqFeatureStore, + XHRBlob, + BlobFilehandleWrapper +) { + return declare([ SeqFeatureStore, BED ], { + downloadUrl: 'https://api.gdc.cancer.gov/data/', + + /** + * Constructor + * @param {*} args + */ + constructor: function (args) { + // Unique file ID + this.fileId = args.fileId; + + this.data = new BlobFilehandleWrapper( + new XHRBlob( + this.resolveUrl( + this.downloadUrl + this.fileId + ) + ) + ); + }, + + _loadFeatures: function () { + var thisB = this; + var features = this.bareFeatures = []; + + var featuresSorted = true; + var seenRefs = this.refSeqs = {}; + var parser = new Parser( + { + featureCallback: function (fs) { + array.forEach(fs, function (feature) { + var prevFeature = features[ features.length - 1 ]; + var regRefName = thisB.browser.regularizeReferenceName(feature.seq_id); + if (regRefName in seenRefs && prevFeature && prevFeature.seq_id != feature.seq_id) {featuresSorted = false;} + if (prevFeature && prevFeature.seq_id == feature.seq_id && feature.start < prevFeature.start) {featuresSorted = false;} + + if (!(regRefName in seenRefs)) {seenRefs[ regRefName ] = features.length;} + + if (thisB.config.featureCallback) { + features.push(thisB.config.featureCallback(feature, thisB)); + } else { + features.push(feature); + } + }); + }, + endCallback: function () { + if (!featuresSorted) { + features.sort(thisB._compareFeatureData); + // need to rebuild the refseq index if changing the sort order + thisB._rebuildRefSeqs(features); + } + + thisB._estimateGlobalStats() + .then(function (stats) { + thisB.globalStats = stats; + thisB._deferred.stats.resolve(); + }); + + thisB._deferred.features.resolve(features); + }, + commentCallback: (this.config.commentCallback || function (i) { }), + store: this + }); + + var fail = lang.hitch(this, '_failAllDeferred'); + // parse the whole file and store it + let i = 0; + this.data.fetchLines( + function (line) { + try { + if (i > 0) { + parser.addLine(thisB.convertLineToBED(line)); + } + i++; + } catch (e) { + fail('Error parsing ' + thisB.getName(), e); + throw e; + } + }, + lang.hitch(parser, 'finish'), + fail + ); + }, + + /** + * Stub for getParser + */ + getParser: function() { + return new Promise(function(resolve, reject) { + resolve({'getMetadata': function() {}}); + }); + }, + }); +}); \ No newline at end of file diff --git a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js index 28d5c5e..0621b7f 100644 --- a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js +++ b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js @@ -3,127 +3,29 @@ */ define([ 'dojo/_base/declare', - 'dojo/_base/array', - 'dojo/_base/lang', - 'JBrowse/Store/SeqFeature/BED', - 'JBrowse/Store/SeqFeature/BED/Parser', - 'JBrowse/Store/SeqFeature', - 'JBrowse/Model/XHRBlob', - 'JBrowse/Model/BlobFilehandleWrapper' - + './BaseBEDLikeFeature', ], function ( declare, - array, - lang, - BED, - Parser, - SeqFeatureStore, - XHRBlob, - BlobFilehandleWrapper + BaseBEDLikeFeature ) { - return declare([ SeqFeatureStore, BED ], { - downloadUrl: 'https://api.gdc.cancer.gov/data/', - - /** - * Constructor - * @param {*} args - */ - constructor: function (args) { - // Unique file ID - this.fileId = args.fileId; - - this.data = new BlobFilehandleWrapper( - new XHRBlob( - this.resolveUrl( - this.downloadUrl + this.fileId - ) - ) - ); + return declare(BaseBEDLikeFeature, { + convertLineToBED: function(line) { + splitLine = line.split('\t') + // Second column contains location in the format refseq:chr:start-end:strand + // This needs to be redistributed to form BED format + featureLocation = splitLine[1] + splitLocation = featureLocation.split(':') + splitPos = splitLocation[2].split('-') + locationArray = [splitLocation[1].replace('chr', ''), splitPos[0], splitPos[1]] + splitLine.splice(1, 1) + splitLine = locationArray.concat(splitLine) + splitLine.splice(5, 0, splitLocation[3]) + return splitLine.join('\t') }, - _loadFeatures: function () { - var thisB = this; - var features = this.bareFeatures = []; - - var featuresSorted = true; - var seenRefs = this.refSeqs = {}; - var parser = new Parser( - { - featureCallback: function (fs) { - array.forEach(fs, function (feature) { - var prevFeature = features[ features.length - 1 ]; - var regRefName = thisB.browser.regularizeReferenceName(feature.seq_id); - if (regRefName in seenRefs && prevFeature && prevFeature.seq_id != feature.seq_id) {featuresSorted = false;} - if (prevFeature && prevFeature.seq_id == feature.seq_id && feature.start < prevFeature.start) {featuresSorted = false;} - - if (!(regRefName in seenRefs)) {seenRefs[ regRefName ] = features.length;} - - if (thisB.config.featureCallback) { - features.push(thisB.config.featureCallback(feature, thisB)); - } else { - features.push(feature); - } - }); - }, - endCallback: function () { - if (!featuresSorted) { - features.sort(thisB._compareFeatureData); - // need to rebuild the refseq index if changing the sort order - thisB._rebuildRefSeqs(features); - } - - thisB._estimateGlobalStats() - .then(function (stats) { - thisB.globalStats = stats; - thisB._deferred.stats.resolve(); - }); - - thisB._deferred.features.resolve(features); - }, - commentCallback: (this.config.commentCallback || function (i) { }), - store: this - }); - - var fail = lang.hitch(this, '_failAllDeferred'); - // parse the whole file and store it - let i = 0; - this.data.fetchLines( - function (line) { - try { - if (i !== 0) { - splitLine = line.split('\t') - // Second column contains location in the format refseq:chr:start-end:strand - // This needs to be redistributed to form BED format - featureLocation = splitLine[1] - splitLocation = featureLocation.split(':') - splitPos = splitLocation[2].split('-') - locationArray = [splitLocation[1].replace('chr', ''), splitPos[0], splitPos[1]] - splitLine.splice(1, 1) - splitLine = locationArray.concat(splitLine) - splitLine.splice(5, 0, splitLocation[3]) - if (i > 0) { - parser.addLine(splitLine.join('\t')); - } - } - i++; - } catch (e) { - fail('Error parsing IsoFormExpressionQuantification.', e); - throw e; - } - }, - lang.hitch(parser, 'finish'), - fail - ); - }, - - /** - * Stub for getParser - */ - getParser: function() { - return new Promise(function(resolve, reject) { - resolve({'getMetadata': function() {}}); - }); - }, + getName: function() { + return 'IsoformExpressionQuantification' + } }); }); \ No newline at end of file From 8d5e0907df18e65c3fcec2c824c18a7e2cf509c7 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Wed, 8 Jul 2020 12:39:06 -0400 Subject: [PATCH 07/13] support storing additional metadata for features --- .../js/Store/SeqFeature/BaseBEDLikeFeature.js | 8 ++- .../js/Store/SeqFeature/BedLikeParser.js | 59 +++++++++++++++++++ .../IsoformExpressionQuantification.js | 35 +++++++---- 3 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 gdc-viewer/js/Store/SeqFeature/BedLikeParser.js diff --git a/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js b/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js index fcdf488..294e089 100644 --- a/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js +++ b/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js @@ -1,12 +1,12 @@ /** - * Base class for some Store SeqFeature for GDC + * Base class for BEDlike files from the GDC */ define([ 'dojo/_base/declare', 'dojo/_base/array', 'dojo/_base/lang', 'JBrowse/Store/SeqFeature/BED', - 'JBrowse/Store/SeqFeature/BED/Parser', + './BedLikeParser', 'JBrowse/Store/SeqFeature', 'JBrowse/Model/XHRBlob', 'JBrowse/Model/BlobFilehandleWrapper' @@ -90,7 +90,9 @@ function( this.data.fetchLines( function (line) { try { - if (i > 0) { + if (i == 0) { + parser.setExtraFields(line) + } else { parser.addLine(thisB.convertLineToBED(line)); } i++; diff --git a/gdc-viewer/js/Store/SeqFeature/BedLikeParser.js b/gdc-viewer/js/Store/SeqFeature/BedLikeParser.js new file mode 100644 index 0000000..7dbfc22 --- /dev/null +++ b/gdc-viewer/js/Store/SeqFeature/BedLikeParser.js @@ -0,0 +1,59 @@ +define([ + 'dojo/_base/declare', + 'dojo/_base/array', + 'JBrowse/Store/SeqFeature/BED/Parser', +], +function ( + declare, + array, + Parser +) { + var bed_feature_names = 'seq_id start end name score strand'.split(" "); + var extra_feature_names = [] + + return declare(Parser, { + setExtraFields: function(extraFields) { + extra_feature_names = extraFields.trim().split('\t') + }, + + parse_feature: function( line ) { + var f = array.map( line.split("\t"), function(a) { + if( a == '.' ) { + return null; + } + return a; + }); + + // unescape only the ref and source columns + f[0] = this.unescape( f[0] ); + + var parsed = {}; + const combinedFeatures = bed_feature_names.concat(extra_feature_names) + for( var i = 0; i < combinedFeatures.length; i++ ) { + if(f[i]) { + parsed[ combinedFeatures[i].toLowerCase() ] = f[i] == '.' ? null : f[i]; + } + } + + if( parsed.start !== null ) + parsed.start = parseInt( parsed.start, 10 ); + if( parsed.end !== null ) + parsed.end = parseInt( parsed.end, 10 ); + if( parsed.score != null ) + parsed.score = parseFloat( parsed.score, 10 ); + + parsed.strand = {'+':1,'-':-1}[parsed.strand] || 0; + + return parsed; + }, + + unescape(s) { + if( s === null ) + return null; + + return s.replace( /%([0-9A-Fa-f]{2})/g, function( match, seq ) { + return String.fromCharCode( parseInt( seq, 16 ) ); + }); + }, + }); +}); \ No newline at end of file diff --git a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js index 0621b7f..f16b26b 100644 --- a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js +++ b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js @@ -1,5 +1,10 @@ /** - * Base class for some Store SeqFeature for GDC + * IsoformExpressionQuantification store class + * See https://docs.gdc.cancer.gov/Data/Bioinformatics_Pipelines/miRNA_Pipeline/#mirna-expression-workflow + * + * Supports (Data format, Data category, Data Type) + * * TSV, Transcriptome Profiling, Isoform Expression Quantification + * * TXT, Transcriptome Profiling, Isoform Expression Quantification */ define([ 'dojo/_base/declare', @@ -11,21 +16,27 @@ function ( ) { return declare(BaseBEDLikeFeature, { convertLineToBED: function(line) { - splitLine = line.split('\t') + originalLine = line.split('\t'); // Second column contains location in the format refseq:chr:start-end:strand // This needs to be redistributed to form BED format - featureLocation = splitLine[1] - splitLocation = featureLocation.split(':') - splitPos = splitLocation[2].split('-') - locationArray = [splitLocation[1].replace('chr', ''), splitPos[0], splitPos[1]] - splitLine.splice(1, 1) - splitLine = locationArray.concat(splitLine) - splitLine.splice(5, 0, splitLocation[3]) - return splitLine.join('\t') + // Create BED line + featureLocation = originalLine[1]; + splitLocation = featureLocation.split(':'); + splitPos = splitLocation[2].split('-'); + locationArray = [splitLocation[1].replace('chr', ''), splitPos[0], splitPos[1]]; + + bedLikeLine = locationArray; + bedLikeLine.push(originalLine[0]) // name + bedLikeLine.push(originalLine[2]) // score + bedLikeLine.push(splitLocation[3]) // strand + + // Combine BED line and existing line + fullLine = bedLikeLine.concat(originalLine) + return fullLine.join('\t'); }, getName: function() { - return 'IsoformExpressionQuantification' - } + return 'IsoformExpressionQuantification'; + }, }); }); \ No newline at end of file From a97f7558f4e72b63e1b91a041bd8f17874cda91f Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Wed, 8 Jul 2020 12:39:21 -0400 Subject: [PATCH 08/13] add seg support --- gdc-viewer/js/Store/SeqFeature/SEG.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 gdc-viewer/js/Store/SeqFeature/SEG.js diff --git a/gdc-viewer/js/Store/SeqFeature/SEG.js b/gdc-viewer/js/Store/SeqFeature/SEG.js new file mode 100644 index 0000000..34d0f2f --- /dev/null +++ b/gdc-viewer/js/Store/SeqFeature/SEG.js @@ -0,0 +1,25 @@ +/** + * SEG store class for CopyNumberSegment files + * Supports (Data format, Data category, Data Type) + * * TXT, Copy number variation, Copy number Segment + * * TXT, Copy number variation, Masked copy number Segment + * * TXT, Copy number variation, Allele-specific copy number Segment + */ +define([ + 'dojo/_base/declare', + './BaseBEDLikeFeature', +], +function ( + declare, + BaseBEDLikeFeature +) { + return declare(BaseBEDLikeFeature, { + convertLineToBED: function(line) { + return line = line.slice(line.indexOf('\t') + 1); + }, + + getName: function() { + return 'SEG' + } + }); +}); \ No newline at end of file From ff665788d3ce6145426f9bbee5f77f55924f58f5 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Wed, 8 Jul 2020 13:11:47 -0400 Subject: [PATCH 09/13] add some clarifying comments --- gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js | 1 + .../js/Store/SeqFeature/IsoformExpressionQuantification.js | 1 + 2 files changed, 2 insertions(+) diff --git a/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js b/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js index 294e089..f4dfe50 100644 --- a/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js +++ b/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js @@ -93,6 +93,7 @@ function( if (i == 0) { parser.setExtraFields(line) } else { + // Creates a line where the first columns are BED format and the remaining are extra metadata parser.addLine(thisB.convertLineToBED(line)); } i++; diff --git a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js index f16b26b..5a987f6 100644 --- a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js +++ b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js @@ -15,6 +15,7 @@ function ( BaseBEDLikeFeature ) { return declare(BaseBEDLikeFeature, { + // Converts a non-BED file line to BED, and appends the original line to the end convertLineToBED: function(line) { originalLine = line.split('\t'); // Second column contains location in the format refseq:chr:start-end:strand From ed21e57ff9e44f156037cd88eccbff8fbe3258b3 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Wed, 8 Jul 2020 14:52:03 -0400 Subject: [PATCH 10/13] use urlTemplate --- gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js | 9 ++------- gdc-viewer/js/Store/SeqFeature/BedLikeParser.js | 2 +- .../Store/SeqFeature/IsoformExpressionQuantification.js | 2 +- gdc-viewer/js/Store/SeqFeature/SEG.js | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js b/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js index f4dfe50..6379e07 100644 --- a/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js +++ b/gdc-viewer/js/Store/SeqFeature/BaseBEDLikeFeature.js @@ -22,20 +22,15 @@ function( BlobFilehandleWrapper ) { return declare([ SeqFeatureStore, BED ], { - downloadUrl: 'https://api.gdc.cancer.gov/data/', - /** * Constructor * @param {*} args */ constructor: function (args) { - // Unique file ID - this.fileId = args.fileId; - this.data = new BlobFilehandleWrapper( new XHRBlob( this.resolveUrl( - this.downloadUrl + this.fileId + args.urlTemplate ) ) ); @@ -116,4 +111,4 @@ function( }); }, }); -}); \ No newline at end of file +}); diff --git a/gdc-viewer/js/Store/SeqFeature/BedLikeParser.js b/gdc-viewer/js/Store/SeqFeature/BedLikeParser.js index 7dbfc22..21b83a1 100644 --- a/gdc-viewer/js/Store/SeqFeature/BedLikeParser.js +++ b/gdc-viewer/js/Store/SeqFeature/BedLikeParser.js @@ -56,4 +56,4 @@ function ( }); }, }); -}); \ No newline at end of file +}); diff --git a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js index 5a987f6..e4f7b96 100644 --- a/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js +++ b/gdc-viewer/js/Store/SeqFeature/IsoformExpressionQuantification.js @@ -40,4 +40,4 @@ function ( return 'IsoformExpressionQuantification'; }, }); -}); \ No newline at end of file +}); diff --git a/gdc-viewer/js/Store/SeqFeature/SEG.js b/gdc-viewer/js/Store/SeqFeature/SEG.js index 34d0f2f..e6af168 100644 --- a/gdc-viewer/js/Store/SeqFeature/SEG.js +++ b/gdc-viewer/js/Store/SeqFeature/SEG.js @@ -22,4 +22,4 @@ function ( return 'SEG' } }); -}); \ No newline at end of file +}); From 24510fe025144e9a5a44dc5d8c08e0954d22767b Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Thu, 9 Jul 2020 10:38:35 -0400 Subject: [PATCH 11/13] fix gdc post calls --- gdc-viewer/js/Model/GeneFeature.js | 2 +- gdc-viewer/js/Store/SeqFeature/CNVs.js | 2 +- gdc-viewer/js/Store/SeqFeature/Genes.js | 2 +- gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js | 2 +- gdc-viewer/js/View/GDCDialog.js | 8 ++++---- gdc-viewer/js/View/GDCPrimarySitesDialog.js | 2 +- gdc-viewer/js/View/GDCProjectDialog.js | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gdc-viewer/js/Model/GeneFeature.js b/gdc-viewer/js/Model/GeneFeature.js index 82011d2..9403d83 100644 --- a/gdc-viewer/js/Model/GeneFeature.js +++ b/gdc-viewer/js/Model/GeneFeature.js @@ -33,7 +33,7 @@ get: function(name) { } fetch('https://api.gdc.cancer.gov/v0/graphql/projectsTable', { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: JSON.stringify(bodyValProjectsTable) }).then(function(response) { return(response.json()); diff --git a/gdc-viewer/js/Store/SeqFeature/CNVs.js b/gdc-viewer/js/Store/SeqFeature/CNVs.js index d7ac91f..5a7ef91 100644 --- a/gdc-viewer/js/Store/SeqFeature/CNVs.js +++ b/gdc-viewer/js/Store/SeqFeature/CNVs.js @@ -80,7 +80,7 @@ function( // Fetch CNVs and create features fetch(thisB.graphQLUrl + '/CNVsTable', { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: bodyVal }).then(function(response) { return(response.json()); diff --git a/gdc-viewer/js/Store/SeqFeature/Genes.js b/gdc-viewer/js/Store/SeqFeature/Genes.js index f94239b..1908662 100644 --- a/gdc-viewer/js/Store/SeqFeature/Genes.js +++ b/gdc-viewer/js/Store/SeqFeature/Genes.js @@ -122,7 +122,7 @@ function( // Fetch Genes and create features fetch(thisB.graphQLUrl + '/GenesTable', { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: bodyVal }).then(function(response) { return(response.json()); diff --git a/gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js b/gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js index eb1cb36..2db5638 100644 --- a/gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js +++ b/gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js @@ -212,7 +212,7 @@ function( // Fetch SSMs and create features fetch(thisB.graphQLUrl + '/SsmsTable', { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: bodyVal }).then(function(response) { return(response.json()); diff --git a/gdc-viewer/js/View/GDCDialog.js b/gdc-viewer/js/View/GDCDialog.js index 09a42aa..7d9c312 100644 --- a/gdc-viewer/js/View/GDCDialog.js +++ b/gdc-viewer/js/View/GDCDialog.js @@ -236,7 +236,7 @@ function ( // Update the accordions with results from the GDC fetch(thisB.baseGraphQLUrl + '/facetResults', { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: JSON.stringify(bodyVal) }).then(function(response) { return(response.json()); @@ -437,7 +437,7 @@ function ( fetch(url, { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: JSON.stringify(bodyVal) }).then(function(response) { return(response.json()); @@ -549,7 +549,7 @@ function ( fetch(url, { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: JSON.stringify(bodyVal) }).then(function(response) { return(response.json()); @@ -670,7 +670,7 @@ function ( fetch(url, { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: JSON.stringify(bodyVal) }).then(function(response) { return(response.json()); diff --git a/gdc-viewer/js/View/GDCPrimarySitesDialog.js b/gdc-viewer/js/View/GDCPrimarySitesDialog.js index aca983e..70c51b0 100644 --- a/gdc-viewer/js/View/GDCPrimarySitesDialog.js +++ b/gdc-viewer/js/View/GDCPrimarySitesDialog.js @@ -76,7 +76,7 @@ function ( fetch(thisB.baseGraphQLUrl + '/primarySites', { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: JSON.stringify(bodyVal) }).then(function(response) { return(response.json()); diff --git a/gdc-viewer/js/View/GDCProjectDialog.js b/gdc-viewer/js/View/GDCProjectDialog.js index 11bb40e..65d587b 100644 --- a/gdc-viewer/js/View/GDCProjectDialog.js +++ b/gdc-viewer/js/View/GDCProjectDialog.js @@ -82,7 +82,7 @@ function ( fetch(url, { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: JSON.stringify(bodyVal) }).then(function(response) { return(response.json()); From f5c0d80f4e4009dbd4088ee7b872690fcc007df9 Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Thu, 9 Jul 2020 13:15:03 -0400 Subject: [PATCH 12/13] missing post content type --- gdc-viewer/js/Model/SSMFeature.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdc-viewer/js/Model/SSMFeature.js b/gdc-viewer/js/Model/SSMFeature.js index af04f03..4143ec3 100644 --- a/gdc-viewer/js/Model/SSMFeature.js +++ b/gdc-viewer/js/Model/SSMFeature.js @@ -30,7 +30,7 @@ get: function(name) { } fetch('https://api.gdc.cancer.gov/v0/graphql/projectsTable', { method: 'post', - headers: { 'X-Requested-With': null }, + headers: { 'X-Requested-With': null, 'Content-Type': 'application/json' }, body: JSON.stringify(bodyValProjectsTable) }).then(function(response) { return(response.json()); From f76c7c3794cbf7f26d440ac632145a9ac3620bcb Mon Sep 17 00:00:00 2001 From: Andrew Duncan Date: Mon, 27 Jul 2020 14:01:27 -0400 Subject: [PATCH 13/13] fix failing travis test --- cypress/integration/ExploreGDC.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cypress/integration/ExploreGDC.js b/cypress/integration/ExploreGDC.js index 9de5e34..2e20b3c 100644 --- a/cypress/integration/ExploreGDC.js +++ b/cypress/integration/ExploreGDC.js @@ -120,9 +120,9 @@ describe('Explore GDC', function() { }) checkAllResultsTab( - ['Showing 1 to 20 of 3,110', 'TCGA-A5-A1OF', 'TCGA-AJ-A3EK'], - ['Showing 1 to 20 of 21,168', 'TP53', 'TTN'], - ['Showing 1 to 20 of 120,824', 'chr2:g.208248388C>T', 'chr17:g.7673803G>A'] + ['Showing 1 to 20 of 3,111', 'TCGA-A5-A1OF', 'TCGA-AJ-A3EK'], + ['Showing 1 to 20 of 21,169', 'TP53', 'TTN'], + ['Showing 1 to 20 of 120,896', 'chr2:g.208248388C>T', 'chr17:g.7673803G>A'] ) // Select is cancer gene census - 1 @@ -131,9 +131,9 @@ describe('Explore GDC', function() { }) checkAllResultsTab( - ['Showing 1 to 20 of 364', 'TCGA-A5-A1OF', 'TCGA-AJ-A3EK'], + ['Showing 1 to 20 of 365', 'TCGA-A5-A1OF', 'TCGA-AJ-A3EK'], ['Showing 1 to 20 of 575', 'TP53', 'PIK3CA'], - ['Showing 1 to 20 of 5,949', 'chr7:g.140753336A>T', 'chr2:g.208248388C>T'] + ['Showing 1 to 20 of 5,952', 'chr7:g.140753336A>T', 'chr2:g.208248388C>T'] ) selectResultsTab(0)