diff --git a/js/feature/segTrack.js b/js/feature/segTrack.js index b73f6c311..a1c208bee 100755 --- a/js/feature/segTrack.js +++ b/js/feature/segTrack.js @@ -1,16 +1,14 @@ -import $ from "../vendor/jquery-3.3.1.slim.js" import FeatureSource from './featureSource.js' import TrackBase from "../trackBase.js" import IGVGraphics from "../igv-canvas.js" -import {IGVMath} from "../../node_modules/igv-utils/src/index.js" -import {createCheckbox} from "../igv-icons.js" -import {GradientColorScale} from "../util/colorScale.js" -import {ColorTable} from "../util/colorPalletes.js" +import { IGVMath } from "../../node_modules/igv-utils/src/index.js" +import { createCheckbox } from "../igv-icons.js" +import { GradientColorScale } from "../util/colorScale.js" +import { ColorTable } from "../util/colorPalletes.js" import SampleInfo from "../sample/sampleInfo.js" import HicColorScale from "../hic/hicColorScale.js" import ShoeboxSource from "../hic/shoeboxSource.js" -import {doSortByAttributes} from "../sample/sampleUtils.js" - +import { doSortByAttributes } from "../sample/sampleUtils.js" class SegTrack extends TrackBase { @@ -31,7 +29,7 @@ class SegTrack extends TrackBase { this.maxHeight = config.maxHeight || 500 this.squishedRowHeight = config.sampleSquishHeight || config.squishedRowHeight || 2 this.expandedRowHeight = config.sampleExpandHeight || config.expandedRowHeight || 13 - this.sampleHeight = this.squishedRowHeight // Initial value, will get overwritten when rendered + this.sampleHeight = this.squishedRowHeight // Initial value, will get overwritten when rendered // Explicitly set samples -- used to select a subset of samples from a dataset this.sampleKeys = [] @@ -45,7 +43,7 @@ class SegTrack extends TrackBase { // Color settings if (config.color) { - this.color = config.color // Overrides defaults, can be a function + this.color = config.color // Overrides defaults, can be a function } else if (config.colorTable) { this.colorTable = new ColorTable(config.colorTable) } else { @@ -63,7 +61,6 @@ class SegTrack extends TrackBase { } } - // Create featureSource // Disable whole genome downsampling unless explicitly. const configCopy = Object.assign({}, this.config) @@ -87,7 +84,7 @@ class SegTrack extends TrackBase { async postInit() { if (typeof this.featureSource.getHeader === "function") { this.header = await this.featureSource.getHeader() - if (this.disposed) return // This track was removed during async load + if (this.disposed) return // This track was removed during async load } // Set properties from track line if (this.header) { @@ -95,7 +92,6 @@ class SegTrack extends TrackBase { } } - menuItemList() { const menuItems = [] @@ -111,10 +107,10 @@ class SegTrack extends TrackBase { return attrs && attrs[attribute] })) { - const object = $('
') - object.html(`  ${attribute.split(SampleInfo.emptySpaceReplacement).join(' ')}`) + const object = document.createElement('div') + object.innerHTML = `  ${attribute.split(SampleInfo.emptySpaceReplacement).join(' ')}` - function attributeSort() { + object.addEventListener('click', () => { const sortDirection = this.#sortDirections.get(attribute) || 1 this.sortByAttribute(attribute, sortDirection) this.#sortDirections.set(attribute, sortDirection * -1) @@ -124,19 +120,18 @@ class SegTrack extends TrackBase { attribute: attribute, direction: sortDirection === 1 ? "ASC" : "DESC" } - } + }) - menuItems.push({object, click: attributeSort}) + menuItems.push({ object }) } } } - const lut = - { - "SQUISHED": "Squish", - "EXPANDED": "Expand", - "FILL": "Fill" - } + const lut = { + "SQUISHED": "Squish", + "EXPANDED": "Expand", + "FILL": "Fill" + } if (this.type === 'shoebox' && this.sbColorScale) { menuItems.push('
') @@ -155,7 +150,10 @@ class SegTrack extends TrackBase { }, e) } - menuItems.push({object: $('
Set color scale threshold
'), dialog: dialogPresentationHandler}) + const divElement = document.createElement('div'); + divElement.textContent = 'Set color scale threshold'; + divElement.addEventListener('click', dialogPresentationHandler.bind(this)); + menuItems.push({ object: divElement }); } menuItems.push('
') @@ -163,17 +161,17 @@ class SegTrack extends TrackBase { const displayOptions = this.type === 'seg' || this.type === 'shoebox' ? ["SQUISHED", "EXPANDED", "FILL"] : ["SQUISHED", "EXPANDED"] for (let displayMode of displayOptions) { const checkBox = createCheckbox(lut[displayMode], displayMode === this.displayMode) - menuItems.push( - { - object: $(checkBox), - click: function displayModeHandler() { - this.displayMode = displayMode - this.config.displayMode = displayMode - this.trackView.checkContentHeight() - this.trackView.repaintViews() - this.trackView.moveScroller(this.trackView.sampleNameViewport.trackScrollDelta) - } - }) + const object = document.createElement('div'); + object.appendChild(checkBox); + object.addEventListener('click', () => { + this.displayMode = displayMode + this.config.displayMode = displayMode + this.trackView.checkContentHeight() + this.trackView.repaintViews() + this.trackView.moveScroller(this.trackView.sampleNameViewport.trackScrollDelta) + }) + + menuItems.push({ object }) } return menuItems @@ -181,7 +179,7 @@ class SegTrack extends TrackBase { } hasSamples() { - return true // SEG, MUT, and MAF tracks have samples by definition + return true // SEG, MUT, and MAF tracks have samples by definition } getSamples() { @@ -193,7 +191,7 @@ class SegTrack extends TrackBase { } async getFeatures(chr, start, end) { - const features = await this.featureSource.getFeatures({chr, start, end}) + const features = await this.featureSource.getFeatures({ chr, start, end }) // New segments could conceivably add new samples this.updateSampleKeys(features) @@ -205,15 +203,14 @@ class SegTrack extends TrackBase { const sortDirection = "DESC" === sort.direction ? 1 : -1 this.sortByAttribute(sort.attribute, sortDirection) } - this.initialSort = undefined // Sample order is sorted, + this.initialSort = undefined // Sample order is sorted, } return features } + draw({ context, pixelTop, pixelWidth, pixelHeight, features, bpPerPixel, bpStart }) { - draw({context, pixelTop, pixelWidth, pixelHeight, features, bpPerPixel, bpStart}) { - - IGVGraphics.fillRect(context, 0, pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"}) + IGVGraphics.fillRect(context, 0, pixelTop, pixelWidth, pixelHeight, { 'fillStyle': "rgb(255, 255, 255)" }) if (features && features.length > 0) { @@ -221,7 +218,7 @@ class SegTrack extends TrackBase { if (this.type === "shoebox" && !this.sbColorScale) { const threshold = this.featureSource.hicFile.percentile95 || 2000 - this.sbColorScale = new HicColorScale({threshold, r: 0, g: 0, b: 255}) + this.sbColorScale = new HicColorScale({ threshold, r: 0, g: 0, b: 255 }) } // Create a map for fast id -> row lookup @@ -241,14 +238,14 @@ class SegTrack extends TrackBase { this.sampleHeight = this.squishedRowHeight border = 0 break - default: // EXPANDED + default: // EXPANDED this.sampleHeight = this.expandedRowHeight border = 1 } const rowHeight = this.sampleHeight for (let segment of features) { - segment.pixelRect = undefined // !important, reset this in case segment is not drawn + segment.pixelRect = undefined // !important, reset this in case segment is not drawn } const pixelBottom = pixelTop + pixelHeight @@ -333,7 +330,7 @@ class SegTrack extends TrackBase { } - f.pixelRect = {x, y, w, h} + f.pixelRect = { x, y, w, h } // Use for diagnostic rendering // context.fillStyle = randomRGB(180, 240) @@ -349,7 +346,6 @@ class SegTrack extends TrackBase { } - checkForLog(features) { if (this.isLog === undefined) { this.isLog = false @@ -363,8 +359,8 @@ class SegTrack extends TrackBase { } /** - * Optional method to compute pixel height to accomodate the list of features. The implementation below - * has side effects (modifiying the samples hash). This is unfortunate, but harmless. + * Optional method to compute pixel height to accommodate the list of features. The implementation below + * has side effects (modifying the samples hash). This is unfortunate, but harmless. * * Note displayMode "FILL" is handled by the viewport * @@ -393,9 +389,8 @@ class SegTrack extends TrackBase { end = sort.end } - if (!featureList) { - featureList = await this.featureSource.getFeatures({chr, start, end}) + featureList = await this.featureSource.getFeatures({ chr, start, end }) } if (!featureList) return @@ -531,7 +526,7 @@ class SegTrack extends TrackBase { label: sortLabel, click: () => { const sort = { - option: "VALUE", // Either VALUE or ATTRIBUTE + option: "VALUE", // Either VALUE or ATTRIBUTE direction, chr: clickState.referenceFrame.chr, start: Math.floor(genomicLocation - bpWidth), @@ -564,8 +559,8 @@ class SegTrack extends TrackBase { } // Default copy number scales -const POS_COLOR_SCALE = {low: 0.1, lowR: 255, lowG: 255, lowB: 255, high: 1.5, highR: 255, highG: 0, highB: 0} -const NEG_COLOR_SCALE = {low: -1.5, lowR: 0, lowG: 0, lowB: 255, high: -0.1, highR: 255, highG: 255, highB: 255} +const POS_COLOR_SCALE = { low: 0.1, lowR: 255, lowG: 255, lowB: 255, high: 1.5, highR: 255, highG: 0, highB: 0 } +const NEG_COLOR_SCALE = { low: -1.5, lowR: 0, lowG: 0, lowB: 255, high: -0.1, highR: 255, highG: 255, highB: 255 } // Mut and MAF file default color table @@ -613,5 +608,4 @@ const MUT_COLORS = { } - export default SegTrack diff --git a/js/trackViewport.js b/js/trackViewport.js index 80b675a4e..03e29549a 100644 --- a/js/trackViewport.js +++ b/js/trackViewport.js @@ -2,77 +2,74 @@ * Created by dat on 9/16/16. */ -import $ from "./vendor/jquery-3.3.1.slim.js" -import Popover from "./ui/popover.js" -import Viewport from "./viewport.js" -import {FileUtils} from "../node_modules/igv-utils/src/index.js" -import * as DOMUtils from "./ui/utils/dom-utils.js" -import C2S from "./canvas2svg.js" -import GenomeUtils from "./genome/genomeUtils.js" -import {bppSequenceThreshold} from "./sequenceTrack.js" +import Popover from "./ui/popover.js"; +import Viewport from "./viewport.js"; +import { FileUtils } from "../node_modules/igv-utils/src/index.js"; +import * as DOMUtils from "./ui/utils/dom-utils.js"; +import C2S from "./canvas2svg.js"; +import GenomeUtils from "./genome/genomeUtils.js"; +import { bppSequenceThreshold } from "./sequenceTrack.js"; -const NOT_LOADED_MESSAGE = 'Error loading track data' +const NOT_LOADED_MESSAGE = 'Error loading track data'; -let mouseDownCoords -let lastClickTime = 0 -let lastHoverUpdateTime = 0 -let popupTimerID -let trackViewportPopoverList = [] +let mouseDownCoords; +let lastClickTime = 0; +let lastHoverUpdateTime = 0; +let popupTimerID; +let trackViewportPopoverList = []; -let popover +let popover; class TrackViewport extends Viewport { - constructor(trackView, viewportColumn, referenceFrame, width) { - super(trackView, viewportColumn, referenceFrame, width) + super(trackView, viewportColumn, referenceFrame, width); } initializationHelper() { + this.spinnerContainer = document.createElement('div'); + this.spinnerContainer.className = 'igv-loading-spinner-container'; + this.viewportElement.appendChild(this.spinnerContainer); - this.$spinner = $('
', {class: 'igv-loading-spinner-container'}) - this.$viewport.append(this.$spinner) - this.$spinner.append($('
')) + const spinner = document.createElement('div'); + this.spinnerContainer.appendChild(spinner); - const track = this.trackView.track + const track = this.trackView.track; if ('sequence' !== track.type) { - this.$zoomInNotice = this.createZoomInNotice(this.$viewport) + this.zoomInNotice = this.createZoomInNotice(this.viewportElement); } if ("sequence" !== track.id) { - this.$trackLabel = $('
') - this.$viewport.append(this.$trackLabel) - this.setTrackLabel(track.name || "") + this.trackLabel = document.createElement('div'); + this.trackLabel.className = 'igv-track-label'; + this.viewportElement.appendChild(this.trackLabel); + this.setTrackLabel(track.name || ""); if (false === this.browser.doShowTrackLabels) { - this.$trackLabel.hide() + this.trackLabel.style.display = 'none'; } } - this.stopSpinner() - this.addMouseHandlers() - + this.stopSpinner(); + this.addMouseHandlers(); } setContentHeight(contentHeight) { - super.setContentHeight(contentHeight) - if (this.featureCache) this.featureCache.redraw = true + super.setContentHeight(contentHeight); + if (this.featureCache) this.featureCache.redraw = true; } setTrackLabel(label) { - - this.$trackLabel.empty() - this.$trackLabel.html(label) - - const txt = this.$trackLabel.text() - this.$trackLabel.attr('title', txt) + this.trackLabel.innerHTML = label; + const txt = this.trackLabel.textContent; + this.trackLabel.setAttribute('title', txt); } startSpinner() { - this.$spinner.show() + this.spinnerContainer.style.display = 'block'; } stopSpinner() { - if (this.$spinner) { - this.$spinner.hide() + if (this.spinnerContainer) { + this.spinnerContainer.style.display = 'none'; } } @@ -84,74 +81,70 @@ class TrackViewport extends Viewport { * @returns {boolean} true if we are zoomed in past visibility window, false otherwise */ checkZoomIn() { - const zoomedOutOfWindow = () => { if (this.referenceFrame.chr.toLowerCase() === "all" && !this.trackView.track.supportsWholeGenome) { - return true + return true; } else { - const visibilityWindow = this.trackView.track.visibilityWindow + const visibilityWindow = this.trackView.track.visibilityWindow; return ( visibilityWindow !== undefined && visibilityWindow > 0 && - (this.referenceFrame.bpPerPixel * this.$viewport.width() > visibilityWindow)) + (this.referenceFrame.bpPerPixel * this.viewportElement.offsetWidth > visibilityWindow)); } - } + }; if (this.trackView.track && "sequence" === this.trackView.track.type && this.referenceFrame.bpPerPixel > bppSequenceThreshold) { - $(this.canvas).remove() - this.canvas = undefined - //this.featureCache = undefined - return false + if (this.canvas) { + this.viewportElement.removeChild(this.canvas); + this.canvas = undefined; + } + return false; } if (!(this.viewIsReady())) { - return false + return false; } - if (zoomedOutOfWindow()) { - // Out of visibility window if (this.canvas) { - $(this.canvas).remove() - this.canvas = undefined - //this.featureCache = undefined + this.viewportElement.removeChild(this.canvas); + this.canvas = undefined; } if (this.trackView.track.autoHeight) { - const minHeight = this.trackView.minHeight || 0 - this.setContentHeight(minHeight) + const minHeight = this.trackView.minHeight || 0; + this.setContentHeight(minHeight); } - if (this.$zoomInNotice) { - this.$zoomInNotice.show() + if (this.zoomInNotice) { + this.zoomInNotice.style.display = 'block'; } - return false + return false; } else { - if (this.$zoomInNotice) { - this.$zoomInNotice.hide() + if (this.zoomInNotice) { + this.zoomInNotice.style.display = 'none'; } - return true + return true; } - } /** * Adjust the canvas to the current genomic state. */ shift() { - const referenceFrame = this.referenceFrame + const referenceFrame = this.referenceFrame; if (this.canvas && this.canvas._data && this.canvas._data.referenceFrame.chr === this.referenceFrame.chr && this.canvas._data.bpPerPixel === referenceFrame.bpPerPixel) { - this.canvas._data.pixelShift = Math.round((this.canvas._data.bpStart - referenceFrame.start) / referenceFrame.bpPerPixel) - this.canvas.style.left = this.canvas._data.pixelShift + "px" + this.canvas._data.pixelShift = Math.round((this.canvas._data.bpStart - referenceFrame.start) / referenceFrame.bpPerPixel); + this.canvas.style.left = this.canvas._data.pixelShift + "px"; } } genomicRange() { return { start: this.referenceFrame.start, - end: this.referenceFrame.start + this.referenceFrame.bpPerPixel * this.$viewport.width() - } + end: this.referenceFrame.start + this.referenceFrame.bpPerPixel * this.viewportElement.offsetWidth + }; } /** @@ -160,90 +153,87 @@ class TrackViewport extends Viewport { * * @param contentTop - the "top" property of the virtual content div, 0 unless track is scrolled vertically * - * */ setTop(contentTop) { - - super.setTop(contentTop) + super.setTop(contentTop); if (!this.canvas) { - this.repaint() + this.repaint(); } else { // See if currently painted canvas covers the vertical range of the viewport. If not repaint - const h = this.$viewport.height() - const vt = contentTop + this.canvas._data.pixelTop - const vb = vt + this.canvas._data.pixelHeight + const h = this.viewportElement.offsetHeight; + const vt = contentTop + this.canvas._data.pixelTop; + const vb = vt + this.canvas._data.pixelHeight; if (vt > 0 || vb < h) { - this.repaint() + this.repaint(); } } // If data is loaded, offset backing canvas to align with the contentTop visual offset. If not data has // been loaded canvas will be undefined - if(this.canvas && this.canvas._data) { - let offset = contentTop + this.canvas._data.pixelTop - this.canvas.style.top = `${offset}px` + if (this.canvas && this.canvas._data) { + let offset = contentTop + this.canvas._data.pixelTop; + this.canvas.style.top = `${offset}px`; } } async loadFeatures() { - - const referenceFrame = this.referenceFrame - const chr = referenceFrame.chr + const referenceFrame = this.referenceFrame; + const chr = referenceFrame.chr; // Expand the requested range so we can pan a bit without reloading. But not beyond chromosome bounds - const chromosome = await this.browser.genome.loadChromosome(chr) - const chrLength = chromosome ? chromosome.bpLength : Number.MAX_SAFE_INTEGER - const pixelWidth = this.$viewport.width()// * 3; - const bpWidth = pixelWidth * referenceFrame.bpPerPixel - const bpStart = Math.floor(Math.max(0, referenceFrame.start - bpWidth)) - const bpEnd = Math.ceil(Math.min(chrLength, referenceFrame.start + bpWidth + bpWidth)) // Add one screen width to end + const chromosome = await this.browser.genome.loadChromosome(chr); + const chrLength = chromosome ? chromosome.bpLength : Number.MAX_SAFE_INTEGER; + const pixelWidth = this.viewportElement.offsetWidth; + const bpWidth = pixelWidth * referenceFrame.bpPerPixel; + const bpStart = Math.floor(Math.max(0, referenceFrame.start - bpWidth)); + const bpEnd = Math.ceil(Math.min(chrLength, referenceFrame.start + bpWidth + bpWidth)); // Add one screen width to end if (this.loading && this.loading.start === bpStart && this.loading.end === bpEnd) { - return undefined + return undefined; } - this.loading = {start: bpStart, end: bpEnd} - this.startSpinner() + this.loading = { start: bpStart, end: bpEnd }; + this.startSpinner(); try { - const track = this.trackView.track - const features = await this.getFeatures(track, chr, bpStart, bpEnd, referenceFrame.bpPerPixel) + const track = this.trackView.track; + const features = await this.getFeatures(track, chr, bpStart, bpEnd, referenceFrame.bpPerPixel); if (features) { - let roiFeatures = [] + let roiFeatures = []; if (track.roiSets && track.roiSets.length > 0) { for (let roiSet of track.roiSets) { - const features = await roiSet.getFeatures(chr, bpStart, bpEnd, referenceFrame.bpPerPixel) - roiFeatures.push({track: roiSet, features}) + const roiFeatures = await roiSet.getFeatures(chr, bpStart, bpEnd, referenceFrame.bpPerPixel); + roiFeatures.push({ track: roiSet, features: roiFeatures }); } } - const mr = track && (track.resolutionAware) // - const windowFunction = this.windowFunction - this.featureCache = new FeatureCache(chr, bpStart, bpEnd, referenceFrame.bpPerPixel, features, roiFeatures, mr, windowFunction) - this.loading = false - this.hideMessage() - this.stopSpinner() - return this.featureCache + const mr = track && (track.resolutionAware); + const windowFunction = this.windowFunction; + this.featureCache = new FeatureCache(chr, bpStart, bpEnd, referenceFrame.bpPerPixel, features, roiFeatures, mr, windowFunction); + this.loading = false; + this.hideMessage(); + this.stopSpinner(); + return this.featureCache; } } catch (error) { // Track might have been removed during load if (this.trackView && this.trackView.disposed !== true) { - this.showMessage(NOT_LOADED_MESSAGE) - this.browser.alert.present(error) - console.error(error) + this.showMessage(NOT_LOADED_MESSAGE); + this.browser.alert.present(error); + console.error(error); } } finally { - this.loading = false - this.stopSpinner() + this.loading = false; + this.stopSpinner(); } } get track() { - return this.trackView.track + return this.trackView.track; } get windowFunction() { - return this.track ? this.track.windowFunction : undefined + return this.track ? this.track.windowFunction : undefined; } /** @@ -253,14 +243,14 @@ class TrackViewport extends Viewport { * @returns {{bpEnd: *, pixelWidth: (*|number), bpStart: number}} */ repaintDimensions() { - const isWGV = GenomeUtils.isWholeGenomeView(this.referenceFrame.chr) - const pixelWidth = isWGV ? this.$viewport.width() : 3 * this.$viewport.width() - const bpPerPixel = this.referenceFrame.bpPerPixel - const bpStart = this.referenceFrame.start - (isWGV ? 0 : this.$viewport.width() * bpPerPixel) - const bpEnd = isWGV ? Number.MAX_SAFE_INTEGER : this.referenceFrame.start + 2 * this.$viewport.width() * bpPerPixel + 1 + const isWGV = GenomeUtils.isWholeGenomeView(this.referenceFrame.chr); + const pixelWidth = isWGV ? this.viewportElement.offsetWidth : 3 * this.viewportElement.offsetWidth; + const bpPerPixel = this.referenceFrame.bpPerPixel; + const bpStart = this.referenceFrame.start - (isWGV ? 0 : this.viewportElement.offsetWidth * bpPerPixel); + const bpEnd = isWGV ? Number.MAX_SAFE_INTEGER : this.referenceFrame.start + 2 * this.viewportElement.offsetWidth * bpPerPixel + 1; return { bpStart, bpEnd, pixelWidth - } + }; } /** @@ -268,85 +258,83 @@ class TrackViewport extends Viewport { * */ repaint() { - if (undefined === this.featureCache) { - return + return; } - const {features, roiFeatures} = this.featureCache + const { features, roiFeatures } = this.featureCache; // Canvas dimensions. // For deep tracks we paint a canvas == 3*viewportHeight centered on the current vertical scroll position - const {bpStart, bpEnd, pixelWidth} = this.repaintDimensions() - const viewportHeight = this.$viewport.height() - const contentHeight = this.getContentHeight() - const maxHeight = roiFeatures ? Math.max(contentHeight, viewportHeight) : contentHeight // Need to fill viewport for ROIs. - const pixelHeight = Math.min(maxHeight, 3 * viewportHeight) + const { bpStart, bpEnd, pixelWidth } = this.repaintDimensions(); + const viewportHeight = this.viewportElement.offsetHeight; + const contentHeight = this.getContentHeight(); + const maxHeight = roiFeatures ? Math.max(contentHeight, viewportHeight) : contentHeight; // Need to fill viewport for ROIs. + const pixelHeight = Math.min(maxHeight, 3 * viewportHeight); if (0 === pixelWidth || 0 === pixelHeight) { if (this.canvas) { - $(this.canvas).remove() + this.viewportElement.removeChild(this.canvas); } - return - } - const pixelTop = Math.max(0, -this.contentTop - Math.floor(pixelHeight / 3)) - - const bpPerPixel = this.referenceFrame.bpPerPixel - const pixelXOffset = Math.round((bpStart - this.referenceFrame.start) / bpPerPixel) - const canvasTop = (this.contentTop || 0) + pixelTop - const newCanvas = document.createElement('canvas')// $('').get(0) - newCanvas.style.position = 'relative' - newCanvas.style.display = 'block' - newCanvas.style.width = pixelWidth + "px" - newCanvas.style.height = pixelHeight + "px" - newCanvas.style.left = pixelXOffset + "px" - newCanvas.style.top = canvasTop + "px" + return; + } + const pixelTop = Math.max(0, -this.contentTop - Math.floor(pixelHeight / 3)); + + const bpPerPixel = this.referenceFrame.bpPerPixel; + const pixelXOffset = Math.round((bpStart - this.referenceFrame.start) / bpPerPixel); + const canvasTop = (this.contentTop || 0) + pixelTop; + const newCanvas = document.createElement('canvas'); + newCanvas.className = 'igv-canvas'; + newCanvas.style.position = 'relative'; + newCanvas.style.display = 'block'; + newCanvas.style.width = pixelWidth + "px"; + newCanvas.style.height = pixelHeight + "px"; + newCanvas.style.left = pixelXOffset + "px"; + newCanvas.style.top = canvasTop + "px"; // Always use high DPI if in "FILL" display mode, otherwise use track setting; const devicePixelRatio = ("FILL" === this.trackView.track.displayMode || this.trackView.track.supportHiDPI !== false) ? - window.devicePixelRatio : 1 - newCanvas.width = devicePixelRatio * pixelWidth - newCanvas.height = devicePixelRatio * pixelHeight - - const ctx = newCanvas.getContext("2d") - ctx.scale(devicePixelRatio, devicePixelRatio) - ctx.translate(0, -pixelTop) - - const drawConfiguration = - { - context: ctx, - pixelXOffset, - pixelWidth, - pixelHeight, - pixelTop, - bpStart, - bpEnd, - bpPerPixel, - pixelShift: pixelXOffset, // Initial value, changes with track pan (drag) - windowFunction: this.windowFunction, - referenceFrame: this.referenceFrame, - selection: this.selection, - viewport: this, - viewportWidth: this.$viewport.width() - } + window.devicePixelRatio : 1; + newCanvas.width = devicePixelRatio * pixelWidth; + newCanvas.height = devicePixelRatio * pixelHeight; + + const ctx = newCanvas.getContext("2d"); + ctx.scale(devicePixelRatio, devicePixelRatio); + ctx.translate(0, -pixelTop); + + const drawConfiguration = { + context: ctx, + pixelXOffset, + pixelWidth, + pixelHeight, + pixelTop, + bpStart, + bpEnd, + bpPerPixel, + pixelShift: pixelXOffset, // Initial value, changes with track pan (drag) + windowFunction: this.windowFunction, + referenceFrame: this.referenceFrame, + selection: this.selection, + viewport: this, + viewportWidth: this.viewportElement.offsetWidth + }; - this.draw(drawConfiguration, features, roiFeatures) + this.draw(drawConfiguration, features, roiFeatures); if (this.canvas) { - $(this.canvas).remove() + this.viewportElement.removeChild(this.canvas); } - newCanvas._data = drawConfiguration - this.canvas = newCanvas - this.$viewport.append($(newCanvas)) - + newCanvas._data = drawConfiguration; + this.canvas = newCanvas; + this.viewportElement.appendChild(newCanvas); } refresh() { - if (!(this.canvas && this.featureCache)) return + if (!(this.canvas && this.featureCache)) return; - const drawConfiguration = this.canvas._data - drawConfiguration.context.clearRect(0, 0, this.canvas.width, this.canvas.height) - const {features, roiFeatures} = this.featureCache - this.draw(drawConfiguration, features, roiFeatures) + const drawConfiguration = this.canvas._data; + drawConfiguration.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + const { features, roiFeatures } = this.featureCache; + this.draw(drawConfiguration, features, roiFeatures); } /** @@ -357,488 +345,439 @@ class TrackViewport extends Viewport { * @param roiFeatures */ draw(drawConfiguration, features, roiFeatures) { - if (features) { - drawConfiguration.features = features - this.trackView.track.draw(drawConfiguration) + drawConfiguration.features = features; + this.trackView.track.draw(drawConfiguration); } if (roiFeatures && roiFeatures.length > 0) { for (let r of roiFeatures) { - drawConfiguration.features = r.features - r.track.draw(drawConfiguration) + drawConfiguration.features = r.features; + r.track.draw(drawConfiguration); } } } containsPosition(chr, position) { if (this.referenceFrame.chr === chr && position >= this.referenceFrame.start) { - return position <= this.referenceFrame.calculateEnd(this.getWidth()) + return position <= this.referenceFrame.calculateEnd(this.getWidth()); } else { - return false + return false; } } isLoading() { - return this.loading + return this.loading; } savePNG() { + if (!this.canvas) return; - if (!this.canvas) return - - const canvasMetadata = this.canvas._data - const canvasTop = canvasMetadata ? canvasMetadata.pixelTop : 0 - const devicePixelRatio = window.devicePixelRatio - const w = this.$viewport.width() * devicePixelRatio - const h = this.$viewport.height() * devicePixelRatio - const x = -$(this.canvas).position().left * devicePixelRatio - const y = (-this.contentTop - canvasTop) * devicePixelRatio + const canvasMetadata = this.canvas._data; + const canvasTop = canvasMetadata ? canvasMetadata.pixelTop : 0; + const devicePixelRatio = window.devicePixelRatio; + const w = this.viewportElement.offsetWidth * devicePixelRatio; + const h = this.viewportElement.offsetHeight * devicePixelRatio; + const x = -this.canvas.offsetLeft * devicePixelRatio; + const y = (-this.contentTop - canvasTop) * devicePixelRatio; - const ctx = this.canvas.getContext("2d") - const imageData = ctx.getImageData(x, y, w, h) - const exportCanvas = document.createElement('canvas') - const exportCtx = exportCanvas.getContext('2d') - exportCanvas.width = imageData.width - exportCanvas.height = imageData.height - exportCtx.putImageData(imageData, 0, 0) + const ctx = this.canvas.getContext("2d"); + const imageData = ctx.getImageData(x, y, w, h); + const exportCanvas = document.createElement('canvas'); + const exportCtx = exportCanvas.getContext('2d'); + exportCanvas.width = imageData.width; + exportCanvas.height = imageData.height; + exportCtx.putImageData(imageData, 0, 0); - // filename = this.trackView.track.name + ".png"; - const filename = (this.$trackLabel.text() ? this.$trackLabel.text() : "image") + ".png" - const data = exportCanvas.toDataURL("image/png") - FileUtils.download(filename, data) + const filename = (this.trackLabel.textContent ? this.trackLabel.textContent : "image") + ".png"; + const data = exportCanvas.toDataURL("image/png"); + FileUtils.download(filename, data); } saveSVG() { + const marginTop = 32; + const marginLeft = 32; - const marginTop = 32 - const marginLeft = 32 + let { width, height } = this.browser.columnContainer.getBoundingClientRect(); - let {width, height} = this.browser.columnContainer.getBoundingClientRect() + const h_render = 8000; - const h_render = 8000 - - const config = - { + const config = { + width, + height: h_render, + backdropColor: 'white', + multiLocusGap: 0, + viewbox: { + x: 0, + y: 0, width, - height: h_render, - backdropColor: 'white', - multiLocusGap: 0, - viewbox: - { - x: 0, - y: 0, - width, - height: h_render - } + height: h_render } + }; - const context = new C2S(config) + const context = new C2S(config); - const delta = - { - deltaX: marginLeft, - deltaY: marginTop - } + const delta = { + deltaX: marginLeft, + deltaY: marginTop + }; - this.renderSVGContext(context, delta, false) + this.renderSVGContext(context, delta, false); // reset height to trim away unneeded svg canvas real estate. Yes, a bit of a hack. - context.setHeight(height) + context.setHeight(height); - const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, '') - const index = this.browser.referenceFrameList.indexOf(this.referenceFrame) + const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, ''); + const index = this.browser.referenceFrameList.indexOf(this.referenceFrame); - const svg = context.getSerializedSvg(true) - const data = URL.createObjectURL(new Blob([svg], {type: "application/octet-stream"})) - - const id = `${str}_referenceFrame_${index}_guid_${DOMUtils.guid()}` - FileUtils.download(`${id}.svg`, data) + const svg = context.getSerializedSvg(true); + const data = URL.createObjectURL(new Blob([svg], { type: "application/octet-stream" })); + const id = `${str}_referenceFrame_${index}_guid_${DOMUtils.guid()}`; + FileUtils.download(`${id}.svg`, data); } + renderSVGContext(context, { deltaX, deltaY }, includeLabel = true) { + const zoomInNotice = this.zoomInNotice && this.zoomInNotice.style.display === "block"; - renderSVGContext(context, {deltaX, deltaY}, includeLabel = true) { + if (!zoomInNotice) { + const { width, height } = this.viewportElement.getBoundingClientRect(); - const zoomInNotice = this.$zoomInNotice && this.$zoomInNotice.is(":visible") + const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, ''); + const index = this.browser.referenceFrameList.indexOf(this.referenceFrame); + const id = `${str}_referenceFrame_${index}_guid_${DOMUtils.guid()}`; - if (!zoomInNotice) { + const x = deltaX; + const y = deltaY + this.contentTop; + const yClipOffset = -this.contentTop; - const {width, height} = this.$viewport.get(0).getBoundingClientRect() - - const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, '') - const index = this.browser.referenceFrameList.indexOf(this.referenceFrame) - const id = `${str}_referenceFrame_${index}_guid_${DOMUtils.guid()}` - - const x = deltaX - const y = deltaY + this.contentTop - const yClipOffset = -this.contentTop - - context.saveWithTranslationAndClipRect(id, x, y, width, height, yClipOffset) - - let {start, bpPerPixel} = this.referenceFrame - - const config = - { - context, - viewport: this, - referenceFrame: this.referenceFrame, - top: yClipOffset, - pixelTop: yClipOffset, - pixelWidth: width, - pixelHeight: height, - bpStart: start, - bpEnd: start + (width * bpPerPixel), - bpPerPixel, - viewportWidth: width, - selection: this.selection - } + context.saveWithTranslationAndClipRect(id, x, y, width, height, yClipOffset); - const features = this.featureCache ? this.featureCache.features : undefined - const roiFeatures = this.featureCache ? this.featureCache.roiFeatures : undefined - this.draw(config, features, roiFeatures) + let { start, bpPerPixel } = this.referenceFrame; - context.restore() - } + const config = { + context, + viewport: this, + referenceFrame: this.referenceFrame, + top: yClipOffset, + pixelTop: yClipOffset, + pixelWidth: width, + pixelHeight: height, + bpStart: start, + bpEnd: start + (width * bpPerPixel), + bpPerPixel, + viewportWidth: width, + selection: this.selection + }; + + const features = this.featureCache ? this.featureCache.features : undefined; + const roiFeatures = this.featureCache ? this.featureCache.roiFeatures : undefined; + this.draw(config, features, roiFeatures); + context.restore(); + } - if (includeLabel && this.$trackLabel && this.browser.doShowTrackLabels) { - const {x, y, width, height} = DOMUtils.relativeDOMBBox(this.$viewport.get(0), this.$trackLabel.get(0)) - this.renderTrackLabelSVG(context, deltaX + x, deltaY + y, width, height) + if (includeLabel && this.trackLabel && this.browser.doShowTrackLabels) { + const { x, y, width, height } = DOMUtils.relativeDOMBBox(this.viewportElement, this.trackLabel); + this.renderTrackLabelSVG(context, deltaX + x, deltaY + y, width, height); } } // render track label element called from renderSVGContext() renderTrackLabelSVG(context, tx, ty, width, height) { + const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, ''); + const id = `${str}_track_label_guid_${DOMUtils.guid()}`; - const str = (this.trackView.track.name || this.trackView.track.id).replace(/\W/g, '') - const id = `${str}_track_label_guid_${DOMUtils.guid()}` - - context.saveWithTranslationAndClipRect(id, tx, ty, width, height, 0) - - context.fillStyle = "white" - context.fillRect(0, 0, width, height) + context.saveWithTranslationAndClipRect(id, tx, ty, width, height, 0); - context.font = "12px Arial" - context.fillStyle = 'rgb(68, 68, 68)' + context.fillStyle = "white"; + context.fillRect(0, 0, width, height); - const {width: stringWidth} = context.measureText(this.$trackLabel.text()) - const dx = 0.25 * (width - stringWidth) - const dy = 0.7 * (height - 12) - context.fillText(this.$trackLabel.text(), dx, height - dy) + context.font = "12px Arial"; + context.fillStyle = 'rgb(68, 68, 68)'; - context.strokeStyle = 'rgb(68, 68, 68)' - context.strokeRect(0, 0, width, height) + const { width: stringWidth } = context.measureText(this.trackLabel.textContent); + const dx = 0.25 * (width - stringWidth); + const dy = 0.7 * (height - 12); + context.fillText(this.trackLabel.textContent, dx, height - dy); - context.restore() + context.strokeStyle = 'rgb(68, 68, 68)'; + context.strokeRect(0, 0, width, height); + context.restore(); } get cachedFeatures() { - return this.featureCache ? this.featureCache.features : [] + return this.featureCache ? this.featureCache.features : []; } clearCache() { - this.featureCache = undefined - if (this.canvas) this.canvas._data = undefined + this.featureCache = undefined; + if (this.canvas) this.canvas._data = undefined; } async getFeatures(track, chr, start, end, bpPerPixel) { if (this.featureCache && this.featureCache.containsRange(chr, start, end, bpPerPixel, this.windowFunction)) { - return this.featureCache.features + return this.featureCache.features; } else if (typeof track.getFeatures === "function") { - const features = await track.getFeatures(chr, start, end, bpPerPixel, this) - this.checkContentHeight(features) - return features + const features = await track.getFeatures(chr, start, end, bpPerPixel, this); + this.checkContentHeight(features); + return features; } else { - return undefined + return undefined; } } needsRepaint() { + if (!this.canvas) return true; - if (!this.canvas) return true - - const data = this.canvas._data + const data = this.canvas._data; return !data || this.referenceFrame.start < data.bpStart || this.referenceFrame.end > data.bpEnd || this.referenceFrame.chr !== data.referenceFrame.chr || this.referenceFrame.bpPerPixel != data.bpPerPixel || - this.windowFunction != data.windowFunction + this.windowFunction != data.windowFunction; } needsReload() { - if (!this.featureCache) return true - const {chr, bpPerPixel} = this.referenceFrame - const {bpStart, bpEnd} = this.repaintDimensions() - return (!this.featureCache.containsRange(chr, bpStart, bpEnd, bpPerPixel, this.windowFunction)) + if (!this.featureCache) return true; + const { chr, bpPerPixel } = this.referenceFrame; + const { bpStart, bpEnd } = this.repaintDimensions(); + return (!this.featureCache.containsRange(chr, bpStart, bpEnd, bpPerPixel, this.windowFunction)); } - createZoomInNotice($parent) { - - const $container = $('
', {class: 'igv-zoom-in-notice-container'}) - $parent.append($container) + createZoomInNotice(parentElement) { + const container = document.createElement('div'); + container.className = 'igv-zoom-in-notice-container'; + parentElement.appendChild(container); - const $e = $('
') - $container.append($e) + const e = document.createElement('div'); + container.appendChild(e); - $e.text('Zoom in to see features') + e.textContent = 'Zoom in to see features'; - $container.hide() + container.style.display = 'none'; - return $container + return container; } viewIsReady() { - return this.browser && this.browser.referenceFrameList && this.referenceFrame + return this.browser && this.browser.referenceFrameList && this.referenceFrame; } addMouseHandlers() { + const viewport = this.viewportElement; - const viewport = this.$viewport.get(0) - - this.addViewportContextMenuHandler(viewport) + this.addViewportContextMenuHandler(viewport); // Mouse down const md = (event) => { - this.enableClick = true - this.browser.mouseDownOnViewport(event, this) - mouseDownCoords = DOMUtils.pageCoordinates(event) - } - viewport.addEventListener('mousedown', md) - viewport.addEventListener('touchstart', md) + this.enableClick = true; + this.browser.mouseDownOnViewport(event, this); + mouseDownCoords = DOMUtils.pageCoordinates(event); + }; + viewport.addEventListener('mousedown', md); + viewport.addEventListener('touchstart', md); // Mouse up const mu = (event) => { // Any mouse up cancels drag and scrolling if (this.browser.dragObject || this.browser.isScrolling) { - this.browser.cancelTrackPan() - // event.preventDefault(); - // event.stopPropagation(); - this.enableClick = false // Until next mouse down + this.browser.cancelTrackPan(); + this.enableClick = false; // Until next mouse down } else { - this.browser.cancelTrackPan() - this.browser.endTrackDrag() + this.browser.cancelTrackPan(); + this.browser.endTrackDrag(); } - } - viewport.addEventListener('mouseup', mu) - viewport.addEventListener('touchend', mu) + }; + viewport.addEventListener('mouseup', mu); + viewport.addEventListener('touchend', mu); // Mouse move if (typeof this.trackView.track.hoverText === 'function') { - viewport.addEventListener('mousemove', (event => { + viewport.addEventListener('mousemove', (event) => { if (event.buttons === 0 && (Date.now() - lastHoverUpdateTime > 100)) { - lastHoverUpdateTime = Date.now() - const clickState = this.createClickState(event) + lastHoverUpdateTime = Date.now(); + const clickState = this.createClickState(event); if (clickState) { - const tooltip = this.trackView.track.hoverText(clickState) + const tooltip = this.trackView.track.hoverText(clickState); if (tooltip) { - this.$viewport[0].setAttribute("title", tooltip) + this.viewportElement.setAttribute("title", tooltip); } else { - this.$viewport[0].removeAttribute("title") + this.viewportElement.removeAttribute("title"); } } } - })) + }); } - this.addViewportClickHandler(this.$viewport.get(0)) + this.addViewportClickHandler(viewport); if (this.trackView.track.name && "sequence" !== this.trackView.track.config.type) { - this.addTrackLabelClickHandler(this.$trackLabel.get(0)) + this.addTrackLabelClickHandler(this.trackLabel); } - } addViewportContextMenuHandler(viewport) { - viewport.addEventListener('contextmenu', (event) => { - // Ignore if we are doing a drag. This can happen with touch events. if (this.browser.dragObject) { - return false + return false; } - const clickState = this.createClickState(event) + const clickState = this.createClickState(event); if (undefined === clickState) { - return false + return false; } - event.preventDefault() + event.preventDefault(); // Track specific items - let menuItems = [] + let menuItems = []; if (typeof this.trackView.track.contextMenuItemList === "function") { - const trackMenuItems = this.trackView.track.contextMenuItemList(clickState) + const trackMenuItems = this.trackView.track.contextMenuItemList(clickState); if (trackMenuItems) { - menuItems = trackMenuItems + menuItems = trackMenuItems; } } // Add items common to all tracks if (menuItems.length > 0) { - menuItems.push({label: $('
')}) + menuItems.push({ label: '
' }); } - menuItems.push({label: 'Save Image (PNG)', click: () => this.savePNG()}) - menuItems.push({label: 'Save Image (SVG)', click: () => this.saveSVG()}) - - this.browser.menuPopup.presentTrackContextMenu(event, menuItems) - }) + menuItems.push({ label: 'Save Image (PNG)', click: () => this.savePNG() }); + menuItems.push({ label: 'Save Image (SVG)', click: () => this.saveSVG() }); + this.browser.menuPopup.presentTrackContextMenu(event, menuItems); + }); } - addViewportClickHandler(viewport) { - viewport.addEventListener('click', (event) => { - if (this.enableClick && this.canvas) { if (3 === event.which || event.ctrlKey) { - return + return; } if (this.browser.dragObject || this.browser.isScrolling) { - return + return; } // Treat as a mouse click, it's either a single or double click. // Handle here and stop propagation / default - event.preventDefault() + event.preventDefault(); - const mouseX = DOMUtils.translateMouseCoordinates(event, this.$viewport.get(0)).x - const mouseXCanvas = DOMUtils.translateMouseCoordinates(event, this.canvas).x - const referenceFrame = this.referenceFrame - const xBP = Math.floor((referenceFrame.start) + referenceFrame.toBP(mouseXCanvas)) + const mouseX = DOMUtils.translateMouseCoordinates(event, this.viewportElement).x; + const mouseXCanvas = DOMUtils.translateMouseCoordinates(event, this.canvas).x; + const referenceFrame = this.referenceFrame; + const xBP = Math.floor((referenceFrame.start) + referenceFrame.toBP(mouseXCanvas)); - const time = Date.now() + const time = Date.now(); if (time - lastClickTime < this.browser.constants.doubleClickDelay) { - // double-click if (popupTimerID) { - window.clearTimeout(popupTimerID) - popupTimerID = undefined + window.clearTimeout(popupTimerID); + popupTimerID = undefined; } - const centerBP = Math.round(referenceFrame.start + referenceFrame.toBP(mouseX)) + const centerBP = Math.round(referenceFrame.start + referenceFrame.toBP(mouseX)); - let string + let string; if ('all' === this.referenceFrame.chr.toLowerCase()) { - - const chr = this.browser.genome.getChromosomeCoordinate(centerBP).chr + const chr = this.browser.genome.getChromosomeCoordinate(centerBP).chr; if (1 === this.browser.referenceFrameList.length) { - string = chr + string = chr; } else { - const loci = this.browser.referenceFrameList.map(({locusSearchString}) => locusSearchString) - const index = this.browser.referenceFrameList.indexOf(this.referenceFrame) - loci[index] = chr - string = loci.join(' ') + const loci = this.browser.referenceFrameList.map(({ locusSearchString }) => locusSearchString); + const index = this.browser.referenceFrameList.indexOf(this.referenceFrame); + loci[index] = chr; + string = loci.join(' '); } - this.browser.search(string) - + this.browser.search(string); } else { - this.browser.zoomWithScaleFactor(0.5, centerBP, this.referenceFrame) + this.browser.zoomWithScaleFactor(0.5, centerBP, this.referenceFrame); } - - } else { // single-click - - /*if (event.shiftKey && typeof this.trackView.track.shiftClick === "function") { - - this.trackView.track.shiftClick(xBP, event) - - } else */ - if (typeof this.trackView.track.popupData === "function") { - popupTimerID = setTimeout(() => { + const content = this.getPopupContent(event); + if (content) { + if (false === event.shiftKey) { + if (popover) { + popover.dispose(); + } - const content = this.getPopupContent(event) - if (content) { - - if (false === event.shiftKey) { - - if (popover) { - popover.dispose() - } - - if (trackViewportPopoverList.length > 0) { - for (const gp of trackViewportPopoverList) { - gp.dispose() - } - trackViewportPopoverList.length = 0 + if (trackViewportPopoverList.length > 0) { + for (const gp of trackViewportPopoverList) { + gp.dispose(); } + trackViewportPopoverList.length = 0; + } - popover = new Popover(this.$viewport.get(0).parentElement, true, undefined, () => { - popover.dispose() - }) - - popover.presentContentWithEvent(event, content) - } else { - - let po = new Popover(this.$viewport.get(0).parentElement, true, undefined, () => { - const index = trackViewportPopoverList.indexOf(po) - trackViewportPopoverList.splice(index, 1) - po.dispose() - }) + popover = new Popover(this.viewportElement.parentElement, true, undefined, () => { + popover.dispose(); + }); - trackViewportPopoverList.push(po) + popover.presentContentWithEvent(event, content); + } else { + let po = new Popover(this.viewportElement.parentElement, true, undefined, () => { + const index = trackViewportPopoverList.indexOf(po); + trackViewportPopoverList.splice(index, 1); + po.dispose(); + }); - po.presentContentWithEvent(event, content) - } + trackViewportPopoverList.push(po); + po.presentContentWithEvent(event, content); } - window.clearTimeout(popupTimerID) - popupTimerID = undefined - }, - this.browser.constants.doubleClickDelay) + } + window.clearTimeout(popupTimerID); + popupTimerID = undefined; + }, this.browser.constants.doubleClickDelay); } } - lastClickTime = time - + lastClickTime = time; } - }) + }); } addTrackLabelClickHandler(trackLabel) { - trackLabel.addEventListener('click', (event) => { + event.stopPropagation(); - event.stopPropagation() - - const {track} = this.trackView + const { track } = this.trackView; - let str + let str; if (typeof track.description === 'function') { - str = track.description() + str = track.description(); } else if (track.description) { - str = `
${track.description}
` + str = `
${track.description}
`; } if (str) { if (undefined === this.popover) { - this.popover = new Popover(this.browser.columnContainer, true, (track.name || ''), undefined) + this.popover = new Popover(this.browser.columnContainer, true, (track.name || ''), undefined); } - this.popover.presentContentWithEvent(event, str) + this.popover.presentContentWithEvent(event, str); } - }) + }); } createClickState(event) { + if (!this.canvas) return; // Can happen during initialization - if (!this.canvas) return // Can happen during initialization - - const referenceFrame = this.referenceFrame - const viewportCoords = DOMUtils.translateMouseCoordinates(event, this.$viewport.get(0)) - const canvasCoords = DOMUtils.translateMouseCoordinates(event, this.canvas) - const genomicLocation = (((referenceFrame.start) + referenceFrame.toBP(viewportCoords.x))) + const referenceFrame = this.referenceFrame; + const viewportCoords = DOMUtils.translateMouseCoordinates(event, this.viewportElement); + const canvasCoords = DOMUtils.translateMouseCoordinates(event, this.canvas); + const genomicLocation = (((referenceFrame.start) + referenceFrame.toBP(viewportCoords.x))); return { event, @@ -848,117 +787,96 @@ class TrackViewport extends Viewport { y: viewportCoords.y - this.contentTop, canvasX: canvasCoords.x, canvasY: canvasCoords.y - } - + }; } getPopupContent(event) { - - const clickState = this.createClickState(event) + const clickState = this.createClickState(event); if (undefined === clickState) { - return + return; } - let track = this.trackView.track - const dataList = track.popupData(clickState) + let track = this.trackView.track; + const dataList = track.popupData(clickState); - const popupClickHandlerResult = this.browser.fireEvent('trackclick', [track, dataList]) + const popupClickHandlerResult = this.browser.fireEvent('trackclick', [track, dataList]); - let content + let content; if (undefined === popupClickHandlerResult || true === popupClickHandlerResult) { // Indicates handler did not handle the result, or the handler wishes default behavior to occur if (dataList && dataList.length > 0) { - content = formatPopoverText(dataList) + content = formatPopoverText(dataList); } - } else if (typeof popupClickHandlerResult === 'string') { - content = popupClickHandlerResult + content = popupClickHandlerResult; } - return content + return content; } dispose() { - if (this.popover) { - this.popover.dispose() + this.popover.dispose(); } - // if (trackViewportPopoverList) { - // for (let i = 0; i < trackViewportPopoverList.length; i++ ) { - // trackViewportPopoverList[ i ].dispose() - // } - // - // trackViewportPopoverList = undefined - // } - - super.dispose() + super.dispose(); } - } - function formatPopoverText(nameValues) { - const rows = nameValues.map(nameValue => { - if (nameValue.name) { - const str = `${nameValue.name}   ${nameValue.value}` - return `
${str}
` + const str = `${nameValue.name}   ${nameValue.value}`; + return `
${str}
`; } else if ('
' === nameValue) { // this can be retired if nameValue.html is allowed. - return nameValue + return nameValue; } else if (nameValue.html) { - return nameValue.html + return nameValue.html; } else { - return `
${nameValue}
` + return `
${nameValue}
`; } + }); - }) - - return rows.join('') + return rows.join(''); } class FeatureCache { - constructor(chr, tileStart, tileEnd, bpPerPixel, features, roiFeatures, multiresolution, windowFunction) { - this.chr = chr - this.bpStart = tileStart - this.bpEnd = tileEnd - this.bpPerPixel = bpPerPixel - this.features = features - this.roiFeatures = roiFeatures - this.multiresolution = multiresolution - this.windowFunction = windowFunction + this.chr = chr; + this.bpStart = tileStart; + this.bpEnd = tileEnd; + this.bpPerPixel = bpPerPixel; + this.features = features; + this.roiFeatures = roiFeatures; + this.multiresolution = multiresolution; + this.windowFunction = windowFunction; } containsRange(chr, start, end, bpPerPixel, windowFunction) { - - if (windowFunction && windowFunction !== this.windowFunction) return false + if (windowFunction && windowFunction !== this.windowFunction) return false; // For multi-resolution tracks allow for a 2X change in bpPerPixel - const r = this.multiresolution ? this.bpPerPixel / bpPerPixel : 1 + const r = this.multiresolution ? this.bpPerPixel / bpPerPixel : 1; - return start >= this.bpStart && end <= this.bpEnd && chr === this.chr && r > 0.5 && r < 2 + return start >= this.bpStart && end <= this.bpEnd && chr === this.chr && r > 0.5 && r < 2; } overlapsRange(chr, start, end) { - return this.chr === chr && end >= this.bpStart && start <= this.bpEnd + return this.chr === chr && end >= this.bpStart && start <= this.bpEnd; } } - /** * Merge 2 arrays. a and/or b can be undefined. If both are undefined, return undefined * @param a An array or undefined * @param b An array or undefined */ function mergeArrays(a, b) { - if (a && b) return a.concat(b) - else if (a) return a - else return b - + if (a && b) return a.concat(b); + else if (a) return a; + else return b; } -export {trackViewportPopoverList} -export default TrackViewport +export { trackViewportPopoverList }; +export default TrackViewport; diff --git a/js/variant/variantTrack.js b/js/variant/variantTrack.js index 42e0dd70f..d54ee0929 100644 --- a/js/variant/variantTrack.js +++ b/js/variant/variantTrack.js @@ -24,26 +24,24 @@ * THE SOFTWARE. */ -import $ from "../vendor/jquery-3.3.1.slim.js" -import FeatureSource from '../feature/featureSource.js' -import TrackBase from "../trackBase.js" -import IGVGraphics from "../igv-canvas.js" -import {createCheckbox} from "../igv-icons.js" -import {ColorTable, PaletteColorTable} from "../util/colorPalletes.js" -import SampleInfo from "../sample/sampleInfo.js" -import {makeVCFChords, sendChords} from "../jbrowse/circularViewUtils.js" -import {FileUtils, StringUtils, IGVColor} from "../../node_modules/igv-utils/src/index.js" -import CNVPytorTrack from "../cnvpytor/cnvpytorTrack.js" -import {doSortByAttributes} from "../sample/sampleUtils.js" - -const isString = StringUtils.isString - -const DEFAULT_COLOR = "rgb(0,0,150)" -const DEFAULT_VISIBILITY_WINDOW = 1000000 -const TOP_MARGIN = 10 +import FeatureSource from '../feature/featureSource.js'; +import TrackBase from "../trackBase.js"; +import IGVGraphics from "../igv-canvas.js"; +import { createCheckbox } from "../igv-icons.js"; +import { ColorTable, PaletteColorTable } from "../util/colorPalletes.js"; +import SampleInfo from "../sample/sampleInfo.js"; +import { makeVCFChords, sendChords } from "../jbrowse/circularViewUtils.js"; +import { FileUtils, StringUtils, IGVColor } from "../../node_modules/igv-utils/src/index.js"; +import CNVPytorTrack from "../cnvpytor/cnvpytorTrack.js"; +import { doSortByAttributes } from "../sample/sampleUtils.js"; + +const isString = StringUtils.isString; + +const DEFAULT_COLOR = "rgb(0,0,150)"; +const DEFAULT_VISIBILITY_WINDOW = 1000000; +const TOP_MARGIN = 10; class VariantTrack extends TrackBase { - static defaults = { displayMode: "EXPANDED", sortDirection: "ASC", @@ -69,136 +67,130 @@ class VariantTrack extends TrackBase { visibilityWindow: undefined, labelDisplayMode: undefined, type: "variant" - } + }; - _sortDirections = new Map() + _sortDirections = new Map(); constructor(config, browser) { - super(config, browser) + super(config, browser); } // Note -- init gets called during base class construction. Confusing init(config) { - - super.init(config) + super.init(config); if (config.variantHeight) { // Override for backward compatibility - this.expandedVariantHeight = config.variantHeight + this.expandedVariantHeight = config.variantHeight; } - this.featureSource = FeatureSource(config, this.browser.genome) + this.featureSource = FeatureSource(config, this.browser.genome); - this.colorTables = new Map() + this.colorTables = new Map(); if (config.colorTable) { - const key = config.colorBy || "*" - this.colorTables.set(key, new ColorTable(config.colorTable)) + const key = config.colorBy || "*"; + this.colorTables.set(key, new ColorTable(config.colorTable)); } - this.strokecolor = config.strokecolor - this._context_hook = config.context_hook + this.strokecolor = config.strokecolor; + this._context_hook = config.context_hook; // If a color is explicitly set disable colorBy if (config.color) { - this.colorBy = undefined + this.colorBy = undefined; } // The number of variant rows are computed dynamically, but start with "1" by default - this.nVariantRows = 1 + this.nVariantRows = 1; // Explicitly set samples -- used to select a subset of samples from a dataset if (config.samples) { // Explicit setting, keys == names - for (let s of config.samples) { - this.sampleKeys = config.samples - } + this.sampleKeys = config.samples; } if (config.sort) { - this.initialSort = config.sort + this.initialSort = config.sort; } - this._colorByItems = new Map([['none', 'None']]) + this._colorByItems = new Map([['none', 'None']]); } async postInit() { - - this.header = await this.getHeader() + this.header = await this.getHeader(); // Set colorBy, if not explicitly set default to allele frequency, if available, otherwise default to none (undefined) - const infoFields = new Set(Object.keys(this.header.INFO)) + const infoFields = new Set(Object.keys(this.header.INFO)); if (this.config.colorBy) { - this.colorBy = this.config.colorBy + this.colorBy = this.config.colorBy; } else if (!this.config.color && infoFields.has('AF')) { - this.colorBy = 'AF' + this.colorBy = 'AF'; } // Configure menu items based on info available if (infoFields.has('AF')) { - this._colorByItems.set('AF', 'Allele frequency') + this._colorByItems.set('AF', 'Allele frequency'); } if (infoFields.has('VT')) { - this._colorByItems.set('VT', 'Variant Type') + this._colorByItems.set('VT', 'Variant Type'); } if (infoFields.has('SVTYPE')) { - this._colorByItems.set('SVTYPE', 'SV Type') + this._colorByItems.set('SVTYPE', 'SV Type'); } if (this.config.colorBy && !this._colorByItems.has(this.config.colorBy)) { - this._colorByItems.set(this.config.colorBy, this.config.colorBy) + this._colorByItems.set(this.config.colorBy, this.config.colorBy); } - - if (this.disposed) return // This track was removed during async load + if (this.disposed) return; // This track was removed during async load if (this.header && !this.sampleKeys) { - this.sampleKeys = this.header.sampleNameMap ? Array.from(this.header.sampleNameMap.keys()) : [] + this.sampleKeys = this.header.sampleNameMap ? Array.from(this.header.sampleNameMap.keys()) : []; } if (undefined === this.visibilityWindow && this.config.indexed !== false) { - const fn = FileUtils.isFile(this.config.url) ? this.config.url.name : this.config.url + const fn = FileUtils.isFile(this.config.url) ? this.config.url.name : this.config.url; if (isString(fn) && fn.toLowerCase().includes("gnomad")) { - this.visibilityWindow = 1000 // these are known to be very dense + this.visibilityWindow = 1000; // these are known to be very dense } else if (typeof this.featureSource.defaultVisibilityWindow === 'function') { - this.visibilityWindow = await this.featureSource.defaultVisibilityWindow() + this.visibilityWindow = await this.featureSource.defaultVisibilityWindow(); } else { - this.visibilityWindow = DEFAULT_VISIBILITY_WINDOW + this.visibilityWindow = DEFAULT_VISIBILITY_WINDOW; } } - return this + return this; } get supportsWholeGenome() { - return !this.config.indexURL || this.config.supportsWholeGenome === true + return !this.config.indexURL || this.config.supportsWholeGenome === true; } get color() { - return this._color || DEFAULT_COLOR + return this._color || DEFAULT_COLOR; } set color(c) { - this._color = c + this._color = c; if (c) { - this.colorBy = undefined + this.colorBy = undefined; } } async getHeader() { if (!this.header) { if (typeof this.featureSource.getHeader === "function") { - this.header = await this.featureSource.getHeader() + this.header = await this.featureSource.getHeader(); } } - return this.header + return this.header; } getSampleCount() { - return this.sampleKeys ? this.sampleKeys.length : 0 + return this.sampleKeys ? this.sampleKeys.length : 0; } async getFeatures(chr, start, end, bpPerPixel) { - if (this.header === undefined) { - this.header = await this.getHeader() + this.header = await this.getHeader(); } const features = await this.featureSource.getFeatures({ chr, @@ -206,45 +198,44 @@ class VariantTrack extends TrackBase { end, bpPerPixel, visibilityWindow: this.visibilityWindow - }) + }); if (this.initialSort) { - const sort = this.initialSort + const sort = this.initialSort; if (sort.option === undefined || sort.option.toUpperCase() === "GENOTYPE") { - this.sortSamplesByGenotype(sort, features) + this.sortSamplesByGenotype(sort, features); } else if ("ATTRIBUTE" === sort.option.toUpperCase() && sort.attribute) { - const sortDirection = "ASC" === sort.direction ? 1 : -1 - this.sortByAttribute(sort.attribute, sortDirection) + const sortDirection = "ASC" === sort.direction ? 1 : -1; + this.sortByAttribute(sort.attribute, sortDirection); } - this.initialSort = undefined // Sample order is sorted, + this.initialSort = undefined; // Sample order is sorted, } - return features + return features; } hasSamples() { - return this.getSampleCount() > 0 + return this.getSampleCount() > 0; } /** * Required method of the sample name and info viewports */ getSamples() { - - const vGap = ("SQUISHED" === this.displayMode) ? this.squishedVGap : this.expandedVGap - const nVariantRows = "COLLAPSED" === this.displayMode ? 1 : this.nVariantRows - const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight - const callHeight = ("SQUISHED" === this.displayMode ? this.squishedCallHeight : this.expandedCallHeight) - const height = nVariantRows * (callHeight + vGap) + const vGap = ("SQUISHED" === this.displayMode) ? this.squishedVGap : this.expandedVGap; + const nVariantRows = "COLLAPSED" === this.displayMode ? 1 : this.nVariantRows; + const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight; + const callHeight = ("SQUISHED" === this.displayMode ? this.squishedCallHeight : this.expandedCallHeight); + const height = nVariantRows * (callHeight + vGap); // Y Offset at which samples begin - const yOffset = TOP_MARGIN + nVariantRows * (variantHeight + vGap) + const yOffset = TOP_MARGIN + nVariantRows * (variantHeight + vGap); return { names: this.sampleKeys, yOffset, height - } + }; } /** @@ -255,353 +246,267 @@ class VariantTrack extends TrackBase { * @returns {*} */ computePixelHeight(features) { + if (!features || 0 === features.length) return TOP_MARGIN; - if (!features || 0 === features.length) return TOP_MARGIN - - const nVariantRows = (this.displayMode === "COLLAPSED") ? 1 : this.nVariantRows - const vGap = (this.displayMode === "SQUISHED") ? this.squishedVGap : this.expandedVGap - const variantHeight = (this.displayMode === "SQUISHED") ? this.squishedVariantHeight : this.expandedVariantHeight - const callHeight = (this.displayMode === "SQUISHED") ? this.squishedCallHeight : this.expandedCallHeight - const nGenotypes = this.showGenotypes === false ? 0 : this.getSampleCount() * nVariantRows - const h = TOP_MARGIN + nVariantRows * (variantHeight + vGap) - return h + vGap + (nGenotypes + 1) * (callHeight + vGap) - + const nVariantRows = (this.displayMode === "COLLAPSED") ? 1 : this.nVariantRows; + const vGap = (this.displayMode === "SQUISHED") ? this.squishedVGap : this.expandedVGap; + const variantHeight = (this.displayMode === "SQUISHED") ? this.squishedVariantHeight : this.expandedVariantHeight; + const callHeight = (this.displayMode === "SQUISHED") ? this.squishedCallHeight : this.expandedCallHeight; + const nGenotypes = this.showGenotypes === false ? 0 : this.getSampleCount() * nVariantRows; + const h = TOP_MARGIN + nVariantRows * (variantHeight + vGap); + return h + vGap + (nGenotypes + 1) * (callHeight + vGap); } variantRowCount(count) { - this.nVariantRows = count + this.nVariantRows = count; } - draw({context, pixelWidth, pixelHeight, bpPerPixel, bpStart, pixelTop, features}) { - - IGVGraphics.fillRect(context, 0, pixelTop, pixelWidth, pixelHeight, {'fillStyle': "rgb(255, 255, 255)"}) + draw({ context, pixelWidth, pixelHeight, bpPerPixel, bpStart, pixelTop, features }) { + IGVGraphics.fillRect(context, 0, pixelTop, pixelWidth, pixelHeight, { 'fillStyle': "rgb(255, 255, 255)" }); - const vGap = ("SQUISHED" === this.displayMode) ? this.squishedVGap : this.expandedVGap - const rowCount = ("COLLAPSED" === this.displayMode) ? 1 : this.nVariantRows - const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight - this.variantBandHeight = TOP_MARGIN + rowCount * (variantHeight + vGap) + const vGap = ("SQUISHED" === this.displayMode) ? this.squishedVGap : this.expandedVGap; + const rowCount = ("COLLAPSED" === this.displayMode) ? 1 : this.nVariantRows; + const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight; + this.variantBandHeight = TOP_MARGIN + rowCount * (variantHeight + vGap); - let callSets = this.sampleColumns + let callSets = this.sampleColumns; - const hasSamples = this.hasSamples() + const hasSamples = this.hasSamples(); if (callSets && hasSamples && this.showGenotypes !== false) { - IGVGraphics.strokeLine(context, 0, this.variantBandHeight, pixelWidth, this.variantBandHeight, {strokeStyle: 'rgb(224,224,224) '}) + IGVGraphics.strokeLine(context, 0, this.variantBandHeight, pixelWidth, this.variantBandHeight, { strokeStyle: 'rgb(224,224,224) ' }); } if (features) { - - const callHeight = ("SQUISHED" === this.displayMode) ? this.squishedCallHeight : this.expandedCallHeight - const vGap = ("SQUISHED" === this.displayMode) ? this.squishedVGap : this.expandedVGap - const bpEnd = bpStart + pixelWidth * bpPerPixel + 1 + const callHeight = ("SQUISHED" === this.displayMode) ? this.squishedCallHeight : this.expandedCallHeight; + const bpEnd = bpStart + pixelWidth * bpPerPixel + 1; // Loop through variants. A variant == a row in a VCF file for (let v of features) { - if (v.end < bpStart) continue - if (v.start > bpEnd) break + if (v.end < bpStart) continue; + if (v.start > bpEnd) break; - const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight - const y = TOP_MARGIN + ("COLLAPSED" === this.displayMode ? 0 : v.row * (variantHeight + vGap)) - const h = variantHeight + const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight; + const y = TOP_MARGIN + ("COLLAPSED" === this.displayMode ? 0 : v.row * (variantHeight + vGap)); + const h = variantHeight; // Compute pixel width. Minimum width is 3 pixels, if > 5 pixels create gap between variants - let x = (v.start - bpStart) / bpPerPixel - let x1 = (v.end - bpStart) / bpPerPixel + let x = (v.start - bpStart) / bpPerPixel; + let x1 = (v.end - bpStart) / bpPerPixel; - let w = Math.max(1, x1 - x) + let w = Math.max(1, x1 - x); if (w < 3) { - w = 3 - x -= 1 + w = 3; + x -= 1; } else if (w > 5) { - x += 1 - w -= 2 + x += 1; + w -= 2; } - const variant = v._f || v // True variant record, used for whole genome view and SV mate records - let af + const variant = v._f || v; // True variant record, used for whole genome view and SV mate records + let af; try { - af = variant.alleleFreq() + af = variant.alleleFreq(); } catch (e) { - console.log(e) + console.log(e); } if ("AF" === this.colorBy && af) { - const hAlt = Math.min(1, af) * h - const hRef = h - hAlt - context.fillStyle = variant.isFiltered() ? this.refColorFiltered : this.refColor - context.fillRect(x, y, w, hRef) - context.fillStyle = variant.isFiltered() ? this.altColorFiltered : this.altColor - context.fillRect(x, y + hRef, w, hAlt) - + const hAlt = Math.min(1, af) * h; + const hRef = h - hAlt; + context.fillStyle = variant.isFiltered() ? this.refColorFiltered : this.refColor; + context.fillRect(x, y, w, hRef); + context.fillStyle = variant.isFiltered() ? this.altColorFiltered : this.altColor; + context.fillRect(x, y + hRef, w, hAlt); } else { - context.fillStyle = this.getColorForFeature(variant) - context.fillRect(x, y, w, h) + context.fillStyle = this.getColorForFeature(variant); + context.fillRect(x, y, w, h); } //only paint stroke if a color is defined - let strokecolor = this.getVariantStrokecolor(variant) + let strokecolor = this.getVariantStrokecolor(variant); if (strokecolor) { - context.strokeStyle = strokecolor - context.strokeRect(x, y, w, h) + context.strokeStyle = strokecolor; + context.strokeRect(x, y, w, h); } // call hook if _context_hook fn is defined - this.callContextHook(variant, context, x, y, w, h) - - //variant.pixelRect = {x, y, w, h} + this.callContextHook(variant, context, x, y, w, h); // Loop though the samples for this variant. if (hasSamples && this.showGenotypes !== false) { + const nVariantRows = "COLLAPSED" === this.displayMode ? 1 : this.nVariantRows; + this.sampleYOffset = this.variantBandHeight + vGap; + this.sampleHeight = nVariantRows * (callHeight + vGap); // For each sample, there is a call for each variant at this position - const nVariantRows = "COLLAPSED" === this.displayMode ? 1 : this.nVariantRows - this.sampleYOffset = this.variantBandHeight + vGap - this.sampleHeight = nVariantRows * (callHeight + vGap) // For each sample, there is a call for each variant at this position - - let sampleNumber = 0 + let sampleNumber = 0; for (let sample of this.sampleKeys) { - - const index = this.header.sampleNameMap.get(sample) - const call = variant.calls[index] + const index = this.header.sampleNameMap.get(sample); + const call = variant.calls[index]; if (call) { - const row = "COLLAPSED" === this.displayMode ? 0 : variant.row - const py = this.sampleYOffset + sampleNumber * this.sampleHeight + row * (callHeight + vGap) - let allVar = true // until proven otherwise - let allRef = true - let noCall = false + const row = "COLLAPSED" === this.displayMode ? 0 : variant.row; + const py = this.sampleYOffset + sampleNumber * this.sampleHeight + row * (callHeight + vGap); + let allVar = true; // until proven otherwise + let allRef = true; + let noCall = false; if (call.genotype) { for (let g of call.genotype) { if ('.' === g) { - noCall = true - break + noCall = true; + break; } else { - if (g !== 0) allRef = false - if (g === 0) allVar = false + if (g !== 0) allRef = false; + if (g === 0) allVar = false; } } } if (!call.genotype) { - context.fillStyle = this.noGenotypeColor + context.fillStyle = this.noGenotypeColor; } else if (noCall) { - context.fillStyle = this.noCallColor + context.fillStyle = this.noCallColor; } else if (allRef) { - context.fillStyle = this.homrefColor + context.fillStyle = this.homrefColor; } else if (allVar) { - context.fillStyle = this.homvarColor + context.fillStyle = this.homvarColor; } else { - context.fillStyle = this.hetvarColor + context.fillStyle = this.hetvarColor; } - context.fillRect(x, py, w, callHeight) - - //callSet.pixelRect = {x, y: py, w, h: callHeight} + context.fillRect(x, py, w, callHeight); } - sampleNumber++ + sampleNumber++; } - } } - } else { - console.log("No feature list") + console.log("No feature list"); } - }; + } get refColorFiltered() { if (!this._refColorFiltered) { - this._refColorFiltered = IGVColor.addAlpha(this.refColor, 0.2) + this._refColorFiltered = IGVColor.addAlpha(this.refColor, 0.2); } - return this._refColorFiltered + return this._refColorFiltered; } get altColorFiltered() { if (!this._altColorFiltered) { - this._altColorFiltered = IGVColor.addAlpha(this.altColor, 0.2) + this._altColorFiltered = IGVColor.addAlpha(this.altColor, 0.2); } - return this._altColorFiltered + return this._altColorFiltered; } getColorForFeature(variant) { - - const v = variant._f || variant - let variantColor + const v = variant._f || variant; + let variantColor; if (this.colorBy && 'none' !== this.colorBy) { - - const value = v.getAttributeValue(this.colorBy) - variantColor = value !== undefined ? this.getVariantColorTable(this.colorBy).getColor(value) : "gray" - + const value = v.getAttributeValue(this.colorBy); + variantColor = value !== undefined ? this.getVariantColorTable(this.colorBy).getColor(value) : "gray"; } else if (this.color) { - variantColor = (typeof this.color === "function") ? this.color(variant) : this.color + variantColor = (typeof this.color === "function") ? this.color(variant) : this.color; } else if ("NONVARIANT" === v.type) { - variantColor = this.nonRefColor + variantColor = this.nonRefColor; } else if ("MIXED" === v.type) { - variantColor = this.mixedColor + variantColor = this.mixedColor; } else { - variantColor = this.color + variantColor = this.color; } if (v.isFiltered()) { - variantColor = IGVColor.addAlpha(variantColor, 0.2) + variantColor = IGVColor.addAlpha(variantColor, 0.2); } - return variantColor + return variantColor; } - getVariantStrokecolor(variant) { - - const v = variant._f || variant - let variantStrokeColor + const v = variant._f || variant; + let variantStrokeColor; if (this.strokecolor) { - variantStrokeColor = (typeof this.strokecolor === "function") ? this.strokecolor(v) : this.strokecolor + variantStrokeColor = (typeof this.strokecolor === "function") ? this.strokecolor(v) : this.strokecolor; } else { - variantStrokeColor = undefined + variantStrokeColor = undefined; } - return variantStrokeColor + return variantStrokeColor; } callContextHook(variant, context, x, y, w, h) { if (this._context_hook) { if (typeof this._context_hook === "function") { - const v = variant._f || variant + const v = variant._f || variant; - context.save() - this._context_hook(v, context, x, y, w, h) - context.restore() + context.save(); + this._context_hook(v, context, x, y, w, h); + context.restore(); } } } clickedFeatures(clickState) { + let featureList = super.clickedFeatures(clickState); - let featureList = super.clickedFeatures(clickState) - - const vGap = (this.displayMode === 'EXPANDED') ? this.expandedVGap : this.squishedVGap - const callHeight = vGap + ("SQUISHED" === this.displayMode ? this.squishedCallHeight : this.expandedCallHeight) + const vGap = (this.displayMode === 'EXPANDED') ? this.expandedVGap : this.squishedVGap; + const callHeight = vGap + ("SQUISHED" === this.displayMode ? this.squishedCallHeight : this.expandedCallHeight); // Find the variant row (i.e. row assigned during feature packing) - const yOffset = clickState.y + const yOffset = clickState.y; if (yOffset <= this.variantBandHeight) { // Variant - const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight - const variantRow = Math.floor((yOffset - TOP_MARGIN) / (variantHeight + vGap)) + const variantHeight = ("SQUISHED" === this.displayMode) ? this.squishedVariantHeight : this.expandedVariantHeight; + const variantRow = Math.floor((yOffset - TOP_MARGIN) / (variantHeight + vGap)); if ("COLLAPSED" !== this.displayMode) { - featureList = featureList.filter(f => f.row === variantRow) + featureList = featureList.filter(f => f.row === variantRow); } } else if (this.sampleKeys) { - const sampleY = yOffset - this.variantBandHeight - const sampleRow = Math.floor(sampleY / this.sampleHeight) + const sampleY = yOffset - this.variantBandHeight; + const sampleRow = Math.floor(sampleY / this.sampleHeight); if (sampleRow >= 0 && sampleRow < this.sampleKeys.length) { - const variantRow = Math.floor((sampleY - sampleRow * this.sampleHeight) / callHeight) - const variants = "COLLAPSED" === this.displayMode ? featureList : featureList.filter(f => f.row === variantRow) - const sampleName = this.sampleKeys[sampleRow] - const index = this.header.sampleNameMap.get(sampleName) + const variantRow = Math.floor((sampleY - sampleRow * this.sampleHeight) / callHeight); + const variants = "COLLAPSED" === this.displayMode ? featureList : featureList.filter(f => f.row === variantRow); + const sampleName = this.sampleKeys[sampleRow]; + const index = this.header.sampleNameMap.get(sampleName); featureList = variants.map(v => { - const call = v.calls[index] + const call = v.calls[index]; // This is hacky, but it avoids expanding all calls in advance in case one is clicked, or // alternatively storing backpoints to the variant for all calls. - call.genotypeString = expandGenotype(call, v) - return call - }) + call.genotypeString = expandGenotype(call, v); + return call; + }); } } - return featureList + return featureList; } - /** * Return "popup data" for feature @ genomic location. Data is an array of key-value pairs */ popupData(clickState, featureList) { + if (featureList === undefined) featureList = this.clickedFeatures(clickState); + const genomicLocation = clickState.genomicLocation; + const genomeID = this.browser.genome.id; - if (featureList === undefined) featureList = this.clickedFeatures(clickState) - const genomicLocation = clickState.genomicLocation - const genomeID = this.browser.genome.id - - let popupData = [] + let popupData = []; for (let v of featureList) { - - const f = v._f || v // Get real variant from psuedo-variant, e.g. whole genome or SV mate + const f = v._f || v; // Get real variant from psuedo-variant, e.g. whole genome or SV mate if (popupData.length > 0) { - popupData.push({html: '
'}) + popupData.push({ html: '
' }); } if (typeof f.popupData === 'function') { - const v = f.popupData(genomicLocation, genomeID) - Array.prototype.push.apply(popupData, v) + const v = f.popupData(genomicLocation, genomeID); + Array.prototype.push.apply(popupData, v); } } - return popupData - + return popupData; } - -// VariantTrack.prototype.contextMenuItemList = function (clickState) { -// -// const self = this; -// const menuItems = []; -// -// const featureList = this.clickedFeatures(clickState); -// -// if (this.sampleColumns && featureList && featureList.length > 0) { -// -// featureList.forEach(function (variant) { -// -// if ('str' === variant.type) { -// -// menuItems.push({ -// label: 'Sort by allele length', -// click: function () { -// sortCallSetsByAlleleLength(self.sampleColumns, variant, self.sortDirection); -// self.sortDirection = (self.sortDirection === "ASC") ? "DESC" : "ASC"; -// self.trackView.repaintViews(); -// } -// }); -// -// } -// -// }); -// } -// -// -// function sortCallSetsByAlleleLength(callSets, variant, direction) { -// var d = (direction === "DESC") ? 1 : -1; -// Object.keys(callSets).forEach(function (property) { -// callSets[property].sort(function (a, b) { -// var aNan = isNaN(variant.calls[a.id].genotype[0]); -// var bNan = isNaN(variant.calls[b.id].genotype[0]); -// if (aNan && bNan) { -// return 0; -// } else if (aNan) { -// return 1; -// } else if (bNan) { -// return -1; -// } else { -// var a0 = getAlleleString(variant.calls[a.id], variant, 0); -// var a1 = getAlleleString(variant.calls[a.id], variant, 1); -// var b0 = getAlleleString(variant.calls[b.id], variant, 0); -// var b1 = getAlleleString(variant.calls[b.id], variant, 1); -// var result = Math.max(b0.length, b1.length) - Math.max(a0.length, a1.length); -// if (result === 0) { -// result = Math.min(b0.length, b1.length) - Math.min(a0.length, a1.length); -// } -// return d * result; -// } -// }); -// }); -// } -// -// -// return menuItems; -// -// }; - menuItemList() { - - const menuItems = [] + const menuItems = []; // color-by INFO attribute if (this.header.INFO) { @@ -611,237 +516,220 @@ class VariantTrack extends TrackBase { // For now stick to explicit info fields (well, exactly 1 for starters) if (this.header.INFO) { //const stringInfoKeys = Object.keys(this.header.INFO).filter(key => this.header.INFO[key].Type === "String") - const colorByItems = this._colorByItems - menuItems.push('
') - const $e = $('
') - $e.text('Color by:') - menuItems.push({name: undefined, object: $e, click: undefined, init: undefined}) + const colorByItems = this._colorByItems; + menuItems.push('
'); + const e = document.createElement('div'); + e.classList.add('igv-track-menu-category', 'igv-track-menu-border-top'); + e.textContent = 'Color by:'; + menuItems.push({ name: undefined, object: e, click: undefined, init: undefined }); for (let key of colorByItems.keys()) { - const selected = (this.colorBy === key) - menuItems.push(this.colorByCB({key: key, label: colorByItems.get(key)}, selected)) + const selected = (this.colorBy === key); + menuItems.push(this.colorByCB({ key: key, label: colorByItems.get(key) }, selected)); } - menuItems.push(this.colorByCB({key: 'info', label: 'Info field...'})) - + menuItems.push(this.colorByCB({ key: 'info', label: 'Info field...' })); } } if (true === doSortByAttributes(this.browser.sampleInfo, this.sampleKeys)) { + menuItems.push('
'); - menuItems.push('
') + const sortLabel = document.createElement('div'); + sortLabel.textContent = 'Sort by attribute:'; + menuItems.push(sortLabel); - menuItems.push("Sort by attribute:") for (const attribute of this.browser.sampleInfo.attributeNames) { - if (this.sampleKeys.some(s => { - const attrs = this.browser.sampleInfo.getAttributes(s) - return attrs && attrs[attribute] + const attrs = this.browser.sampleInfo.getAttributes(s); + return attrs && attrs[attribute]; })) { + const object = document.createElement('div'); + object.innerHTML = `  ${attribute.split(SampleInfo.emptySpaceReplacement).join(' ')}`; - - const object = $('
') - object.html(`  ${attribute.split(SampleInfo.emptySpaceReplacement).join(' ')}`) - - function attributeSort() { - const sortDirection = this._sortDirections.get(attribute) || 1 - this.sortByAttribute(attribute, sortDirection) + const attributeSort = () => { + const sortDirection = this._sortDirections.get(attribute) || 1; + this.sortByAttribute(attribute, sortDirection); this.config.sort = { option: "ATTRIBUTE", attribute: attribute, direction: sortDirection > 0 ? "ASC" : "DESC" - } - this._sortDirections.set(attribute, sortDirection * -1) - } + }; + this._sortDirections.set(attribute, sortDirection * -1); + }; - menuItems.push({object, click: attributeSort}) + menuItems.push({ object, click: attributeSort }); } } } - menuItems.push('
') + menuItems.push('
'); if (this.getSampleCount() > 0) { - menuItems.push({object: $('
')}) + menuItems.push({ object: document.createElement('div').classList.add('igv-track-menu-border-top') }); + const showGenotypesCheckbox = createCheckbox("Show Genotypes", this.showGenotypes); menuItems.push({ - object: $(createCheckbox("Show Genotypes", this.showGenotypes)), + object: showGenotypesCheckbox, click: function showGenotypesHandler() { - this.showGenotypes = !this.showGenotypes - this.trackView.checkContentHeight() - this.trackView.repaintViews() - this.browser.sampleNameControl.performClickWithState(this.browser, this.showGenotypes) - this.browser.sampleInfoControl.performClickWithState(this.browser, this.showGenotypes) + this.showGenotypes = !this.showGenotypes; + this.trackView.checkContentHeight(); + this.trackView.repaintViews(); + this.browser.sampleNameControl.performClickWithState(this.browser, this.showGenotypes); + this.browser.sampleInfoControl.performClickWithState(this.browser, this.showGenotypes); } - }) + }); } - menuItems.push({object: $('
')}) + menuItems.push({ object: document.createElement('div').classList.add('igv-track-menu-border-top') }); for (let displayMode of ["COLLAPSED", "SQUISHED", "EXPANDED"]) { - var lut = - { - "COLLAPSED": "Collapse", - "SQUISHED": "Squish", - "EXPANDED": "Expand" - } + var lut = { + "COLLAPSED": "Collapse", + "SQUISHED": "Squish", + "EXPANDED": "Expand" + }; - menuItems.push( - { - object: $(createCheckbox(lut[displayMode], displayMode === this.displayMode)), - click: function displayModeHandler() { - this.displayMode = displayMode - this.trackView.checkContentHeight() - this.trackView.repaintViews() - } - }) + menuItems.push({ + object: createCheckbox(lut[displayMode], displayMode === this.displayMode), + click: function displayModeHandler() { + this.displayMode = displayMode; + this.trackView.checkContentHeight(); + this.trackView.repaintViews(); + } + }); } // Experimental JBrowse circular view integration if (this.browser.circularView) { - - menuItems.push('
') + menuItems.push('
'); menuItems.push({ label: 'Add SVs to circular view', click: function circularViewHandler() { - const inView = [] + const inView = []; for (let viewport of this.trackView.viewports) { - this.sendChordsForViewport(viewport) + this.sendChordsForViewport(viewport); } } - }) + }); } // Experimental CNVPytor support if (this.canCovertToPytor()) { - menuItems.push('
') + menuItems.push('
'); menuItems.push({ label: 'Convert to CNVpytor track', click: function cnvPytorHandler() { - this.convertToPytor() + this.convertToPytor(); } - }) + }); } - return menuItems + return menuItems; } - contextMenuItemList(clickState) { - - const list = [] + const list = []; if (this.hasSamples() && this.showGenotypes) { - const referenceFrame = clickState.viewport.referenceFrame - const genomicLocation = clickState.genomicLocation + const referenceFrame = clickState.viewport.referenceFrame; + const genomicLocation = clickState.genomicLocation; // We can't know genomic location intended with precision, define a buffer 5 "pixels" wide in genomic coordinates - const bpWidth = referenceFrame.toBP(2.5) - - const direction = this._sortDirections.get('genotype') || 1 - this._sortDirections.set('genotype', direction * -1) // Toggle for next sort + const bpWidth = referenceFrame.toBP(2.5); - list.push( - { - label: 'Sort by genotype', - click: (e) => { + const direction = this._sortDirections.get('genotype') || 1; + this._sortDirections.set('genotype', direction * -1); // Toggle for next sort - const sort = { - direction, - option: 'genotype', - chr: clickState.viewport.referenceFrame.chr, - start: Math.floor(genomicLocation - bpWidth), - end: Math.ceil(genomicLocation + bpWidth) - - } - const viewport = clickState.viewport - const features = viewport.cachedFeatures - this.sortSamplesByGenotype(sort, features) - - this.config.sort = sort - } + list.push({ + label: 'Sort by genotype', + click: (e) => { + const sort = { + direction, + option: 'genotype', + chr: clickState.viewport.referenceFrame.chr, + start: Math.floor(genomicLocation - bpWidth), + end: Math.ceil(genomicLocation + bpWidth) + }; + const viewport = clickState.viewport; + const features = viewport.cachedFeatures; + this.sortSamplesByGenotype(sort, features); + + this.config.sort = sort; } - ) - list.push('
') + }); + list.push('
'); } // Experimental JBrowse circular view integration if (this.browser.circularView) { - - const viewport = clickState.viewport + const viewport = clickState.viewport; list.push({ label: 'Add SVs to Circular View', click: () => { - this.sendChordsForViewport(viewport) + this.sendChordsForViewport(viewport); } - }) - list.push('
') + }); + list.push('
'); } - return list - + return list; } - - async sortSamplesByGenotype({chr, position, start, end, direction}, featureList) { - - if (start === undefined) start = position - 1 - if (end === undefined) end = position + async sortSamplesByGenotype({ chr, position, start, end, direction }, featureList) { + if (start === undefined) start = position - 1; + if (end === undefined) end = position; if (!featureList) { - featureList = await this.featureSource.getFeatures({chr, start, end}) + featureList = await this.featureSource.getFeatures({ chr, start, end }); } - if (!featureList) return + if (!featureList) return; - const scores = new Map() - const d2 = (direction === "ASC" ? 1 : -1) + const scores = new Map(); + const d2 = (direction === "ASC" ? 1 : -1); // Compute score for each sample for (let variant of featureList) { - if (variant.end < start) continue - if (variant.start > end) break + if (variant.end < start) continue; + if (variant.start > end) break; for (let call of variant.calls) { - const sample = call.sample - const callScore = call.zygosityScore() - scores.set(sample, scores.has(sample) ? scores.get(sample) + callScore : callScore) + const sample = call.sample; + const callScore = call.zygosityScore(); + scores.set(sample, scores.has(sample) ? scores.get(sample) + callScore : callScore); } } // Now sort sample names by score this.sampleKeys.sort(function (a, b) { - let sa = scores.get(a) || 0 - let sb = scores.get(b) || 0 - return d2 * (sa - sb) - }) + let sa = scores.get(a) || 0; + let sb = scores.get(b) || 0; + return d2 * (sa - sb); + }); - this.trackView.repaintViews() + this.trackView.repaintViews(); } sortByAttribute(attribute, sortDirection) { - this.config.sort = { option: "ATTRIBUTE", attribute: attribute, direction: sortDirection === 1 ? "ASC" : "DESC" - } - - this.sampleKeys = this.browser.sampleInfo.getSortedSampleKeysByAttribute(this.sampleKeys, attribute, sortDirection) - this.trackView.repaintViews() + }; + this.sampleKeys = this.browser.sampleInfo.getSortedSampleKeysByAttribute(this.sampleKeys, attribute, sortDirection); + this.trackView.repaintViews(); } - sendChordsForViewport(viewport) { - const refFrame = viewport.referenceFrame - let inView + const refFrame = viewport.referenceFrame; + let inView; if ("all" === refFrame.chr) { - const all = this.featureSource.getAllFeatures() - const arrays = Object.keys(all).map(k => all[k]) - inView = [].concat(...arrays) + const all = this.featureSource.getAllFeatures(); + const arrays = Object.keys(all).map(k => all[k]); + inView = [].concat(...arrays); } else { - inView = this.featureSource.featureCache.queryFeatures(refFrame.chr, refFrame.start, refFrame.end) - + inView = this.featureSource.featureCache.queryFeatures(refFrame.chr, refFrame.start, refFrame.end); } - const chords = makeVCFChords(inView) - sendChords(chords, this, refFrame, 0.5) + const chords = makeVCFChords(inView); + sendChords(chords, this, refFrame, 0.5); } /** @@ -851,47 +739,44 @@ class VariantTrack extends TrackBase { * @returns {{init: undefined, name: undefined, click: clickHandler, object: (jQuery|HTMLElement|jQuery.fn.init)}} */ colorByCB(menuItem, showCheck) { - - const $e = $(createCheckbox(menuItem.label, showCheck)) + const e = createCheckbox(menuItem.label, showCheck); if (menuItem.key !== 'info') { - function clickHandler() { - const colorBy = ('none' === menuItem.key) ? undefined : menuItem.key - this.colorBy = colorBy - this.config.colorBy = colorBy - this.trackView.repaintViews() - } - - return {name: undefined, object: $e, click: clickHandler, init: undefined} + const clickHandler = () => { + const colorBy = ('none' === menuItem.key) ? undefined : menuItem.key; + this.colorBy = colorBy; + this.config.colorBy = colorBy; + this.trackView.repaintViews(); + }; + + return { name: undefined, object: e, click: clickHandler, init: undefined }; } else { - function dialogPresentationHandler(ev) { + const dialogPresentationHandler = (ev) => { this.browser.inputDialog.present({ label: 'Info field', value: '', callback: (infoField) => { if (infoField) { - this.colorBy = infoField - this._colorByItems.set(infoField, infoField) + this.colorBy = infoField; + this._colorByItems.set(infoField, infoField); } else { - this.colorBy = undefined + this.colorBy = undefined; } - this.trackView.repaintViews() + this.trackView.repaintViews(); } - }, ev) - } + }, ev); + }; - return {name: undefined, object: $e, dialog: dialogPresentationHandler, init: undefined} + return { name: undefined, object: e, dialog: dialogPresentationHandler, init: undefined }; } } getState() { - - const config = super.getState() + const config = super.getState(); if (this.color && typeof this.color !== "function") { - config.color = this.color + config.color = this.color; } - return config - + return config; } /** @@ -900,33 +785,31 @@ class VariantTrack extends TrackBase { * @returns {any} */ getVariantColorTable(key) { - if (this.colorTables.has(key)) { - return this.colorTables.get(key) + return this.colorTables.get(key); } else if (this.colorTables.has("*")) { - return this.colorTables.get("*") + return this.colorTables.get("*"); } else { - let tbl + let tbl; switch (key) { - case "SVTYPE" : - tbl = SV_COLOR_TABLE - break + case "SVTYPE": + tbl = SV_COLOR_TABLE; + break; default: - tbl = new PaletteColorTable("Set1") + tbl = new PaletteColorTable("Set1"); } - this.colorTables.set(key, tbl) - return tbl + this.colorTables.set(key, tbl); + return tbl; } } - ///////////// CNVPytor converstion support follows //////////////////////////////////////////////////////////// + ///////////// CNVPytor conversion support follows //////////////////////////////////////////////////////////// /** - * This do-nothing method is neccessary to allow conversion to a CNVPytor track, which needs dom elements for an - * // axis. The dom elements are created as a side effect of this function being defined + * This do-nothing method is necessary to allow conversion to a CNVPytor track, which needs dom elements for an + * axis. The dom elements are created as a side effect of this function being defined */ - paintAxis() { - } + paintAxis() { } /** * Check conditions for pytor track @@ -936,81 +819,72 @@ class VariantTrack extends TrackBase { * (4) Not indexed -- must read entire file */ canCovertToPytor() { - if (this.config.indexURL) { - return false + return false; } if (this.header) { return Object.keys(this.sampleKeys).length === 1 && this.header.FORMAT && this.header.FORMAT.AD && - this.header.FORMAT.DP + this.header.FORMAT.DP; } else { // Cant know until header is read - return false + return false; } } async convertToPytor() { - // Store state in case track is reverted - this.variantState = {...this.config, ...this.getState()} - this.variantState.trackHeight = this.height - + this.variantState = { ...this.config, ...this.getState() }; + this.variantState.trackHeight = this.height; - this.trackView.startSpinner() - // The timeout is neccessary to give the spinner time to start. + this.trackView.startSpinner(); + // The timeout is necessary to give the spinner time to start. setTimeout(async () => { try { - const newConfig = Object.assign({}, this.config) - Object.setPrototypeOf(this, CNVPytorTrack.prototype) - - this.init(newConfig) - await this.postInit() - - this.trackView.clearCachedFeatures() - this.trackView.setTrackHeight(this.config.height || CNVPytorTrack.DEFAULT_TRACK_HEIGHT) - this.trackView.checkContentHeight() - this.trackView.updateViews() - this.trackView.track.autoHeight = false + const newConfig = Object.assign({}, this.config); + Object.setPrototypeOf(this, CNVPytorTrack.prototype); + this.init(newConfig); + await this.postInit(); + this.trackView.clearCachedFeatures(); + this.trackView.setTrackHeight(this.config.height || CNVPytorTrack.DEFAULT_TRACK_HEIGHT); + this.trackView.checkContentHeight(); + this.trackView.updateViews(); + this.trackView.track.autoHeight = false; } finally { - this.trackView.stopSpinner() + this.trackView.stopSpinner(); } - }, 100) - + }, 100); } } - function expandGenotype(call, variant) { - if (call.genotype) { - let gt = '' + let gt = ''; if (variant.alternateBases === ".") { - gt = "No Call" + gt = "No Call"; } else { - const altArray = variant.alternateBases.split(",") + const altArray = variant.alternateBases.split(","); for (let allele of call.genotype) { if (gt.length > 0) { - gt += " | " + gt += " | "; } if ('.' === allele) { - gt += '.' + gt += '.'; } else if (allele === 0) { - gt += variant.referenceBases + gt += variant.referenceBases; } else { - let alt = altArray[allele - 1].replace("<", "<") - gt += alt + let alt = altArray[allele - 1].replace("<", "<"); + gt += alt; } } } - return gt + return gt; } } - const SV_COLOR_TABLE = new ColorTable({ 'DEL': '#ff2101', 'INS': '#001888', @@ -1019,7 +893,6 @@ const SV_COLOR_TABLE = new ColorTable({ 'CNV': '#8931ff', 'BND': '#891100', '*': '#002eff' -}) - +}); -export default VariantTrack +export default VariantTrack;