diff --git a/README.md b/README.md index 456f022..269748f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GDC JBrowse Plugin - Faceted Search and New Store Classes +# GDC JBrowse Plugin A plugin for [JBrowse](https://jbrowse.org/) for viewing [GDC](https://gdc.cancer.gov/) data. For any bugs, issues, or feature recommendations please create an issue through GitHub. # Installation and Setup @@ -9,8 +9,8 @@ Quick setup of JBrowse - https://github.com/GMOD/jbrowse/#install-jbrowse-from-g See [JBrowse - Installing Plugins](https://jbrowse.org/docs/plugins.html) for a general approach to installing plugins. For installing gdc-viewer plugin: -1) Copy the gdc-viewer folder into the JBrowse `plugins` directory. -2) Add 'gdc-viewer' to the array of plugins in the `jbrowse_conf.json`. +1. Copy the gdc-viewer folder into the JBrowse `plugins` directory. +2. Add 'gdc-viewer' to the array of plugins in the `jbrowse_conf.json`. ## 3. Install Reference Sequence Data Now setup the reference sequence used. GDC requires the GRCh38 Human reference files, which can be found at http://ftp.ensembl.org/pub/release-94/fasta/homo_sapiens/dna/. You'll want to download the files of the form `Homo_sapiens.GRCh38.dna.chromosome.1.fa.gz`. @@ -33,13 +33,13 @@ Note that you can specify multiple fast in one command by doing `--fasta fasta1. ## 4. Adding new tracks We have some basic example tracks in `data/tracks.conf`. You can also add new tracks by using the GDC dialog accessible within JBrowse. These are present in the menu under `GDC`. -### A. Explore GDC +### A. Explore cases, genes and mutations This dialog is similar to the Exploration section of the GDC data portal. As you apply facets on the left-hand side, updated results will be shown on the right side. You can create donor specific SSM, Gene, and CNV tracks, along with GDC-wide SSM, Gene and CNV tracks. -### B. View GDC Projects +### B. Explore Projects This dialog shows the projects present on the GDC Data Portal. You can add SSM, Gene, and CNV tracks for each project. -### C. View GDC Primary Sites +### C. Explore Primary Sites This dialog shows the primary sites present on the GDC Data Portal. You can add SSM, Gene, and CNV tracks for each primary site. ## 5. Run JBrowse @@ -115,7 +115,7 @@ Example Track: ``` [tracks.GDC_SSM] storeClass=gdc-viewer/Store/SeqFeature/SimpleSomaticMutations -type=JBrowse/View/Track/CanvasVariants +type=gdc-viewer/View/Track/CanvasVariants key=GDC SSM metadata.datatype=SSM unsafePopup=true @@ -138,7 +138,7 @@ Example Track: ``` [tracks.GDC_CNV] storeClass=gdc-viewer/Store/SeqFeature/CNVs -type=JBrowse/View/Track/Wiggle/XYPlot +type=gdc-viewer/View/Track/Wiggle/XYPlot key=GDC CNV metadata.datatype=CNV autoscale=local @@ -155,4 +155,12 @@ filters={"op":"=","content":{"field":"cnv_change","value":["Gain"]}} You can set the max number of CNVs to return with the `size` field. It defaults to 500. You can view case specific CNVs by setting the `case` field. -Note: You can also use a density plot for the copy number data. Simply change the type from `JBrowse/View/Track/Wiggle/XYPlot` to `JBrowse/View/Track/Wiggle/Density.` \ No newline at end of file +Note: You can also use a density plot for the copy number data. Simply change the type from `JBrowse/View/Track/Wiggle/XYPlot` to `JBrowse/View/Track/Wiggle/Density.` + +# Export Types +The following export types are supported by both GDC Genes and SSMs. To export, select `Save track data` in the track dropdown. Note that not all track information is carried over to the exported file. +* BED +* GFF3 +* Sequin Table +* CSV +* Track Config \ No newline at end of file diff --git a/data/tracks.conf b/data/tracks.conf index 0e41f5a..fd195ef 100644 --- a/data/tracks.conf +++ b/data/tracks.conf @@ -1,6 +1,6 @@ [tracks.GDC_SSM] storeClass=gdc-viewer/Store/SeqFeature/SimpleSomaticMutations -type=JBrowse/View/Track/CanvasVariants +type=gdc-viewer/View/Track/CanvasVariants key=GDC SSM metadata.datatype=SSM unsafePopup=true @@ -8,7 +8,7 @@ fmtDetailValue_projects=function(value) { return "
.fasta { + width: 80% !important; } \ No newline at end of file diff --git a/gdc-viewer/js/Model/GeneFeature.js b/gdc-viewer/js/Model/GeneFeature.js index 726a6da..d526daf 100644 --- a/gdc-viewer/js/Model/GeneFeature.js +++ b/gdc-viewer/js/Model/GeneFeature.js @@ -101,34 +101,34 @@ get: function(name) { */ createProjectTable: function(response) { var thisB = this; - var thStyle = 'border: 1px solid #e6e6e6; padding: .2rem .2rem;'; var headerRow = ` - Project - Disease Type - Site - # SSM Affected Cases - # CNV Gains - # CNV Losses + Project + Disease Type + Site + # SSM Affected Cases + # CNV Gains + # CNV Losses `; - var table = '' + headerRow; + var table = '
' + headerRow; var count = 0; - for (project of response.data.viewer.explore.cases.filtered.project__project_id.buckets) { + var projects = response.data.viewer.explore.cases.filtered.project__project_id.buckets; + for (project of projects) { var trStyle = ''; if (count % 2 != 0) { trStyle = 'style=\"background-color: #f2f2f2\"'; } var projectInfo = thisB.projects.find(x => x.node.project_id === project.key); var row = ` - - - - - - + + + + + + `; @@ -136,6 +136,14 @@ createProjectTable: function(response) { count++; } + if (projects.length == 0) { + table += ` + + + + `; + } + table += ''; return table; }, diff --git a/gdc-viewer/js/Model/SSMFeature.js b/gdc-viewer/js/Model/SSMFeature.js index a9303e0..3a175cd 100644 --- a/gdc-viewer/js/Model/SSMFeature.js +++ b/gdc-viewer/js/Model/SSMFeature.js @@ -95,30 +95,30 @@ get: function(name) { */ createProjectTable: function(response) { var thisB = this; - var thStyle = 'border: 1px solid #e6e6e6; padding: .2rem .2rem;'; var headerRow = ` - - Project - Disease Type - Site - # SSM Affected Cases + + Project + Disease Type + Site + # SSM Affected Cases `; - var table = '' + headerRow; + var table = '
' + headerRow; var count = 0; - for (project of response.data.viewer.explore.cases.filtered.project__project_id.buckets) { + var projects = response.data.viewer.explore.cases.filtered.project__project_id.buckets; + for (project of projects) { var trStyle = ''; if (count % 2 != 0) { trStyle = 'style=\"background-color: #f2f2f2\"'; } var projectInfo = thisB.projects.find(x => x.node.project_id === project.key); var row = ` - - - - + + + + `; @@ -126,6 +126,14 @@ createProjectTable: function(response) { count++; } + if (projects.length == 0) { + table += ` + + + + `; + } + table += ''; return table; }, diff --git a/gdc-viewer/js/Store/SeqFeature/BaseSeqFeature.js b/gdc-viewer/js/Store/SeqFeature/BaseSeqFeature.js index ee7213f..2832c1a 100644 --- a/gdc-viewer/js/Store/SeqFeature/BaseSeqFeature.js +++ b/gdc-viewer/js/Store/SeqFeature/BaseSeqFeature.js @@ -40,7 +40,7 @@ function( * @return {string} a tag */ createLinkWithId: function(link, id) { - return id ? "" + id + "" : "n/a"; + return this.valueIsDefined(id) ? "" + id + "" : "n/a"; }, /** @@ -51,7 +51,7 @@ function( * @return {string} a tag */ createLinkWithIdAndName: function(link, id, name) { - return id ? "" + name + "" : "n/a"; + return this.valueIsDefined(id) ? "" + name + "" : "n/a"; }, /** @@ -60,7 +60,16 @@ function( * @return {string} pretty text */ prettyText: function(text) { - return text && text !== null && text !== undefined ? text : 'n/a'; + if (text && text !== null && text !== undefined && typeof text !== 'undefined') { + if (Array.isArray(text)) { + return text.join(', '); + } else if (typeof text == 'number') { + return text; + } else { + return text.toString(); + } + } + return 'n/a'; }, /** @@ -112,5 +121,13 @@ function( resolve({'getMetadata': function() {}}); }); }, + + /** + * True if value is defined, false otherwise + * @param {*} value + */ + valueIsDefined: function(value) { + return value && value !== null && value !== undefined && typeof value !== 'undefined' && value.length > 0; + } }); }); \ No newline at end of file diff --git a/gdc-viewer/js/Store/SeqFeature/Genes.js b/gdc-viewer/js/Store/SeqFeature/Genes.js index 3ef17bf..146ca01 100644 --- a/gdc-viewer/js/Store/SeqFeature/Genes.js +++ b/gdc-viewer/js/Store/SeqFeature/Genes.js @@ -69,10 +69,12 @@ function( geneFeature = { id: gene.gene_id, data: { + 'entity_name': gene.gene_id, 'start': thisB.prettyText(gene.gene_start), 'end': thisB.prettyText(gene.gene_end), 'strand': thisB.prettyText(gene.gene_strand), 'type': 'Gene', + 'gene description': thisB.prettyText(gene.description), 'about': { 'biotype': thisB.prettyText(gene.biotype), 'gene name': thisB.prettyText(gene.name), @@ -80,8 +82,7 @@ function( 'symbol': thisB.prettyText(gene.symbol), 'synonyms': thisB.prettyText(gene.synonyms) }, - 'gene description': thisB.prettyText(gene.description), - 'external references': { + 'references': { 'ncbi gene': thisB.createLinkWithId(NCBI_LINK, gene.external_db_ids.entrez_gene), 'uniprotkb swiss-prot': thisB.createLinkWithId(UNI_LINK, gene.external_db_ids.uniprotkb_swissprot), 'hgnc': thisB.createLinkWithId(HGNC_LINK, gene.external_db_ids.hgnc), diff --git a/gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js b/gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js index ed21670..7625801 100644 --- a/gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js +++ b/gdc-viewer/js/Store/SeqFeature/SimpleSomaticMutations.js @@ -40,10 +40,14 @@ function( const COSMIC_LINK = 'https://cancer.sanger.ac.uk/cosmic/mutation/overview?id='; array.forEach(cosmic, function(cosmicId) { var cosmicIdNoPrefix = cosmicId.replace('COSM', ''); + cosmicIdNoPrefix = cosmicIdNoPrefix.replace('COSN', ''); cosmicLinks.push(thisB.createLinkWithId(COSMIC_LINK, cosmicIdNoPrefix)); }); - - return cosmicLinks.join(", "); + if (cosmicLinks.length > 0) { + return cosmicLinks.join(", "); + } else { + return 'n/a'; + } }, /** @@ -62,7 +66,7 @@ function( * @return {string} pretty score */ prettyScore: function(label, score) { - return label && score ? label + ' (' + score + ')' : ''; + return label && score ? label + ' (' + score + ')' : 'n/a'; }, /** @@ -74,20 +78,19 @@ function( var thisB = this; const TRANSCRIPT_LINK = 'http://may2015.archive.ensembl.org/Homo_sapiens/Gene/Summary?db=core;g='; const GENES_LINK = 'https://portal.gdc.cancer.gov/genes/'; - var thStyle = 'border: 1px solid #e6e6e6; padding: .2rem .2rem;'; var headerRow = ` - Gene - AA Change - Consequence - Coding DNA Change - Impact - Gene Strand - Transcript(s) + Gene + AA Change + Consequence + Coding DNA Change + Impact + Gene Strand + Transcript(s) `; - var consequenceTable = '' + headerRow; + var consequenceTable = '
' + headerRow; var count = 0; for (consequence of consequences) { @@ -96,19 +99,19 @@ function( trStyle = 'style=\"background-color: #f2f2f2\"'; } var consequenceRow = ` - - - - - + + + + - - + + `; @@ -116,6 +119,14 @@ function( count++; } + if (consequences.length == 0) { + table += ` + + + + `; + } + consequenceTable += ''; return consequenceTable; }, @@ -131,18 +142,18 @@ function( variantFeature = { id: mutation.ssm_id, data: { + 'entity_name': mutation.ssm_id, 'start': thisB.prettyText(mutation.start_position), 'end': thisB.prettyText(mutation.end_position), 'type': 'Simple Somatic Mutation', 'projects': mutation.ssm_id, 'about': { - 'mutation type': thisB.prettyText(mutation.mutation_type), 'subtype': thisB.prettyText(mutation.mutation_subtype), 'dna change': thisB.prettyText(mutation.genomic_dna_change), 'reference allele': thisB.prettyText(mutation.reference_allele), 'id': thisB.prettyText(mutation.ssm_id) }, - 'external references': { + 'references': { 'gdc': thisB.createLinkWithId(GDC_LINK, mutation.ssm_id), 'cosmic': thisB.createCOSMICLinks(mutation.cosmic_id) }, diff --git a/gdc-viewer/js/View/Export/BED.js b/gdc-viewer/js/View/Export/BED.js new file mode 100644 index 0000000..798ed40 --- /dev/null +++ b/gdc-viewer/js/View/Export/BED.js @@ -0,0 +1,53 @@ +define([ 'dojo/_base/declare', + 'dojo/_base/array', + 'JBrowse/View/Export/BED' + ], + function( declare, array, BED ) { + +return declare( BED, + +{ + + bed_field_names: [ + 'seq_id', + 'start', + 'end', + 'entity_name', + 'score', + 'strand', + 'thickStart', + 'thickEnd', + 'itemRgb', + 'blockCount', + 'blockSizes', + 'blockStarts' + ], + + /** + * Format a feature into a string. + * @param {Object} feature feature object (like those returned from JBrowse/Store/SeqFeature/*) + * @returns {String} BED string representation of the feature + */ + formatFeature: function( feature ) { + var fields = array.map( + [ feature.get('seq_id') || this.refSeq.name ] + .concat( dojo.map( this.bed_field_names.slice(1,11), function(field) { + return feature.get( field ); + },this) + ), + function( data ) { + var t = typeof data; + if( t == 'string' || t == 'number' ) + return data; + return ''; + }, + this + ); + + // normalize the strand field + fields[5] = { '1': '+', '-1': '-', '0': '+' }[ fields[5] ] || fields[5]; + return fields.join("\t")+"\n"; + } +}); + +}); diff --git a/gdc-viewer/js/View/Export/CSV.js b/gdc-viewer/js/View/Export/CSV.js new file mode 100644 index 0000000..666ef0a --- /dev/null +++ b/gdc-viewer/js/View/Export/CSV.js @@ -0,0 +1,69 @@ +/** + * Support for Sequin Feature table export. See + * http://www.ncbi.nlm.nih.gov/Sequin/table.html. + */ + +define([ 'dojo/_base/declare', + 'JBrowse/View/Export' + ], + function( declare, ExportBase ) { + +return declare( ExportBase, + +{ + constructor: function( args ) { + this._printHeader(args); + }, + + defaultHeader: [ + 'type', + 'start', + 'end', + 'strand', + 'id' + ], + + geneHeader: [ + 'gene name', + 'biotype', + 'symbol', + 'synonyms' + ], + + ssmHeader: [ + 'reference allele', + 'dna change', + 'subtype' + ], + + fullHeader: [], + + _printHeader: function() { + if (this.store.config.storeClass === 'gdc-viewer/Store/SeqFeature/Genes') { + this.fullHeader = this.geneHeader + } else if (this.store.config.storeClass === 'gdc-viewer/Store/SeqFeature/SimpleSomaticMutations') { + this.fullHeader = this.ssmHeader + } + + var headerString = (this.defaultHeader.concat(this.fullHeader)).join(',') + '\n' + this.print(headerString) + }, + + formatFeature: function( feature ) { + var featureArray = [] + this.defaultHeader.forEach(field => featureArray.push(feature.get(field))) + var about = feature.get('about') + this.fullHeader.forEach(field => { + if (field === 'synonyms') { + featureArray.push + if (about[field]) { + featureArray.push('[' + about[field] + ']') + } + } else { + featureArray.push(about[field]) + } + }) + return featureArray.join(",") + "\n" + } +}); +}); \ No newline at end of file diff --git a/gdc-viewer/js/View/Export/GFF3.js b/gdc-viewer/js/View/Export/GFF3.js new file mode 100644 index 0000000..f087eb1 --- /dev/null +++ b/gdc-viewer/js/View/Export/GFF3.js @@ -0,0 +1,30 @@ +import gff from '@gmod/gff' + +define([ 'dojo/_base/declare', + 'JBrowse/View/Export/GFF3' + ], + function( declare, GFF3 ) { + +return declare( GFF3, +{ + + /** + * Need to overwrite this function to deal with object attributes + * @private + * @returns {String} formatted attribute string + */ + _gff3_format_attributes: function( attrs ) { + var attrOrder = []; + for( var tag in attrs ) { + var val = attrs[tag]; + if(!val || tag != 'about') { + continue; + } + + var valstring = gff.util.escape(JSON.stringify(val)) + attrOrder.push( gff.util.escape( tag )+'='+valstring); + } + return attrOrder.join(';') || '.'; + }, +}); +}); diff --git a/gdc-viewer/js/View/Export/SequinTable.js b/gdc-viewer/js/View/Export/SequinTable.js new file mode 100644 index 0000000..0689a23 --- /dev/null +++ b/gdc-viewer/js/View/Export/SequinTable.js @@ -0,0 +1,39 @@ +define([ 'dojo/_base/declare', + 'dojo/_base/array', + 'JBrowse/View/Export/SequinTable' + ], + function( declare, array, SequinTable ) { + +return declare( SequinTable, + +{ + formatFeature: function( feature ) { + var thisB = this; + if( ! this.headerPrinted ) + this.headerPrinted = this._printHeader( feature ); + + var featLine = [ feature.get('start')+1, + feature.get('end'), + feature.get('type') || 'region' + ]; + if( feature.get('strand') == -1 ) { + var t = featLine[0]; + featLine[0] = featLine[1]; + featLine[1] = t; + } + + var qualifiers = [] + var about = feature.get('about') + for (var key of Object.keys(about)) { + var qualifier = [] + qualifier.push(key) + qualifier.push(about[key]) + qualifiers.push(qualifier) + } + + return featLine.join("\t")+"\n" + array.map( qualifiers, function( q ) { return "\t\t\t"+q.join("\t")+"\n"; } ).join('') + + array.map(feature.children(), f => this.formatFeature(f)).join('') + } +}); + +}); diff --git a/gdc-viewer/js/View/Export/TrackConfig.js b/gdc-viewer/js/View/Export/TrackConfig.js new file mode 100644 index 0000000..1d5f453 --- /dev/null +++ b/gdc-viewer/js/View/Export/TrackConfig.js @@ -0,0 +1,53 @@ +/** + * Support for Sequin Feature table export. See + * http://www.ncbi.nlm.nih.gov/Sequin/table.html. + */ + +define([ 'dojo/_base/declare', + 'JBrowse/View/Export' + ], + function( declare, ExportBase ) { + +return declare( ExportBase, + +{ + constructor: function( args ) { + this._printHeader(args); + }, + + _printHeader: function() { + var storeArray = (this.store.config.storeClass).split('/') + + var trackArray = [ + '[tracks.' + this.store.config.label + ']', + 'storeClass=' + this.store.config.storeClass, + 'type=' + this.track.config.type, + 'key=' + this.store.config.key, + 'metadata.datatype=' + storeArray[storeArray.length - 1], + 'unsafePopup=true', + ] + + if (this.store.config.storeClass === 'gdc-viewer/Store/SeqFeature/SimpleSomaticMutations' || this.store.config.storeClass === 'gdc-viewer/Store/SeqFeature/Genes') { + trackArray.push('fmtDetailValue_projects=function(value) { return "
Loading...
";}'); + } + + if (this.store.config.storeClass === 'gdc-viewer/Store/SeqFeature/CNVs') { + trackArray.push("autoscale=local"); + trackArray.push("bicolor_pivot=0"); + } + + if (this.store.filters) { + trackArray.push('filters=' + JSON.stringify(this.store.filters)) + } + + var trackString = trackArray.join('\n') + + this.print(trackString) + }, + + formatFeature: function( feature ) { + // This file type only requires track information and not feature information + return '' + } +}); +}); \ No newline at end of file diff --git a/gdc-viewer/js/View/Export/TrackConfigJson.js b/gdc-viewer/js/View/Export/TrackConfigJson.js new file mode 100644 index 0000000..cf2e573 --- /dev/null +++ b/gdc-viewer/js/View/Export/TrackConfigJson.js @@ -0,0 +1,55 @@ +/** + * Support for Sequin Feature table export. See + * http://www.ncbi.nlm.nih.gov/Sequin/table.html. + */ + +define([ 'dojo/_base/declare', + 'JBrowse/View/Export' + ], + function( declare, ExportBase ) { + +return declare( ExportBase, + +{ + constructor: function( args ) { + this._printHeader(args); + }, + + _printHeader: function() { + var storeArray = (this.store.config.storeClass).split('/') + + var trackObject = { + 'label': this.store.config.label, + 'storeClass': this.store.config.storeClass, + 'type': this.track.config.type, + 'key': this.store.config.key, + 'metadata': { + 'datatype': storeArray[storeArray.length - 1] + }, + 'unsafePopup': true + } + + if (this.store.config.storeClass === 'gdc-viewer/Store/SeqFeature/SimpleSomaticMutations' || this.store.config.storeClass === 'gdc-viewer/Store/SeqFeature/Genes') { + trackObject['fmtDetailValue_projects'] = 'function(value) { return "
Loading...
";}'; + } + + if (this.store.config.storeClass === 'gdc-viewer/Store/SeqFeature/CNVs') { + trackObject['autoscale'] = 'local'; + trackObject['bicolor_pivot'] = 0; + } + + if (this.store.filters) { + trackObject['filters'] = JSON.stringify(this.store.filters); + } + + var trackString = JSON.stringify(trackObject, null, '\t'); + + this.print(trackString) + }, + + formatFeature: function( feature ) { + // This file type only requires track information and not feature information + return '' + } +}); +}); \ No newline at end of file diff --git a/gdc-viewer/js/View/GDCDialog.js b/gdc-viewer/js/View/GDCDialog.js index 4ed6777..96677c3 100644 --- a/gdc-viewer/js/View/GDCDialog.js +++ b/gdc-viewer/js/View/GDCDialog.js @@ -212,7 +212,7 @@ function ( var combinedFilters = thisB.combineAllFilters(); combinedFilters = combinedFilters ? JSON.parse(combinedFilters) : combinedFilters - var facetQuery = `query Queries($filters:FiltersArgument!,$first:Int!,$offset:Int!) {viewer {...F4}} fragment F0 on ECaseAggregations {primary_site {buckets {doc_count,key}},project__program__name {buckets {doc_count,key}},project__project_id {buckets {doc_count,key}},disease_type {buckets {doc_count,key}},demographic__gender {buckets {doc_count,key}},diagnoses__age_at_diagnosis {stats {max,min,count}},diagnoses__vital_status {buckets {doc_count,key}},diagnoses__days_to_death {stats {max,min,count}},demographic__race {buckets {doc_count,key}},demographic__ethnicity {buckets {doc_count,key}}} fragment F1 on GeneAggregations {biotype {buckets {doc_count,key}},case__cnv__cnv_change {buckets {doc_count,key,key_as_string}},is_cancer_gene_census {buckets {doc_count,key,key_as_string}}} fragment F2 on CNVAggregations {cnv_change {buckets {doc_count,key,key_as_string}}} fragment F3 on SsmAggregations {consequence__transcript__annotation__vep_impact {buckets {doc_count,key}},consequence__transcript__annotation__polyphen_impact {buckets {doc_count,key}},consequence__transcript__annotation__sift_impact {buckets {doc_count,key}},consequence__transcript__consequence_type {buckets {doc_count,key}},mutation_subtype {buckets {doc_count,key}},occurrence__case__observation__variant_calling__variant_caller {buckets {doc_count,key}}} fragment F4 on Root {explore {cases {_aggregations:aggregations(filters:$filters,aggregations_filter_themselves:false) {...F0},_hits:hits(first:$first,offset:$offset,filters:$filters,score:"gene.gene_id") {total}},genes {_aggregations:aggregations(filters:$filters,aggregations_filter_themselves:false) {...F1},_hits:hits(filters:$filters) {total}},cnvs {_aggregations:aggregations(filters:$filters,aggregations_filter_themselves:false) {...F2},_hits:hits(filters:$filters) {total}},ssms {_aggregations:aggregations(filters:$filters,aggregations_filter_themselves:false) {...F3},_hits:hits(filters:$filters) {total}}}}`; + var facetQuery = `query Queries($filters:FiltersArgument!,$first:Int!,$offset:Int!) {viewer {...F4}} fragment F0 on ECaseAggregations {primary_site {buckets {doc_count,key}},project__program__name {buckets {doc_count,key}},project__project_id {buckets {doc_count,key}},disease_type {buckets {doc_count,key}},demographic__gender {buckets {doc_count,key}},diagnoses__age_at_diagnosis {stats {max,min,count}},demographic__race {buckets {doc_count,key}},demographic__ethnicity {buckets {doc_count,key}}} fragment F1 on GeneAggregations {biotype {buckets {doc_count,key}},case__cnv__cnv_change {buckets {doc_count,key,key_as_string}},is_cancer_gene_census {buckets {doc_count,key,key_as_string}}} fragment F2 on CNVAggregations {cnv_change {buckets {doc_count,key,key_as_string}}} fragment F3 on SsmAggregations {consequence__transcript__annotation__vep_impact {buckets {doc_count,key}},consequence__transcript__annotation__polyphen_impact {buckets {doc_count,key}},consequence__transcript__annotation__sift_impact {buckets {doc_count,key}},consequence__transcript__consequence_type {buckets {doc_count,key}},mutation_subtype {buckets {doc_count,key}},occurrence__case__observation__variant_calling__variant_caller {buckets {doc_count,key}}} fragment F4 on Root {explore {cases {_aggregations:aggregations(filters:$filters,aggregations_filter_themselves:false) {...F0},_hits:hits(first:$first,offset:$offset,filters:$filters,score:"gene.gene_id") {total}},genes {_aggregations:aggregations(filters:$filters,aggregations_filter_themselves:false) {...F1},_hits:hits(filters:$filters) {total}},cnvs {_aggregations:aggregations(filters:$filters,aggregations_filter_themselves:false) {...F2},_hits:hits(filters:$filters) {total}},ssms {_aggregations:aggregations(filters:$filters,aggregations_filter_themselves:false) {...F3},_hits:hits(filters:$filters) {total}}}}`; var bodyVal = { query: facetQuery, variables: { @@ -315,7 +315,11 @@ function ( }); var facetHolder = dom.create('span', { className: "flex-column" }); - + // If facet has no terms + if (!results._aggregations[facet].buckets || results._aggregations[facet].buckets.length == 0) { + dom.create('span', { className: "flex-row", innerHTML: "No terms available for the selected facet." }, facetHolder) + } + // If facet has at least one term if (results._aggregations[facet].buckets && results._aggregations[facet].buckets.length > 0) { // Alphabetical sort results._aggregations[facet].buckets.sort(thisB.compareTermElements); @@ -451,7 +455,7 @@ function ( label: "Filtered SSMs form GDC", iconClass: "dijitIconNewTask", onClick: function() { - thisB.addTrack('SimpleSomaticMutations', undefined, combinedFilters, 'CanvasVariants'); + thisB.addTrack('SimpleSomaticMutations', undefined, combinedFilters, 'gdc-viewer/View/Track/SSMTrack'); alert("Adding track with all SSMs from the GDC, with current filters applied"); } }); @@ -463,7 +467,7 @@ function ( iconClass: "dijitIconNewTask", dropDown: ssmMenu, onClick: function() { - thisB.addTrack('SimpleSomaticMutations', undefined, undefined, 'CanvasVariants'); + thisB.addTrack('SimpleSomaticMutations', undefined, undefined, 'gdc-viewer/View/Track/SSMTrack'); alert("Add track with all SSMs from the GDC"); } }); @@ -562,7 +566,7 @@ function ( label: "Filtered Genes from GDC", iconClass: "dijitIconNewTask", onClick: function() { - thisB.addTrack('Genes', undefined, combinedFilters, 'CanvasVariants'); + thisB.addTrack('Genes', undefined, combinedFilters, 'gdc-viewer/View/Track/GeneTrack'); alert("Adding track with all genes from the GDC, with current filters applied"); } }); @@ -575,7 +579,7 @@ function ( dropDown: geneMenu, style: "padding-right: 8px;", onClick: function() { - thisB.addTrack('Genes', undefined, undefined, 'CanvasVariants'); + thisB.addTrack('Genes', undefined, undefined, 'gdc-viewer/View/Track/GeneTrack'); alert("Adding track with all genes from the GDC"); } }); @@ -590,7 +594,7 @@ function ( label: "Filtered CNVs from GDC", iconClass: "dijitIconNewTask", onClick: function() { - thisB.addTrack('CNVs', undefined, combinedFilters, 'Wiggle/XYPlot'); + thisB.addTrack('CNVs', undefined, combinedFilters, 'JBrowse/View/Track/Wiggle/XYPlot'); alert("Adding track with all CNVs from the GDC, with current filters applied"); } }); @@ -602,7 +606,7 @@ function ( iconClass: "dijitIconNewTask", dropDown: cnvMenu, onClick: function() { - thisB.addTrack('CNVs', undefined, undefined, 'Wiggle/XYPlot'); + thisB.addTrack('CNVs', undefined, undefined, 'JBrowse/View/Track/Wiggle/XYPlot'); alert("Adding track with all CNVs from the GDC"); } }); @@ -647,7 +651,7 @@ function ( // Create body for GraphQL query var start = thisB.getStartIndex(thisB.casePage); var size = thisB.pageSize; - var caseQuery = `query caseResultsTableQuery( $filters: FiltersArgument $casesSize: Int $casesOffset: Int $casesScore: String $sort: [Sort] ) { exploreCasesTableViewer: viewer { explore { cases { hits(first: $casesSize, offset: $casesOffset, filters: $filters, score: $casesScore, sort: $sort) { total edges { node { score id case_id primary_site disease_type submitter_id project { project_id program { name } id } diagnoses { hits(first: 1) { edges { node { primary_diagnosis age_at_diagnosis vital_status days_to_death id } } } } demographic { gender ethnicity race } summary { data_categories { file_count data_category } experimental_strategies { experimental_strategy file_count } file_count } } } } } } } }`; + var caseQuery = `query caseResultsTableQuery( $filters: FiltersArgument $casesSize: Int $casesOffset: Int $casesScore: String $sort: [Sort] ) { exploreCasesTableViewer: viewer { explore { cases { hits(first: $casesSize, offset: $casesOffset, filters: $filters, score: $casesScore, sort: $sort) { total edges { node { score id case_id primary_site disease_type submitter_id project { project_id program { name } id } diagnoses { hits(first: 1) { edges { node { primary_diagnosis age_at_diagnosis id } } } } demographic { gender ethnicity race } summary { data_categories { file_count data_category } experimental_strategies { experimental_strategy file_count } file_count } } } } } } } }`; var combinedFilters = thisB.combineAllFilters(); if (combinedFilters) { @@ -713,8 +717,10 @@ function ( var rowsHolderNode = dom.toDom(rowsHolder); + var hasHits = false; if (response.data) { for (var hitId in response.data.exploreCasesTableViewer.explore.cases.hits.edges) { + hasHits = true; var hit = response.data.exploreCasesTableViewer.explore.cases.hits.edges[hitId].node; var caseRowContent = ` @@ -742,7 +748,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit, combinedFilters) { return function() { - thisB.addTrack('Genes', hit.case_id, combinedFilters, 'CanvasVariants'); + thisB.addTrack('Genes', hit.case_id, combinedFilters, 'gdc-viewer/View/Track/GeneTrack'); alert("Adding Gene track for case " + hit.case_id); } })(hit, combinedFilters) @@ -756,7 +762,7 @@ function ( dropDown: geneMenu, onClick: (function(hit) { return function() { - thisB.addTrack('Genes', hit.case_id, undefined, 'CanvasVariants'); + thisB.addTrack('Genes', hit.case_id, undefined, 'gdc-viewer/View/Track/GeneTrack'); alert("Adding Gene track for case " + hit.case_id); } })(hit) @@ -779,7 +785,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit, combinedFilters) { return function() { - thisB.addTrack('SimpleSomaticMutations', hit.case_id, combinedFilters, 'CanvasVariants'); + thisB.addTrack('SimpleSomaticMutations', hit.case_id, combinedFilters, 'gdc-viewer/View/Track/SSMTrack'); alert("Adding Simple Somatic Mutation track for case " + hit.case_id); } })(hit, combinedFilters) @@ -793,7 +799,7 @@ function ( dropDown: ssmMenu, onClick: (function(hit) { return function() { - thisB.addTrack('SimpleSomaticMutations', hit.case_id, undefined, 'CanvasVariants'); + thisB.addTrack('SimpleSomaticMutations', hit.case_id, undefined, 'gdc-viewer/View/Track/SSMTrack'); alert("Adding Simple Somatic Mutation track for case " + hit.case_id); } })(hit) @@ -816,7 +822,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit, combinedFilters) { return function() { - thisB.addTrack('CNVs', hit.case_id, combinedFilters, 'Wiggle/XYPlot'); + thisB.addTrack('CNVs', hit.case_id, combinedFilters, 'JBrowse/View/Track/Wiggle/XYPlot'); alert("Adding CNV track for case " + hit.case_id); } })(hit, combinedFilters) @@ -830,7 +836,7 @@ function ( dropDown: cnvMenu, onClick: (function(hit) { return function() { - thisB.addTrack('CNVs', hit.case_id, undefined, 'Wiggle/XYPlot'); + thisB.addTrack('CNVs', hit.case_id, undefined, 'JBrowse/View/Track/Wiggle/XYPlot'); alert("Adding CNV track for case " + hit.case_id); } })(hit) @@ -851,6 +857,18 @@ function ( } } + + if (!hasHits) { + var noResultsHtml = ` + + No cases found + + `; + + var noResultsRow = dom.toDom(noResultsHtml); + dom.place(noResultsRow, rowsHolderNode); + } + // Place rows into table dom.place(rowsHolderNode, tableNode); @@ -880,8 +898,11 @@ function ( var rowsHolderNode = dom.toDom(rowsHolder); + var hasHits = false; + if (response.data) { for (var hitId in response.data.genesTableViewer.explore.genes.hits.edges) { + hasHits = true; var hit = response.data.genesTableViewer.explore.genes.hits.edges[hitId].node; var caseRowContent = ` @@ -902,6 +923,17 @@ function ( } } + + if (!hasHits) { + var noResultsHtml = ` + + No genes found + + `; + + var noResultsRow = dom.toDom(noResultsHtml); + dom.place(noResultsRow, rowsHolderNode); + } dom.place(rowsHolderNode, tableNode); dom.place(tableNode, location); }, @@ -984,8 +1016,10 @@ function ( `; var rowsHolderNode = dom.toDom(rowsHolder); + var hasHits = false; if (response.data) { for (var hitId in response.data.viewer.explore.ssms.hits.edges) { + hasHits = true; var hit = response.data.viewer.explore.ssms.hits.edges[hitId]; var caseRowContent = ` @@ -1005,6 +1039,17 @@ function ( } } + if (!hasHits) { + var noResultsHtml = ` + + No mutations found + + `; + + var noResultsRow = dom.toDom(noResultsHtml); + dom.place(noResultsRow, rowsHolderNode); + } + dom.place(rowsHolderNode, tableNode); dom.place(tableNode, location); }, @@ -1302,7 +1347,7 @@ function ( } var trackConf = { - type: 'JBrowse/View/Track/' + trackType, + type: trackType, store: storeName, label: label, key: key, diff --git a/gdc-viewer/js/View/GDCPrimarySitesDialog.js b/gdc-viewer/js/View/GDCPrimarySitesDialog.js index 5cd45a6..478f21b 100644 --- a/gdc-viewer/js/View/GDCPrimarySitesDialog.js +++ b/gdc-viewer/js/View/GDCPrimarySitesDialog.js @@ -84,20 +84,30 @@ function ( if (response.data) { thisB.createPrimarySiteTable(response); } else { - var errorMessageHolder = dom.create('div', { style: 'display: flex; flex-direction: column; align-items: center;' }, thisB.resultsContainer); - var errorMessage = dom.create('div', { innerHTML: 'There was an error contacting GDC.' }, errorMessageHolder); - var hardRefreshButton = new Button({ - label: 'Refresh Results', - onClick: function() { - thisB.getPrimarySiteInformation(); - } - }).placeAt(errorMessageHolder); + thisB.createHardRefreshMessage(thisB.resultsContainer); } }).catch(function(err) { console.log(err); + thisB.createHardRefreshMessage(thisB.resultsContainer); }); }, + /** + * Adds a error message to the given location along with a button to hard refresh the results + * @param {*} location + */ + createHardRefreshMessage: function(location) { + var thisB = this; + var errorMessageHolder = dom.create('div', { style: 'display: flex; flex-direction: column; align-items: center;' }, location); + var errorMessage = dom.create('div', { innerHTML: 'There was an error contacting GDC.' }, errorMessageHolder); + var hardRefreshButton = new Button({ + label: 'Refresh Results', + onClick: function() { + thisB.getPrimarySiteInformation(); + } + }).placeAt(errorMessageHolder); + }, + /** * Creates a table with primary sites * @param {object} response object returned by graphQL call @@ -137,7 +147,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit) { return function() { - thisB.addTrack('SimpleSomaticMutations', hit.key, 'CanvasVariants'); + thisB.addTrack('SimpleSomaticMutations', hit.key, 'gdc-viewer/View/Track/SSMTrack'); alert("Adding SSM track for primary site " + hit.key); } })(hit) @@ -149,7 +159,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit) { return function() { - thisB.addTrack('Genes', hit.key, 'CanvasVariants'); + thisB.addTrack('Genes', hit.key, 'gdc-viewer/View/Track/GeneTrack'); alert("Adding Gene track for primary site " + hit.key); } })(hit) @@ -161,7 +171,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit) { return function() { - thisB.addTrack('CNVs', hit.key, 'Wiggle/XYPlot'); + thisB.addTrack('CNVs', hit.key, 'JBrowse/View/Track/Wiggle/XYPlot'); alert("Adding CNV track for primary site " + hit.key); } })(hit) @@ -223,7 +233,7 @@ function ( label += '_' + primarySite var trackConf = { - type: 'JBrowse/View/Track/' + trackType, + type: trackType, store: storeName, label: label, key: key, diff --git a/gdc-viewer/js/View/GDCProjectDialog.js b/gdc-viewer/js/View/GDCProjectDialog.js index 03045d2..64e3ef3 100644 --- a/gdc-viewer/js/View/GDCProjectDialog.js +++ b/gdc-viewer/js/View/GDCProjectDialog.js @@ -100,20 +100,30 @@ function ( thisB.createProjectsTable(response); thisB.createPaginationButtons(totalPages); } else { - var errorMessageHolder = dom.create('div', { style: 'display: flex; flex-direction: column; align-items: center;' }, thisB.resultsContainer); - var errorMessage = dom.create('div', { innerHTML: 'There was an error contacting GDC.' }, errorMessageHolder); - var hardRefreshButton = new Button({ - label: 'Refresh Results', - onClick: function() { - thisB.getProjectInformation(); - } - }).placeAt(errorMessageHolder); + thisB.createHardRefreshMessage(thisB.resultsContainer); } }).catch(function(err) { console.log(err); + thisB.createHardRefreshMessage(thisB.resultsContainer); }); }, + /** + * Adds a error message to the given location along with a button to hard refresh the results + * @param {*} location + */ + createHardRefreshMessage: function(location) { + var thisB = this; + var errorMessageHolder = dom.create('div', { style: 'display: flex; flex-direction: column; align-items: center;' }, location); + var errorMessage = dom.create('div', { innerHTML: 'There was an error contacting GDC.' }, errorMessageHolder); + var hardRefreshButton = new Button({ + label: 'Refresh Results', + onClick: function() { + thisB.getProjectInformation(); + } + }).placeAt(errorMessageHolder); + }, + /** * Creates a table with projects * @param {object} response Object returned from GraphQL call @@ -159,7 +169,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit) { return function() { - thisB.addTrack('SimpleSomaticMutations', hit.project_id, 'CanvasVariants'); + thisB.addTrack('SimpleSomaticMutations', hit.project_id, 'gdc-viewer/View/Track/SSMTrack'); alert("Adding SSM track for project " + hit.project_id); } })(hit) @@ -171,7 +181,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit) { return function() { - thisB.addTrack('Genes', hit.project_id, 'CanvasVariants'); + thisB.addTrack('Genes', hit.project_id, 'gdc-viewer/View/Track/GeneTrack'); alert("Adding Gene track for project " + hit.project_id); } })(hit) @@ -183,7 +193,7 @@ function ( iconClass: "dijitIconNewTask", onClick: (function(hit) { return function() { - thisB.addTrack('CNVs', hit.project_id, 'Wiggle/XYPlot'); + thisB.addTrack('CNVs', hit.project_id, 'JBrowse/View/Track/Wiggle/XYPlot'); alert("Adding CNV track for project " + hit.project_id); } })(hit) @@ -245,7 +255,7 @@ function ( label += '_' + projectId var trackConf = { - type: 'JBrowse/View/Track/' + trackType, + type: trackType, store: storeName, label: label, key: key, diff --git a/gdc-viewer/js/View/Track/GeneTrack.js b/gdc-viewer/js/View/Track/GeneTrack.js new file mode 100644 index 0000000..459a4c8 --- /dev/null +++ b/gdc-viewer/js/View/Track/GeneTrack.js @@ -0,0 +1,53 @@ +define( + [ + "dojo/_base/declare", + "JBrowse/View/Track/CanvasFeatures", + 'JBrowse/View/Track/_ExportMixin', + 'dojo/dom-construct' + ], + function( + declare, + CanvasFeatures, + ExportMixin, + domConstruct) { + return declare([ CanvasFeatures, ExportMixin ], { + + _exportFormats: function() { + return [ + {name: 'gdc-viewer/View/Export/GFF3', label: 'GFF3', fileExt: 'gff3'}, + {name: 'gdc-viewer/View/Export/BED', label: 'BED', fileExt: 'bed'}, + {name: 'gdc-viewer/View/Export/CSV', label: 'CSV', fileExt: 'csv'}, + {name: 'icgc-viewer/View/Export/SequinTable', label: 'Sequin Table', fileExt: 'sqn'}, + {name: 'gdc-viewer/View/Export/TrackConfig', label: 'Track Config INI', fileExt: 'conf'}, + {name: 'gdc-viewer/View/Export/TrackConfigJson', label: 'Track Config JSON', fileExt: 'json'} + ]; + }, + + _renderAdditionalTagsDetail: function( track, f, featDiv, container ) { + var atElement = domConstruct.create( + 'div', + { className: 'additional', + innerHTML: '

Information

' + }, + container ) + + var coreDetailsContent = dojo.create('div', { className: 'core', style: 'display: flex; flex-direction: column;' }, atElement ); + var firstRow = dojo.create('div', { style: 'display: flex; flex-direction: row;' }, coreDetailsContent ); + var secondRow = dojo.create('div', { style: 'display: flex; flex-direction: row;' }, coreDetailsContent ); + var thirdRow = dojo.create('div', { style: 'display: flex; flex-direction: row;' }, coreDetailsContent ); + + var descriptionSection = dojo.create('div', { style: 'flex-grow:1; flex-basis: 0' }, firstRow) + this.renderDetailField( descriptionSection, 'gene description', f.get('gene description'), f, undefined, track.store.getTagMetadata('gene description')) + + var aboutSection = dojo.create('div', { style: 'flex-grow:1; flex-basis: 0' }, secondRow) + var referenceSection = dojo.create('div', { style: 'flex-grow:1; flex-basis: 0' }, secondRow) + + this.renderDetailField( aboutSection, 'about', f.get('about'), f, undefined, track.store.getTagMetadata('about')) + this.renderDetailField( referenceSection, 'references', f.get('references'), f, undefined, track.store.getTagMetadata('references')) + + var projectSection = dojo.create('div', { style: 'flex-grow:1; flex-basis: 0' }, thirdRow) + this.renderDetailField( projectSection, 'projects', f.get('projects'), f, undefined, track.store.getTagMetadata('projects')) + }, + }); + } +); \ No newline at end of file diff --git a/gdc-viewer/js/View/Track/SSMTrack.js b/gdc-viewer/js/View/Track/SSMTrack.js new file mode 100644 index 0000000..c3c5041 --- /dev/null +++ b/gdc-viewer/js/View/Track/SSMTrack.js @@ -0,0 +1,52 @@ +define( + [ + "dojo/_base/declare", + "JBrowse/View/Track/CanvasFeatures", + 'JBrowse/View/Track/_ExportMixin', + 'dojo/dom-construct' + ], + function( + declare, + CanvasFeatures, + ExportMixin, + domConstruct) { + return declare([ CanvasFeatures, ExportMixin ], { + + _exportFormats: function() { + return [ + {name: 'gdc-viewer/View/Export/GFF3', label: 'GFF3', fileExt: 'gff3'}, + {name: 'gdc-viewer/View/Export/BED', label: 'BED', fileExt: 'bed'}, + {name: 'gdc-viewer/View/Export/CSV', label: 'CSV', fileExt: 'csv'}, + {name: 'icgc-viewer/View/Export/SequinTable', label: 'Sequin Table', fileExt: 'sqn'}, + {name: 'gdc-viewer/View/Export/TrackConfig', label: 'Track Config INI', fileExt: 'conf'}, + {name: 'gdc-viewer/View/Export/TrackConfigJson', label: 'Track Config JSON', fileExt: 'json'}]; + }, + + _renderAdditionalTagsDetail: function( track, f, featDiv, container ) { + var atElement = domConstruct.create( + 'div', + { className: 'additional', + innerHTML: '

Information

' + }, + container ) + + var coreDetailsContent = dojo.create('div', { className: 'core', style: 'display: flex; flex-direction: column; width: 100%;' }, atElement ); + var firstRow = dojo.create('div', { style: 'display: flex; flex-direction: row;' }, coreDetailsContent ); + var secondRow = dojo.create('div', { style: 'display: flex; flex-direction: row;' }, coreDetailsContent ); + var thirdRow = dojo.create('div', { style: 'display: flex; flex-direction: row;' }, coreDetailsContent ); + + var aboutSection = dojo.create('div', { style: 'flex-grow:1; flex-basis: 0' }, firstRow) + var referenceSection = dojo.create('div', { style: 'flex-grow:1; flex-basis: 0' }, firstRow) + + this.renderDetailField( aboutSection, 'about', f.get('about'), f, undefined, track.store.getTagMetadata('about')) + this.renderDetailField( referenceSection, 'references', f.get('references'), f, undefined, track.store.getTagMetadata('references')) + + var consequenceSection = dojo.create('div', { style: 'flex-grow:1; flex-basis: 0' }, secondRow) + this.renderDetailField( consequenceSection, 'mutation consequences', f.get('mutation consequences'), f, undefined, track.store.getTagMetadata('mutation consequences')) + + var projectSection = dojo.create('div', { style: 'flex-grow:1; flex-basis: 0' }, thirdRow) + this.renderDetailField( projectSection, 'projects', f.get('projects'), f, undefined, track.store.getTagMetadata('projects')) + }, + }); + } +); \ No newline at end of file diff --git a/gdc-viewer/js/main.js b/gdc-viewer/js/main.js index 747eb4b..01ef9d5 100644 --- a/gdc-viewer/js/main.js +++ b/gdc-viewer/js/main.js @@ -22,19 +22,19 @@ return declare( JBrowsePlugin, this.browser.afterMilestone('initView', function () { this.browser.addGlobalMenuItem('gdc', new MenuItem( { - label: 'Explore GDC', + label: 'Explore cases, genes and mutations', iconClass: "dijitIconSearch", onClick: lang.hitch(this, 'createGDCExplore') })); this.browser.addGlobalMenuItem('gdc', new MenuItem( { - label: 'GDC Projects', + label: 'Explore Projects', iconClass: "dijitIconSearch", onClick: lang.hitch(this, 'createGDCProject') })); this.browser.addGlobalMenuItem('gdc', new MenuItem( { - label: 'GDC Primary Sites', + label: 'Explore Primary Sites', iconClass: "dijitIconSearch", onClick: lang.hitch(this, 'createGDCPrimarySites') }));