diff --git a/examples/vanilla/gene-leads.html b/examples/vanilla/gene-leads.html index 47f0cb17..11f05b18 100644 --- a/examples/vanilla/gene-leads.html +++ b/examples/vanilla/gene-leads.html @@ -251,7 +251,8 @@

Gene LeadsEnrich your gene search

onClickAnnot } - // If you don't want certain genes displayed, do this: + const annotsInList = 'all'; + // If you only want certain genes displayed, do this: // annotsInList = ['CDK9', 'CDK19', 'CDK1']; // // A longer list of genes, from Scanpy / Seurat 3k PBMC tutorial @@ -260,16 +261,16 @@

Gene LeadsEnrich your gene search

// "CORO1C","USP30","GLTP","GIT2","HVCN1","ALDH2","TMEM116","NAA25","RPH3A","OAS1","FBXO21","RFC5","TAOK3","DYNLL1","MLEC","OASL","ORAI1","SETD1B","CLIP1","RILPL2","SNRNP35","EIF2B1","NCOR2","NOC4L","MRP63","GTF3A","MTIF3","FRY-AS1","N4BP2L1","SPG20","ALG5","EXOSC8","MTRF1","DNAJC15","CCDC122","TSC22D1","DLEU7","RNASEH2B","MZT1","DIS3","MYCBP2","RBM26-AS1","UBAC2","GPR183","CLYBL","ARGLU1","ABHD13","TNFSF13B","ARHGEF7","MCF2L","CDC16","RPPH1","METTL3","CMTM5","DHRS4","PSME1","RP11-468E2.4","IRF9","CHMP4A","MDP1","GMPR2","TINF2","CIDEB","LTB4R","GZMH","GZMB","SCFD1","AP4S1","EAPP","PPP2R3C","FBXO33","FKBP3","MIS18BP1","NEMF","ARF6","C14orf166","PTGDR","PSMC6","CGRRF1","MAPK1IP1L","LGALS3","RP11-349A22.5","ARID4A","JKAMP","MNAT1","CTD-2302E22.4","ATP6V1D","EIF2S1","ARG2","VTI1B","RAD51B","DCAF5","ERH","SRSF5","PCNX","RBM25","NUMB","LIN52","NPC2","ISCA2","DLST","RPS6KL1","RP11-950C14.3","C14orf1","RP11-488C13.5","GSTZ1","AHSA1","SNW1","SEL1L","SPATA7","TTC8","TRIP11","ATXN3","IFI27","DICER1","TCL1B","TCL1A","RP11-164H13.1","HSP90AA1","ANKRD9","CDC42BPB","TNFAIP2","EIF5","TRMT61A","BAG5","SIVA1","PLD4","C14orf80","AL928768.3","KIAA0125","UBE3A","ATP10A","KLF13","RP11-1000B6.3","ARHGAP11A","DPH6","THBS1","EIF2AK4","PLCB2","C15orf57","RPUSD2","RMDN3","TMEM87A","ZNF106","TMEM62","ADAL","PDIA3","CASC4","TRIM69","USP8","SPPL2A","RP11-430B1.2","ARPP19","RP11-139H15.1","TCF12","LINC00926","ADAM10","MYO1E","BNIP2","NARG2","RP11-219B17.1","RORA","VPS13C","TPM1","RAB8B","APH1B","FAM96A","KIAA0101","CLPX","AAGAB","C15orf61","FEM1B","EDC3","COMMD4","NEIL1","SIN3A","SCAPER","RCN2","HMG20A","ST20","BCL2A1","MESDC2","IL16","WHAMM","HOMER2","FANCI","IDH2","UNC45A","HDDC3","CHD2","ARRDC4","LYSMD4","TARSL2","POLR3K","HBA1","LUC7L","RAB40C","C16orf13","STUB1","FAM173A","RPUSD1","NME3","HAGH","NDUFB10","GFER","IL32","ZNF263","CORO7","DNAJA3","NUDT16L1","CARHSP1","USP7","ATF7IP2","DEXI","LITAF","TNFRSF17","PDXDC1","FOPNL","COQ7","CCP110","IGSF6","C16orf52","POLR3E","GGA2","EARS2","PRKCB","RBBP6","RABEP2","CD19","SPNS1","SNX29P2","C16orf54","TMEM219","TAOK2","YPEL3","SEPHS2","ZNF747","ZNF688","PHKG2","STX4","FUS","PYCARD","TGFB1I1","C16orf58","RP11-42I10.1","CNEP1R1","SNX20","AKTIP","BBS2","CPNE2","ARL2BP","CIAPIN1","GPR56","KIFC3","C16orf80","GOT2","FAM96B","CES4A","TMEM208","ACD","TANGO6","CYB5B","NFAT5","ST3GAL2","RP11-432I5.1","ATXN1L","HP","PSMD7","RP11-252A24.3","GABARAPL2","KARS","TERF2IP","COTL1","C16orf74","IRF8","BANP","CYBA","MVD","SPG7","ZNF276","TIMM22","YWHAE","PITPNA-AS1","PRPF8","SERPINF1","SMYD4","PAFAH1B1","SHPK-1","P2RX5","ARRB2","PSMB6","GP1BA","SLC25A11","ZNF232","ZNF594","TXNDC17","ALOX12","CLEC10A","ASGR1","ACADVL","ELP5","ACAP1","TMEM102","TNFSF12","AC113189.5","SAT2","C17orf59","ADPRM","UBB","TRPV2","PLD6","NT5M","MED9","RASD1","PEMT","SREBF1","TRIM16L","GRAP","IFT20","POLDIP2","TMEM199","PROCA1","RAB34","NEK8","PHF12","ADAP2","NF1","UTP6","CCL2","SLFN5","SLFN12","CCL5","CCL3","CCL4","CCL4L1","GGNBP2","MRM1","AATF","TADA2A","MLLT6","RARA","CTD-2267D19.2","RP5-1028K7.2","DNAJC7","NKIRAS2","RP11-400F19.6","MLX","TUBG2","VPS25","RUNDC1","IFI35","NBR1","DHX8","RP11-527L4.5","HDAC5","GRN","ITGA2B","HEXIM2","RP11-798G7.6","GOSR2","ITGB3","OSBPL7","ABI3","PDK2","SAMD14","LRRC59","ABCC3","LUC7L3","MMD","NOG","SCPEP1","MRPS23","BZRAP1-AS1","SEPT4","YPEL2","INTS2","CYB561","LIMD2","PSMC5","CD79B","ICAM2","TEX2","NOL11","ARSG","CTD-2006K23.1","NAT9","TMEM104","ATP5H","SLC16A5","ARMC7","NT5C","GGA3","GRB2","SMIM5","GALK1","SPHK1","USP36","RNF213","RP11-1055B8.7","OXLD1","ARL16","ARHGDIA","STRA13","RFNG","GPS1","C17orf62","FN3KRP","YES1","SMCHD1","RP13-270P17.3","RALBP1","VAPA","PSMG2","OSBPL1A","RNF125","ZNF397","INO80C","ATP5A1","CXXC1","NARS","MALT1","RP11-879F14.2","TMX3","NFATC1","RBCK1","RP11-314N13.3","SIRPG","SNRPB","ITPA","C20orf27","PRNP","PCNA","CRLS1","ANKEF1","MKKS","TASP1","SNX5","RIN2","NAA20","CD93","CST3","CST7","ABHD12","PLAGL2","SNTA1","NECAB3","PXMP4","RALY","PIGU","UQCC1","RBM39","AAR2","MYL9","RPN2","BLCAP","CTNNBL1","MAFB","SRSF6","TTPAL","PKIG","YWHAB","DNTTIP1","UBE2C","MMP9","LINC00494","STAU1","CEBPB","KCNG1","ATP9A","FAM210B","RTFDC1","ZBP1","PMEPA1","STX16","TUBB1","PSMA7","DIDO1","GID8","TPD52L2","TCEA2","RGS19","PRSS57","ELANE","POLR2E","ATP5D","C19orf24","MUM1","ATP8B3","PLEKHJ1","GADD45B","SLC39A3","NFIC","DOHH","TBXA2R","SIRT6","CCDC94","FEM1A","SAFB2","DUS3L","VMAC","ALKBH7","DENND1C","PCP2","RETN","MAP2K7","CD320","RAB11B","MARCH2","HNRNPM","ZNF559","ZNF561","CDC37","S1PR5","ILF3-AS1","ILF3","C19orf52","SMARCA4","TMEM205","ZNF653","ECSIT","ELOF1","ZNF69","ZNF799","FBXW9","HOOK2","RAD23A","IL27RA","SAMD1","PRKACA","ZNF333","AC004257.3","AKAP8","TPM4","FAM32A","CHERP","SMIM7","MYO9B","BABAM1","MVB12A","SLC27A1","COLGALT1","INSL3","CCDC124","IFI30","JUND","ELL","CRTC1","RFXANK","NR2C2AP","ZNF682","ZNF493","LINC00662","PLEKHF1","ANKRD27","LSM14A","PDCD2L","ARHGAP33","TYROBP","POLR2I","TBCB","AC092295.7","AC074138.3","ZNF567","PPP1R14A","C19orf33","KCNK6","RASGRP4","ACTN4","CAPN12","HNRNPL","MRPS12","SAMD4B","EID2","PSMC4","TMEM91","ATP5SL","CEACAM4","CD79A","CTC-378H22.1","ETHE1","ZNF404","ZNF45","TOMM40","RELB","PRKD2","BBC3","C5AR1","C5AR2","DHX34","NAPA-AS1","PLA2G4C","CYTH2","GYS1","TRPM4","CD37","FCGRT","PRRG2","IL4I1","SPIB","NKG7","ZNF175","SIGLEC14","FPR1","ZNF350","ZNF615","ZNF841","ZNF836","ZNF528","ZNF468","MYADM","OSCAR","PRPF31","MBOAT7","LILRA4","LAIR1","KIR2DL3","KIR3DL2","FCAR","PPP6R1","SUV420H2","ISOC2","ZNF628","CTD-2537I9.12","EPN1","AURKC","CTC-444N24.11","ZNF587B","ZNF256","CTD-2368P22.1","AC010642.1","EIF1AY","TPTEP1","CECR5","PEX26","MRPL40","CLDN5","SEPT5","ARVCF","PPIL2","IGLL5","DDT","GUCD1","EWSR1","GAS2L1","NEFH","ZMAT5","RFPL2","FBXO7","HMGXB4","EIF3D","PVALB","MPST","LGALS2","SH3BP1","LGALS1","ANKRD54","DDX17","APOBEC3A","APOBEC3B","APOBEC3G","RPS19BP1","ADSL","PHF5A","DESI1","WBP2NL","SMDT1","PACSIN2","PARVB","LDOC1L","PRR5","ATXN10","TTC38","CTA-29F11.1","CRELD2","LMF2","TYMP","RABL2B","SAMSN1","USP25","MAP3K7CL","BACH1","MIS18A","ATP5O","KCNE1","TTC3","DSCR3","BRWD1","BACE2","SIK1","C21orf33","ICOSLG","SUMO3","SLC19A1","S100B","PRMT2" // ] // - // let ideogram = Ideogram.initRelatedGenes(config, annotsInList) + let ideogram = Ideogram.initRelatedGenes(config, annotsInList) - let ideogram; + // let ideogram; // A switch to expose internal timing data, to aid performance engineering if ('debug' in urlParams) config.debug = true; if ('cold' in urlParams === false) { if ('q' in urlParams) { - ideogram = Ideogram.initRelatedGenes(config); + ideogram = Ideogram.initRelatedGenes(config, annotsInList); } else { config.legendName = 'Gene leads' // config.legendContent = '' @@ -289,7 +290,7 @@

