From 7209cd63e48a81a6081cc93a5422043bb8671ab8 Mon Sep 17 00:00:00 2001 From: Jim Robinson <933148+jrobinso@users.noreply.github.com> Date: Wed, 25 Oct 2023 21:21:00 -0700 Subject: [PATCH] Hubs 3 (#1722) * fix problem with genome name aliasing * refinements to hub loading -- as "genome" => only sequence, cytoband, and tracks in group "genes" -- as "session" => all tracks in hubs.txt not marked "visibility=hide". --- dev/tnt/chm13v2.0.html | 77 -------------- dev/twobit/twobit_aliasBB.html | 52 ---------- dev/twobit/twobit_basic.html | 50 --------- dev/twobit/twobit_chromSizes.html | 37 ------- dev/twobit/twobit_t2t.html | 86 ---------------- dev/ucsc/hub.html | 14 ++- dev/{twobit => ucsc}/twobit.html | 0 dev/ucsc/twobit_t2t.html | 61 +++++++++++ dev/{twobit => ucsc}/twobithg38.html | 5 +- js/bigwig/trix.js | 1 - js/browser.js | 145 +++++++++++---------------- js/genome/genome.js | 35 +++---- js/ucsc/ucscHub.js | 85 +++++++++------- 13 files changed, 194 insertions(+), 454 deletions(-) delete mode 100644 dev/tnt/chm13v2.0.html delete mode 100644 dev/twobit/twobit_aliasBB.html delete mode 100644 dev/twobit/twobit_basic.html delete mode 100644 dev/twobit/twobit_chromSizes.html delete mode 100644 dev/twobit/twobit_t2t.html rename dev/{twobit => ucsc}/twobit.html (100%) create mode 100644 dev/ucsc/twobit_t2t.html rename dev/{twobit => ucsc}/twobithg38.html (86%) diff --git a/dev/tnt/chm13v2.0.html b/dev/tnt/chm13v2.0.html deleted file mode 100644 index 445b4aea3..000000000 --- a/dev/tnt/chm13v2.0.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - All demo - - - - -
-
-
- - - - - diff --git a/dev/twobit/twobit_aliasBB.html b/dev/twobit/twobit_aliasBB.html deleted file mode 100644 index ca25a799a..000000000 --- a/dev/twobit/twobit_aliasBB.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - twobit - - - - -
- - - - - - diff --git a/dev/twobit/twobit_basic.html b/dev/twobit/twobit_basic.html deleted file mode 100644 index 909d16e65..000000000 --- a/dev/twobit/twobit_basic.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - twobit - - - - -
- - - - - - diff --git a/dev/twobit/twobit_chromSizes.html b/dev/twobit/twobit_chromSizes.html deleted file mode 100644 index 6962b8e4c..000000000 --- a/dev/twobit/twobit_chromSizes.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - twobit - - - - -
- - - - - - diff --git a/dev/twobit/twobit_t2t.html b/dev/twobit/twobit_t2t.html deleted file mode 100644 index 078bd9bd3..000000000 --- a/dev/twobit/twobit_t2t.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - twobit - - - - -
- - - - - - 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/dev/twobit/twobit.html b/dev/ucsc/twobit.html similarity index 100% rename from dev/twobit/twobit.html rename to dev/ucsc/twobit.html diff --git a/dev/ucsc/twobit_t2t.html b/dev/ucsc/twobit_t2t.html new file mode 100644 index 000000000..f58e4f583 --- /dev/null +++ b/dev/ucsc/twobit_t2t.html @@ -0,0 +1,61 @@ + + + + twobit + + + + +
+ + + + + + diff --git a/dev/twobit/twobithg38.html b/dev/ucsc/twobithg38.html similarity index 86% rename from dev/twobit/twobithg38.html rename to dev/ucsc/twobithg38.html index 85ddca1f3..b8f034ddc 100644 --- a/dev/twobit/twobithg38.html +++ b/dev/ucsc/twobithg38.html @@ -22,10 +22,9 @@ "fastaURL": "http://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa", "indexURL": "http://s3.amazonaws.com/igv.broadinstitute.org/genomes/seq/hg38/hg38.fa.fai", "cytobandURL": "http://hgdownload.cse.ucsc.edu/goldenPath/hg38/database/cytoBandIdeo.txt.gz", - "aliasURL": "https://s3.amazonaws.com/igv.broadinstitute.org/annotations/hg38/hg38_alias.tab", - "format": "2bit", + "aliasURL": "https://hgdownload.cse.ucsc.edu/goldenPath/hg38/database/chromAlias.txt.gz", + "chromSizes": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.chrom.sizes", "twoBitURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.2bit", - "aliasBbURL": "https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.chromAlias.bb", "tracks": [ { "name": "Refseq Genes", 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 640273008..df6497f78 100755 --- a/js/browser.js +++ b/js/browser.js @@ -448,16 +448,11 @@ 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 {*} */ async loadSession(options) { - // UCSC hub hack - const chromosomeSelectWidget = this.chromosomeSelectWidget - this.sampleInfo.initialize() // TODO: deprecated @@ -487,7 +482,6 @@ class Browser { } if (filename.endsWith(".xml")) { - const knownGenomes = GenomeUtils.KNOWN_GENOMES const string = await igvxhr.loadString(urlOrFile) return new XMLSession(string, knownGenomes) @@ -495,15 +489,8 @@ class Browser { } else if (filename.endsWith("hub.txt")) { const hub = await Hub.loadHub(urlOrFile, options) - - if(chromosomeSelectWidget) { - chromosomeSelectWidget.hide() - } - const genomeConfig = hub.getGenomeConfig(options.includeTracks) - const initialLocus = hub.getDefaultPosition() + const genomeConfig = hub.getGenomeConfig() const config = { - showChromosomeWidget: false, - locus: initialLocus, reference: genomeConfig } return setDefaults(config) @@ -516,7 +503,6 @@ class Browser { } } - /** * Note: public API function * @param session @@ -668,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 } /** @@ -701,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() - //} + this.removeAllTracks() + + let locus = initialLocus || genome.initialLocus + if (Array.isArray(locus)) { + locus = locus.join(' ') + } - let locus = getInitialLocus(initialLocus, genome) 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) { @@ -726,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)}`) + 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() } } @@ -771,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 || [] @@ -794,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) } /** @@ -1424,22 +1390,22 @@ class Browser { } // An autoscaleGroup of only one (1) trackView has the lone trackView removed from group autoscale mode - const singleTonKeys = Object.keys(groupAutoscaleTrackViews).filter(key => 1 === groupAutoscaleTrackViews[ key ].length) + const singleTonKeys = Object.keys(groupAutoscaleTrackViews).filter(key => 1 === groupAutoscaleTrackViews[key].length) if (singleTonKeys.length > 0) { // Look for any single trackView groupAutoscale groups and move the single trackView to otherTrackViews list for (const key of singleTonKeys) { - for (const trackView of groupAutoscaleTrackViews[ key ]) { + for (const trackView of groupAutoscaleTrackViews[key]) { trackView.track.autoscaleGroup = undefined } - otherTrackViews.push(...groupAutoscaleTrackViews[ key ]) - delete groupAutoscaleTrackViews[ key ] + otherTrackViews.push(...groupAutoscaleTrackViews[key]) + delete groupAutoscaleTrackViews[key] } } // Calculate group autoscale dataRange if (Object.entries(groupAutoscaleTrackViews).length > 0) { - for (const [ group, trackViews] of Object.entries(groupAutoscaleTrackViews)) { + for (const [group, trackViews] of Object.entries(groupAutoscaleTrackViews)) { const featureArray = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures())) const dataRange = doAutoscale(featureArray.flat()) for (const trackView of trackViews) { @@ -1473,13 +1439,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]) @@ -1571,6 +1536,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 @@ -2269,14 +2250,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 4e2324a1d..c6a226f37 100644 --- a/js/genome/genome.js +++ b/js/genome/genome.js @@ -47,11 +47,10 @@ class Genome { } // For backward compatibility - if(this.chromosomes.size > 0) { + if (this.chromosomes.size > 0) { this.chromosomeNames = Array.from(this.chromosomes.keys()) } - if (config.chromAliasBbURL) { this.chromAlias = new ChromAliasBB(config.chromAliasBbURL, Object.assign({}, config), this) } else if (config.aliasURL) { @@ -60,7 +59,7 @@ class Genome { if (config.cytobandBbURL) { this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this) - } else if(config.cytobandURL) { + } else if (config.cytobandURL) { this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config)) } @@ -86,7 +85,6 @@ class Genome { } - get description() { return this.config.description || `${this.id}\n${this.name}` } @@ -109,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() { @@ -143,23 +141,20 @@ class Genome { async loadChromosome(chr) { + let chromAliasRecord + if (this.chromAlias) { + chromAliasRecord = await this.chromAlias.search(chr) + chr = chromAliasRecord.chr + } + if (!this.chromosomes.has(chr)) { - const sequenceRecord = await this.sequence.getSequenceRecord(chr) + let chromosome + const sequenceRecord = await this.sequence.getSequenceRecord(chr) if (sequenceRecord) { - const chromosome = new Chromosome(chr, 0, sequenceRecord.bpLength) - this.chromosomes.set(chr, chromosome) - } else { - // Try alias - if (this.chromAlias) { - const chromAliasRecord = await this.chromAlias.search(chr) - if (chromAliasRecord) { - const chromosome = new Chromosome(chromAliasRecord.chr, 0, sequenceRecord.bpLength) - this.chromosomes.set(chr, chromosome) - } - } - - this.chromosomes.set(chr, undefined) // Prevents future attempts + chromosome = new Chromosome(chr, 0, sequenceRecord.bpLength) } + + this.chromosomes.set(chr, chromosome) // <= chromosome might be undefined, setting it prevents future attempts } return this.chromosomes.get(chr) 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 =