From 724bbcfbb753836f7d763b25e58128c43cd8da40 Mon Sep 17 00:00:00 2001 From: jrobinso <933148+jrobinso@users.noreply.github.com> Date: Wed, 25 Oct 2023 21:19:04 -0700 Subject: [PATCH] refinements to hub genome & session -- genome => fewer initial tracks --- dev/ucsc/hub.html | 14 ++++- js/bigwig/trix.js | 1 - js/browser.js | 141 +++++++++++++++++--------------------------- js/genome/genome.js | 22 ++++--- js/ucsc/ucscHub.js | 85 ++++++++++++++------------ 5 files changed, 120 insertions(+), 143 deletions(-) diff --git a/dev/ucsc/hub.html b/dev/ucsc/hub.html index 0531e15fe..4465cc909 100644 --- a/dev/ucsc/hub.html +++ b/dev/ucsc/hub.html @@ -38,21 +38,29 @@ includeTracks: true } - const hub = await Hub.loadHub("https://hgdownload.soe.ucsc.edu/hubs/GCA/018/471/515/GCA_018471515.1/hub.txt", hubOptions) + const hub = await Hub.loadHub("https://hgdownload.soe.ucsc.edu/hubs/GCA/009/914/755/GCA_009914755.4/hub.txt", hubOptions) const ref = hub.getGenomeConfig() const igvConfig = { - locus: hub.getDefaultPosition(), - showChromosomeWidget: false, reference: ref } + // for(let tc of hub.getTrackConfigurations()) { + // for(let t of tc.tracks) { + // if(t.url.endsWith("undefined")) console.log(`${tc.label} ${t.name} ${t.url}`) + // } + // } + const browser = await igv.createBrowser(document.getElementById('igvDiv'), igvConfig) const selector = document.getElementById("select") selector.addEventListener("change", () => document.getElementById("hub-input").value = selector.value) + + document.getElementById("hub-input").value = "https://hgdownload.soe.ucsc.edu/hubs/GCA/009/914/755/GCA_009914755.4/hub.txt" + document.getElementById("load-genome").addEventListener("click", () => browser.loadGenome({url: document.getElementById("hub-input").value})) + document.getElementById("load-session").addEventListener("click", () => browser.loadSession({url: document.getElementById("hub-input").value})) diff --git a/js/bigwig/trix.js b/js/bigwig/trix.js index 8129bf4ef..1ad726211 100644 --- a/js/bigwig/trix.js +++ b/js/bigwig/trix.js @@ -51,7 +51,6 @@ export default class Trix { const match = word.startsWith(searchWord) if (match) { matches.push(line) - console.log("match " + line) } // we are done scanning if we are lexicographically greater than the search string if (word.slice(0, searchWord.length) > searchWord) { diff --git a/js/browser.js b/js/browser.js index 9ffe65eb6..1a567ccb7 100755 --- a/js/browser.js +++ b/js/browser.js @@ -448,8 +448,6 @@ class Browser { /** * Initialize a session from an object, json, or by loading from a file. * - * TODO Really should be split into at least 2 functions, load from file and load from object/json - * * @param options * @returns {*} */ @@ -463,14 +461,6 @@ class Browser { let session if (options.url || options.file) { session = await loadSessionFile(options) - - // Hack to accomodate ucsc Hubs. Refactor this -- hide based on genome characteristics, not session - if (this.config.showChromosomeWidget === false || session.showChromosomeWidget === false) { - this.chromosomeSelectWidget.hide() - } else { - this.chromosomeSelectWidget.show() - } - } else { session = options } @@ -499,13 +489,9 @@ class Browser { } else if (filename.endsWith("hub.txt")) { const hub = await Hub.loadHub(urlOrFile, options) - const genomeConfig = hub.getGenomeConfig(options.includeTracks) - const initialLocus = hub.getDefaultPosition() + const genomeConfig = hub.getGenomeConfig() const config = { - showChromosomeWidget: false, - locus: initialLocus, - reference: genomeConfig, - _isHub: true + reference: genomeConfig } return setDefaults(config) } else if (filename.endsWith(".json")) { @@ -517,7 +503,6 @@ class Browser { } } - /** * Note: public API function * @param session @@ -669,20 +654,23 @@ class Browser { } - createCenterLineList(columnContainer) { + cleanHouseForSession() { - const centerLines = columnContainer.querySelectorAll('.igv-center-line') - for (let i = 0; i < centerLines.length; i++) { - centerLines[i].remove() + for (let trackView of this.trackViews) { + // empty axis column, viewport columns, sampleName column, scroll column, drag column, gear column + trackView.removeDOMFromColumnContainer() } - const centerLineList = [] - const viewportColumns = columnContainer.querySelectorAll('.igv-column') - for (let i = 0; i < viewportColumns.length; i++) { - centerLineList.push(new ViewportCenterLine(this, this.referenceFrameList[i], viewportColumns[i])) + // discard all columns + const elements = this.columnContainer.querySelectorAll('.igv-axis-column, .igv-column-shim, .igv-column, .igv-sample-info-column, .igv-sample-name-column, .igv-scrollbar-column, .igv-track-drag-column, .igv-gear-menu-column') + elements.forEach(column => column.remove()) + + this.trackViews = [] + + if (this.circularView) { + this.circularView.clearChords() } - return centerLineList } /** @@ -702,20 +690,16 @@ class Browser { this.updateNavbarDOMWithGenome(genome) - // TODO -- I don't understand the genomeChange test. We always want to trigger a fresh start on loading a session or genome - //if (genomeChange) { this.removeAllTracks() - //} - let locus = getInitialLocus(initialLocus, genome) + let locus = initialLocus || genome.initialLocus + if (Array.isArray(locus)) { + locus = locus.join(' ') + } + const locusFound = await this.search(locus, true) if (!locusFound) { - console.log("Initial locus not found: " + locus) - locus = genome.getHomeChromosomeName() - const locusFound = await this.search(locus, true) - if (!locusFound) { - throw new Error("Cannot set initial locus") - } + throw new Error(`Cannot set initial locus ${locus}`) } if (genomeChange && this.circularView) { @@ -727,31 +711,23 @@ class Browser { } } - cleanHouseForSession() { - - for (let trackView of this.trackViews) { - // empty axis column, viewport columns, sampleName column, scroll column, drag column, gear column - trackView.removeDOMFromColumnContainer() - } - - // discard all columns - const elements = this.columnContainer.querySelectorAll('.igv-axis-column, .igv-column-shim, .igv-column, .igv-sample-info-column, .igv-sample-name-column, .igv-scrollbar-column, .igv-track-drag-column, .igv-gear-menu-column') - elements.forEach(column => column.remove()) - - this.trackViews = [] - - if (this.circularView) { - this.circularView.clearChords() - } - - } updateNavbarDOMWithGenome(genome) { let genomeLabel = (genome.id && genome.id.length < 20 ? genome.id : `${genome.id.substring(0, 8)}...${genome.id.substring(genome.id.length - 8)}`) this.$current_genome.text(genomeLabel) this.$current_genome.attr('title', genome.description) - if (this.config.showChromosomeWidget !== false) { + + // chromosome select widget -- Show this IFF its not explicitly hidden AND the genome has pre-loaded chromosomes + const showChromosomeWidget = + this.config.showChromosomeWidget !== false && + genome.getChromosomes().size > 1 && + (genome.wgChromosomeNames || genome.getChromosomes().size < 1000) + + if (showChromosomeWidget) { this.chromosomeSelectWidget.update(genome) + this.chromosomeSelectWidget.show() + } else { + this.chromosomeSelectWidget.hide() } } @@ -772,7 +748,7 @@ class Browser { genomeConfig = idOrConfig //await GenomeUtils.expandReference(this.alert, idOrConfig) } - await this.loadReference(genomeConfig, undefined) + await this.loadReference(genomeConfig) const tracks = genomeConfig.tracks || [] @@ -784,14 +760,6 @@ class Browser { await this.loadTrackList(tracks) - // Hack to accomodate ucsc Hubs. Refactor this -- hide based on genome characteristics, not session - if (this.config.showChromosomeWidget === false) { - this.chromosomeSelectWidget.hide() - } else { - this.chromosomeSelectWidget.show() - } - - await this.updateViews() return this.genome @@ -803,20 +771,9 @@ class Browser { * @returns {Promise} */ async loadTrackHub(options) { - const hub = await Hub.loadHub(options.url, options) - const genomeConfig = hub.getGenomeConfig() - const initialLocus = hub.getDefaultPosition() - if (initialLocus) { - const session = { - locus: initialLocus, - reference: genomeConfig - } - return this.loadSessionObject(session) - - } else { - return this.loadGenome(genomeConfig) - } + const genomeConfig = setDefaults(hub.getGenomeConfig()) + return this.loadGenome(genomeConfig) } /** @@ -1473,12 +1430,12 @@ class Browser { referenceFrame.end = referenceFrame.start + referenceFrame.bpPerPixel * width } - this.chromosomeSelectWidget.select.value = referenceFrameList.length === 1 ? this.referenceFrameList[0].chr : '' - + if (this.chromosomeSelectWidget) { + this.chromosomeSelectWidget.select.value = referenceFrameList.length === 1 ? this.referenceFrameList[0].chr : '' + } const loc = this.referenceFrameList.map(rf => rf.getLocusString()).join(' ') - this.$searchInput.val(loc) this.fireEvent('locuschange', [this.referenceFrameList]) @@ -1570,6 +1527,22 @@ class Browser { await this.updateViews(true) } + createCenterLineList(columnContainer) { + + const centerLines = columnContainer.querySelectorAll('.igv-center-line') + for (let i = 0; i < centerLines.length; i++) { + centerLines[i].remove() + } + + const centerLineList = [] + const viewportColumns = columnContainer.querySelectorAll('.igv-column') + for (let i = 0; i < viewportColumns.length; i++) { + centerLineList.push(new ViewportCenterLine(this, this.referenceFrameList[i], viewportColumns[i])) + } + + return centerLineList + } + async removeMultiLocusPanel(referenceFrame) { // find the $column corresponding to this referenceFrame and remove it @@ -2268,14 +2241,6 @@ function mouseUpOrLeave(e) { } -function getInitialLocus(locus, genome) { - if (locus) { - return Array.isArray(locus) ? locus.join(' ') : locus - } else { - return genome.getHomeChromosomeName() - } -} - function logo() { return $( diff --git a/js/genome/genome.js b/js/genome/genome.js index 31d95e52f..c6a226f37 100644 --- a/js/genome/genome.js +++ b/js/genome/genome.js @@ -107,8 +107,8 @@ class Genome { return Object.assign({}, this.config, {tracks: undefined}) } - getInitialLocus() { - + get initialLocus() { + return this.config.locus ? this.config.locus : this.getHomeChromosomeName() } getHomeChromosomeName() { @@ -141,21 +141,19 @@ class Genome { async loadChromosome(chr) { + let chromAliasRecord + if (this.chromAlias) { + chromAliasRecord = await this.chromAlias.search(chr) + chr = chromAliasRecord.chr + } + if (!this.chromosomes.has(chr)) { let chromosome - let sequenceRecord = await this.sequence.getSequenceRecord(chr) + const sequenceRecord = await this.sequence.getSequenceRecord(chr) if (sequenceRecord) { chromosome = new Chromosome(chr, 0, sequenceRecord.bpLength) - } else { - // Try alias - if (this.chromAlias) { - const chromAliasRecord = await this.chromAlias.search(chr) - if (chromAliasRecord) { - sequenceRecord = await this.sequence.getSequenceRecord(chromAliasRecord.chr) - chromosome = new Chromosome(chromAliasRecord.chr, 0, sequenceRecord.bpLength) - } - } } + this.chromosomes.set(chr, chromosome) // <= chromosome might be undefined, setting it prevents future attempts } diff --git a/js/ucsc/ucscHub.js b/js/ucsc/ucscHub.js index e1a41440a..37bedde8d 100644 --- a/js/ucsc/ucscHub.js +++ b/js/ucsc/ucscHub.js @@ -9,23 +9,35 @@ import {igvxhr} from "../../node_modules/igv-utils/src/index.js" class Hub { - static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred"]) + static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"]) static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps", "cpgIslandExtUnmasked", "windowMasker"]) static async loadHub(url, options) { + const idx = url.lastIndexOf("/") + const baseURL = url.substring(0, idx + 1) const stanzas = await loadStanzas(url, options) let groups if ("genome" === stanzas[1].type) { const genome = stanzas[1] if (genome.hasProperty("groups")) { - const idx = url.lastIndexOf("/") - const baseURL = url.substring(0, idx + 1) const groupsTxtURL = baseURL + genome.getProperty("groups") groups = await loadStanzas(groupsTxtURL) } } + + // load includes. Nested includes are not supported + for (let s of stanzas.slice()) { + if ("include" === s.type) { + const includeStanzas = await loadStanzas(baseURL + s.getProperty("include")) + for (s of includeStanzas) { + s.setProperty("visibility", "hide") + stanzas.push(s) + } + } + } + return new Hub(url, stanzas, groups) } @@ -59,8 +71,6 @@ class Hub { for (let i = 2; i < stanzas.length; i++) { if ("track" === stanzas[i].type) { this.trackStanzas.push(stanzas[i]) - } else { - console.warn(`Unexpected stanza type: ${stanzas[i].type}`) } } @@ -84,38 +94,26 @@ class Hub { // Organize track configs by group const trackConfigMap = new Map() for (let c of this.#getTracksConfigs()) { - const name = c.group || "other" - if (trackConfigMap.has(name)) { - trackConfigMap.get(name).push(c) + const groupName = c.group || "other" + if (trackConfigMap.has(groupName)) { + trackConfigMap.get(groupName).push(c) } else { - trackConfigMap.set(name, [c]) + trackConfigMap.set(groupName, [c]) } } // Build group structure - const t = [] - if (this.groupStanzas) { - for (let g of this.groupStanzas) { - const groupName = g.getProperty("name") - if(trackConfigMap.has(groupName)) { - t.push( - { - label: g.getProperty("label"), - tracks: trackConfigMap.get(g.getProperty("name")) - } - ) - } + const groupStanazMap = this.groupStanzas ? + new Map(this.groupStanzas.map(groupStanza => [groupStanza.getProperty("name"), groupStanza])) : + new Map() + + return Array.from(trackConfigMap.keys()).map(groupName => { + return { + label: groupStanazMap.has(groupName) ? groupStanazMap.get(groupName).getProperty("label") : groupName, + tracks: trackConfigMap.get(groupName) } - } - - if(trackConfigMap.has("other")) { - t.push({ - label: "other", - tracks: trackConfigMap.get("other") - }) - } + }) - return t } /* Example genome stanza @@ -136,7 +134,7 @@ transBlat dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1 isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1 */ - getGenomeConfig(includeTracks = "all") { + getGenomeConfig(includeTrackGroups = "all") { // TODO -- add blat? htmlPath? const config = { id: this.genomeStanza.getProperty("genome"), @@ -146,6 +144,10 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1 wholeGenomeView: false } + if (this.genomeStanza.hasProperty("defaultPos")) { + config.locus = this.genomeStanza.getProperty("defaultPos") + } + config.description = config.id if (this.genomeStanza.hasProperty("blat")) { @@ -165,6 +167,7 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1 // if (this.genomeStanza.hasProperty("chromSizes")) { // config.chromSizes = this.baseURL + this.genomeStanza.getProperty("chromSizes") // } + if (this.genomeStanza.hasProperty("description")) { config.description += `\n${this.genomeStanza.getProperty("description")}` } @@ -189,14 +192,17 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1 type bigBed 4 + bigDataUrl bbi/GCA_004027145.1_DauMad_v1_BIUU.cytoBand.bb */ - const cytoStanza = this.trackStanzas.filter(t => "cytoBandIdeo" === t.name && t.getProperty("bigDataUrl")) + const cytoStanza = this.trackStanzas.filter(t => "cytoBandIdeo" === t.name && t.hasProperty("bigDataUrl")) if (cytoStanza.length > 0) { config.cytobandBbURL = this.baseURL + cytoStanza[0].getProperty("bigDataUrl") } - // Tracks. To prevent loading tracks set `includeTracks`to false - if (includeTracks) { - config.tracks = this.#getTracksConfigs(Hub.filterTracks) + // Tracks. To prevent loading tracks set `includeTrackGroups`to false or "none" + if (includeTrackGroups && "none" !== includeTrackGroups) { + const filter = (t) => !Hub.filterTracks.has(t.name) && + "hide" !== t.getProperty("visibility") && + ("all" === includeTrackGroups || t.getProperty("group") === includeTrackGroups) + config.tracks = this.#getTracksConfigs(filter) } return config @@ -207,10 +213,7 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1 */ #getTracksConfigs(filter) { return this.trackStanzas.filter(t => { - return t.getProperty("visibility") !== "hide" && - Hub.supportedTypes.has(t.format) && - (!filter || !Hub.filterTracks.has(t.name) && - t.hasProperty("bigDataUrl")) + return Hub.supportedTypes.has(t.format) && t.hasProperty("bigDataUrl") && (!filter || filter(t)) }) .map(t => this.#getTrackConfig(t)) } @@ -246,6 +249,10 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1 "displayMode": t.displayMode, } + if ("vcfTabix" === format) { + config.indexURL = config.url + ".tbi" + } + if (t.hasProperty("longLabel") && t.hasProperty("html")) { if (config.description) config.description += "
" config.description =