Gene LeadsEnrich your gene search

ideogram = Ideogram.initGeneLeads(config); } } else { - ideogram = Ideogram.initRelatedGenes(config); + ideogram = Ideogram.initRelatedGenes(config, annotsInList); } } else { diff --git a/src/js/annotations/draw.js b/src/js/annotations/draw.js index 986feb9b..0b5fbdf5 100644 --- a/src/js/annotations/draw.js +++ b/src/js/annotations/draw.js @@ -46,6 +46,11 @@ function drawAnnots(friendlyAnnots, layout, keep=false, isOtherLayout=false) { ideo = this, chrs = ideo.chromosomes[ideo.config.taxid]; // TODO: multiorganism + if (friendlyAnnots.length === 0) { + ideo.annots = []; + return; + } + if ( 'annots' in friendlyAnnots[0] || // When filtering 'values' in friendlyAnnots[0] // When drawing cached expression matrices diff --git a/src/js/annotations/legend.js b/src/js/annotations/legend.js index 942fd168..b04d44c8 100644 --- a/src/js/annotations/legend.js +++ b/src/js/annotations/legend.js @@ -52,7 +52,7 @@ function getListItems(labels, svg, list, nameHeight, ideo) { for (i = 0; i < list.rows.length; i++) { row = list.rows[i]; - labels += '
  • ' + row.name + '
  • '; + labels += '
  • ' + row.name + '
  • '; y = lineHeight * (i - 1) + nameHeight + 1; if ('name' in list) y += lineHeight; icon = getIcon(row, ideo); @@ -94,7 +94,8 @@ function writeLegend(ideo) { `left: -${nameHeight - 5}px;"`; } if ('name' in list) { - labels = `
    ` + list.name + `
    `; + labels = + `
  • ` + list.name + `
  • `; } svg = ''; [labels, svg] = getListItems(labels, svg, list, nameHeight, ideo); diff --git a/src/js/kit/protein-color.js b/src/js/kit/protein-color.js index 0bf001f4..4c4aec05 100644 --- a/src/js/kit/protein-color.js +++ b/src/js/kit/protein-color.js @@ -194,6 +194,7 @@ export function getColors(domainType) { domainType.includes('(G-protein), alpha subunit') || domainType === 'SCAN domain' || domainType === 'Apolipoprotein A/E' || + domainType.includes('Clusterin') || // a.k.a. apolipoprotein J, e.g. CLU domainType.includes('SMAD domain') || domainType === 'PLAC' || domainType.endsWith('tripeptidyl peptidase II') || @@ -366,6 +367,7 @@ export function getColors(domainType) { domainType === 'Phosphopantetheine binding ACP domain' || domainType === 'Multicopper oxidase, second cupredoxin domain' || domainType === 'Helicase, C-terminal' || + domainType.includes('Carboxylesterase') || // e.g. ACHE domainType.includes('presequence') || // e.g. ALAS2 domainType.endsWith('CC3') // e.g. PRKDC ) { @@ -588,6 +590,7 @@ export function getColors(domainType) { domainType.includes('TATA') || domainType.includes('Citron') || domainType === 'RIH domain' || + domainType.toLowerCase().includes('sclerostin') || // e.g. SOSTDC1 domainType === 'ZU5 domain' // e.g. TJP1 ) { return [yellow, yellowLine]; @@ -601,6 +604,7 @@ export function getColors(domainType) { domainType === 'DIX domain' || domainType === 'Ferritin-like diiron domain' || domainType === '4Fe-4S dicluster domain' || + domainType === 'Transferrin-like domain' || // iron-related, e.g. MELTF domainType === 'PAS domain' || domainType === 'PAS fold' || domainType === 'Polyketide synthase, dehydratase domain' || @@ -646,6 +650,7 @@ export function getColors(domainType) { domainType === 'VWF/SSPO/Zonadhesin-like, cysteine-rich domain' || // E.g. VWF domainType === 'Kappa casein' || + domainType === 'Casein, alpha/beta' || domainType === 'Natriuretic peptide' || domainType === 'EMI domain' || domainType === 'Neurohypophysial hormone' || @@ -696,6 +701,7 @@ export function getColors(domainType) { domainType.includes('Shisa') || domainType === 'ABC-2 family transporter protein' || domainType === 'Anoctamin' || + domainType === 'Sodium-dependent phosphate transport protein' || // SLC34A2 domainType.includes('MHC II-interacting') // e.g. CD74 ) { return [darkBrown, darkBrownLine]; @@ -775,10 +781,12 @@ export function getColors(domainType) { domainType.endsWith('coiled-coil region') || domainType.includes('zinc ribbon fold') || domainType.includes('zinc-ribbon') || + domainType.toLowerCase().includes('anhydrase') || // e.g. CA6 domainType === 'Macroglobulin domain' || domainType.includes('KH0') || domainType.includes('EGF') || - domainType.toLowerCase().includes('olfact') + domainType.toLowerCase().includes('olfact') || + domainType.toLowerCase().includes('mucin') // e.g. CEL ) { return [seafoam, seafoamLine]; } else if ( @@ -911,7 +919,8 @@ export function getColors(domainType) { domainType.toLowerCase().includes('decarboxylase') || domainType.toLowerCase().includes('hydrolase') || domainType.includes('PH domain') || - domainType.includes('cytosolic') + domainType.includes('cytosolic') || + domainType.toLowerCase().includes('homeobox') ) { return [lightBlue, lightBlueLine]; } diff --git a/src/js/kit/related-genes.js b/src/js/kit/related-genes.js index db0c422d..8616597e 100644 --- a/src/js/kit/related-genes.js +++ b/src/js/kit/related-genes.js @@ -75,10 +75,6 @@ function setRelatedAnnotDomIds(ideo) { // Arrange related annots by chromosome const annotsByChr = {}; ideo.annots.forEach((annot) => { - // if (annot.chr in annotsByChr) { - // annotsByChr[annot.chr].push(annot); - // } else { - const relevanceSortedAnnots = annot.annots.sort((a, b) => { return -ideo.annotSortFunction(a, b); }); @@ -110,6 +106,7 @@ function setRelatedAnnotDomIds(ideo) { Object.entries(annotsByChr).forEach(([chr, annots]) => { updatedAnnots[chr] = {chr, annots: []}; annots.forEach((annot, annotIndex) => { + // Annots have DOM IDs keyed by chromosome index and annotation index. // We reconstruct those here using structures built in two blocks above. const chrIndex = sortedChrNames.indexOf(chr); @@ -666,6 +663,7 @@ async function fetchParalogPositionsFromMyGeneInfo( } function drawNeighborhoods(neighborhoodAnnots, ideo) { + neighborhoodAnnots = applyAnnotsIncludeList(neighborhoodAnnots, ideo); ideo.drawAnnots(neighborhoodAnnots, 'overlay', true, true); moveLegend(ideo); } @@ -983,7 +981,8 @@ function initInteractiveLegend(ideo) { function dehighlight() { dehighlightAll(ideo); } - document.querySelectorAll('#_ideogramLegend li').forEach(li => { + const entrySelector = '#_ideogramLegend li._ideoLegendEntry'; + document.querySelectorAll(entrySelector).forEach(li => { // li.addEventListener('click', toggleHighlight); // WIP: 14373b18319e99febd91816fbc0c1b2e0f20f277 li.addEventListener('mouseenter', highlight); @@ -1058,9 +1057,12 @@ function moveLegend(ideo, extraPad=0) { initInteractiveLegend(ideo); } -/** Filter annotations to only include those in configured list */ +/** + * Filter annotations to only include those in configured list + * + * @return {List} includedAnnots List of filtered annots objects + */ function applyAnnotsIncludeList(annots, ideo) { - if (ideo.config.annotsInList === 'all') return annots; const includedAnnots = []; @@ -1072,6 +1074,33 @@ function applyAnnotsIncludeList(annots, ideo) { return includedAnnots; } +function filterByAnnotsIncludeList(annots, ideo) { + if (ideo.config.annotsInList === 'all') return annots; + + const annotsInList = ideo.config.annotsInList; + + const updated = []; + const updatedAnnots = {}; + ideo.annots.forEach(chrAnnots => { + const {chr, annots} = chrAnnots; + updatedAnnots[chr] = {chr, annots: []}; + annots.forEach((annot) => { + + const lcAnnotName = annot.name.toLowerCase(); + if ( + 'relatedAnnots' in ideo && + !annotsInList.includes(lcAnnotName) + ) { + return; + } + + updatedAnnots[chr].annots.push(annot); + }); + updated.push(updatedAnnots[chr]); + }); + return updated; +} + /** Fetch and draw interacting genes, return Promise for annots */ function processInteractions(annot, ideo) { return new Promise(async (resolve) => { @@ -1199,8 +1228,8 @@ function onBeforeDrawAnnots() { const ideo = this; setRelatedAnnotDomIds(ideo); + // Handle differential expression extension const chrAnnots = ideo.annots; - for (let i = 0; i < chrAnnots.length; i++) { const annots = chrAnnots[i].annots; @@ -1217,6 +1246,11 @@ function onBeforeDrawAnnots() { } } +function filterAndDrawAnnots(annots, ideo) { + annots = applyAnnotsIncludeList(annots, ideo); + ideo.drawAnnots(annots); +} + /** Filter, sort, draw annots. Move legend. */ function finishPlotRelatedGenes(type, ideo) { @@ -1228,7 +1262,7 @@ function finishPlotRelatedGenes(type, ideo) { annots = mergeAnnots(annots); - ideo.drawAnnots(annots); + filterAndDrawAnnots(annots, ideo); if (ideo.config.showAnnotLabels) { const sortedAnnots = ideo.flattenAnnots().sort((a, b) => { @@ -1929,7 +1963,7 @@ function initSearchIdeogram(kitDefaults, config, annotsInList) { if (annotsInList !== 'all') { annotsInList = annotsInList.map(name => name.toLowerCase()); } - kitDefaults.annotsInList = annotsInList; + config.annotsInList = annotsInList; kitDefaults.legendPad = kitDefaults.showAnnotLabels ? 80 : 30; @@ -1998,7 +2032,7 @@ function initSearchIdeogram(kitDefaults, config, annotsInList) { ideogram.onPlotFoundGenesCallback = config.onPlotFoundGenes; } - // Called upon completing last plot, including all related genes + // Called upon hovering over a legend entry, e.g. "Interacting genes" if (config.onHoverLegend) { ideogram.onHoverLegendCallback = config.onHoverLegend; } diff --git a/test/offline/core.test.js b/test/offline/core.test.js index 77e3aa85..ec318e6f 100644 --- a/test/offline/core.test.js +++ b/test/offline/core.test.js @@ -166,10 +166,10 @@ describe('Ideogram', function() { function callback() { var numAnnots = document.getElementsByClassName('annot').length; assert.equal(numAnnots, 1000); - var numLegendRows = document.querySelectorAll('#_ideogramLegend li').length; + var numLegendRows = document.querySelectorAll('._ideoLegendEntry').length; assert.equal(numLegendRows, 3); - var legendNameDom = document.querySelector('#_ideogramLegend div'); + var legendNameDom = document.querySelector('._ideoLegendName'); const height = parseFloat(legendNameDom.style.height.replace('px', '')); const expectedHeight = 19.12; const heightDiff = Math.abs(expectedHeight - height); diff --git a/test/offline/synteny.test.js b/test/offline/synteny.test.js index 6e8f732f..d591a20e 100644 --- a/test/offline/synteny.test.js +++ b/test/offline/synteny.test.js @@ -197,8 +197,8 @@ describe('Ideogram synteny support', function() { var numSyntenicRegions = document.getElementsByClassName('syntenicRegion').length; assert.equal(numSyntenicRegions, 25); - var srID = '#chr1-10090_54961724_56447175___chr2-10090_6000000_6500000'; - var otherSrID = '#chr1-10090_54961724_56447175___chr2-10090_12000000_13000000'; + var srID = '#chr1-10090_54516053_55989459___chr2-10090_6000000_6500000'; + var otherSrID = '#chr1-10090_54516053_55989459___chr2-10090_12000000_13000000'; var sr = d3.select(srID); var otherSr = d3.select(otherSrID); @@ -209,6 +209,7 @@ describe('Ideogram synteny support', function() { sr.dispatch('click'); var otherSrIsHidden = /hidden/.test(otherSr.nodes()[0].classList.value); + assert.equal(otherSrIsHidden, true); done(); diff --git a/test/online/related-genes.test.js b/test/online/related-genes.test.js index 168482fa..e7a232f3 100644 --- a/test/online/related-genes.test.js +++ b/test/online/related-genes.test.js @@ -357,6 +357,28 @@ describe('Ideogram related genes kit', function() { const ideogram = Ideogram.initRelatedGenes(config); }); + // If an app only wants to render specific genes (per `annotsInList`), + // and more than those specific genes are found interacting or paralogous, + // then ensure Ideogram doesn't display the unspecified genes + it('can omit genes not in specified list', done => { + + async function callback() { + await ideogram.plotRelatedGenes('CDK9'); + const annots = document.querySelectorAll('.annot'); + assert.equal(annots.length, 3); + done(); + } + + var config = { + organism: 'Homo sapiens', + onLoad: callback, + dataDir: '/dist/data/bands/native/' + }; + + const annotsInList = ['CDK9', 'CDK19', 'CDK1']; + const ideogram = Ideogram.initRelatedGenes(config, annotsInList); + }); + it('handles default display of highly cited genes', done => { function callback() {