client/client/modules/render/tracks/vcf/index.js (425 lines of code) (raw):

import { StatisticsContainer, VariantContainer, VCFCollapsedRenderer, VCFExpandedRenderer, VcfTransformer } from './internal'; import {GENETrack} from '../gene'; import VcfConfig from './vcfConfig'; import {VcfDataService} from '../../../../dataServices'; import {default as menu} from './menu'; import Menu from '../../core/menu'; import {variantsView} from './modes'; import {menu as menuUtilities, PlaceholderRenderer} from '../../utilities'; import {EventVariationInfo} from '../../../../app/shared/utils/events'; export class VCFTrack extends GENETrack { _collapsedRenderer: VCFCollapsedRenderer = null; _lastHovered = null; _variantsMaximumRange; _zoomInRenderer: PlaceholderRenderer = new PlaceholderRenderer(this); _highlightProfile = null; _highlightProfileConditions = []; projectContext; static getTrackDefaultConfig() { return VcfConfig; } get stateKeys() { return ['variantsView', 'header', 'collapseSamples', 'variantsDensity']; } get featuresFilteringEnabled () { return false; } static preStateMutatorFn = (track) => ({ oldVariantsView: track.state.variantsView, oldCollapseSamples: track.state.collapseSamples, oldVariantsDensity: track.state.variantsDensity }); static postStateMutatorFn = (track, key, prePayload) => { const { oldVariantsView, oldCollapseSamples, oldVariantsDensity } = prePayload || {}; track.transformer.collapsed = track.state.variantsView === variantsView.variantsViewCollapsed; if ( oldVariantsView !== track.state.variantsView || oldCollapseSamples !== track.state.collapseSamples ) { track.cache = {}; track._flags.renderReset = true; } if (oldVariantsDensity !== track.state.variantsDensity) { track._flags.heightChanged = true; } track.updateAndRefresh(); track.reportTrackState(); }; static Menu = Menu( menu, { postStateMutatorFn: VCFTrack.postStateMutatorFn, preStateMutatorFn: VCFTrack.preStateMutatorFn } ); constructor(opts) { super(opts); this._variantsMaximumRange = opts.variantsMaximumRange; this._highlightProfileConditions = this.projectContext.highlightProfileConditions; this.transformer.collapsed = this.state.variantsView === variantsView.variantsViewCollapsed; this._actions = [ { enabled: function () { return true; }, label: 'Navigation', type: 'groupLinks', links: [{ label: 'Prev', handleClick: ::this.prevVariation }, { label: 'Next', handleClick: ::this.nextVariation }] } ]; } async updateCache() { if (this._variantsMaximumRange >= this.viewport.actualBrushSize) { return await super.updateCache(); } return false; } trackSettingsChanged(params) { if (this.config.bioDataItemId === params.id && this.config.format.toLowerCase() === 'vcf') { const settings = params.settings; settings.forEach(setting => { const menuItem = menuUtilities.findMenuItem(this._menu, setting.name); if (menuItem.type === 'checkbox') { menuItem.enable(); } }); } } globalSettingsChanged(state) { const changed = this._variantsMaximumRange !== state.variantsMaximumRange || this._highlightProfile !== state.highlightProfile; this._variantsMaximumRange = state.variantsMaximumRange; this._highlightProfile = state.highlightProfile; this._highlightProfileConditions = this.projectContext.highlightProfileConditions; super.globalSettingsChanged(state); Promise.resolve().then(async () => { if (changed && this._variantsMaximumRange > this.viewport.actualBrushSize) { this.transformer.highlightProfileConditions = this._highlightProfileConditions; try { this.unsetError(); await this.updateCache(); } catch (e) { this.reportError(e.message); } } if (changed) { this._flags.renderReset = true; } this._flags.renderFeaturesChanged = true; this.requestRenderRefresh(); }); } getSettings() { if (this._menu) { return this._menu; } this._menu = this.constructor.Menu.attach(this, {browserId: this.browserId}); this.hotKeyListener = (event) => { if (event) { const path = event.split('>'); if (path && path[0] === 'vcf') { const menuItem = menuUtilities.findMenuItem(this._menu, event); if (menuItem) { if (menuItem.type === 'button') { menuItem.perform(); } else if (menuItem.type === 'checkbox') { menuItem.isEnabled() ? menuItem.disable() : menuItem.enable(); } } } } }; const _hotKeyListener = ::this.hotKeyListener; const self = this; this._removeHotKeyListener = function () { self.dispatcher.removeListener('hotkeyPressed', _hotKeyListener); }; this.dispatcher.on('hotkeyPressed', _hotKeyListener); return this._menu; } get transformer() { if (!this._transformer) { this._transformer = new VcfTransformer(this.trackConfig, this.config.chromosome, this._highlightProfileConditions); } return this._transformer; } get dataService() { if (!this._dataService) { this._dataService = new VcfDataService(this.dispatcher); } return this._dataService; } get downloadHistogramFn() { return () => new Promise(resolve => resolve([])); } get downloadDataFn() { return ::this.dataService.loadVcfTrack; } applyAdditionalRequestParameters(params) { params.collapsed = this.state.variantsView === variantsView.variantsViewCollapsed; } get renderer() { if (!this._renderer) { this._renderer = new VCFExpandedRenderer( this.trackConfig, this.transformer, this ); } if (!this._collapsedRenderer) { this._collapsedRenderer = new VCFCollapsedRenderer( VcfConfig, this ); } if (this.state.variantsView === variantsView.variantsViewCollapsed) { return this._collapsedRenderer; } return this._renderer; } canScroll(delta) { if (this.state.variantsView === variantsView.variantsViewExpanded) { return super.canScroll(delta); } return false; } isScrollable() { if (this.state.variantsView === variantsView.variantsViewExpanded) { return super.isScrollable(); } return false; } _getZoomInPlaceholderText() { const unitThreshold = 1000; const noReadText = { unit: this._variantsMaximumRange < unitThreshold ? 'BP' : 'kBP', value: this._variantsMaximumRange < unitThreshold ? this._variantsMaximumRange : Math.ceil(this._variantsMaximumRange / unitThreshold) }; return `Zoom in to see variants. Minimal zoom level is at ${noReadText.value}${noReadText.unit}`; } render(flags) { if (flags.renderReset) { this.container.removeChildren(); this.container.addChild(this._zoomInRenderer.container); this._zoomInRenderer.init(this._getZoomInPlaceholderText(), { height: this._pixiRenderer.height, width: this._pixiRenderer.width }); } else if (flags.widthChanged || flags.heightChanged) { this._zoomInRenderer.init(this._getZoomInPlaceholderText(), { height: this._pixiRenderer.height, width: this._pixiRenderer.width }); } this._zoomInRenderer.container.visible = this._variantsMaximumRange < this.viewport.actualBrushSize; this.renderer.container.visible = this._variantsMaximumRange >= this.viewport.actualBrushSize; if (this.state.variantsView === variantsView.variantsViewExpanded) { return super.render(flags); } else { let somethingChanged = false; if (flags.renderReset) { this.container.addChild(this.renderer.container); somethingChanged = true; } if (flags.brushChanged || flags.widthChanged || flags.heightChanged || flags.renderReset || flags.dataChanged) { this.renderer.height = this.height; this.renderer.render(this.viewport, this.cache, flags.heightChanged || flags.dataChanged, this._showCenterLine); somethingChanged = true; } return somethingChanged; } } onStatisticsClicked (statisticsContainer: StatisticsContainer) { const length = statisticsContainer.variant.endIndex - statisticsContainer.variant.startIndex; const bubbleExpandFactor = 5; this.moveBrush({ end: statisticsContainer.variant.endIndex + length / bubbleExpandFactor, start: statisticsContainer.variant.startIndex - length / bubbleExpandFactor }); } onVariantContainerClicked (variantContainer: VariantContainer, position, sample) { const mapEndContainersFn = (m) => ({ chromosome: m.chromosome || this.config.chromosome.name, chromosomeId: this.dataConfig.chromosomeId, position: m.displayPosition || m.position }); const [endPoints] = variantContainer._endContainers .filter(m => m.visible === true) .map(mapEndContainersFn); const variantRequest = new EventVariationInfo( { chromosome: { id: this.config.chromosome.id, name: this.config.chromosome.name }, endPoints, id: variantContainer._variant.identifier, position: variantContainer._variant.serverStartIndex, type: variantContainer._variant.type, sample, vcfFileId: this.dataConfig.id, projectId: this.config.projectId, projectIdNumber: this.config.project.id } ); if (this.dataItemClicked !== null && this.dataItemClicked !== undefined) { this.dataItemClicked(this, variantRequest, {name: 'variant-request', position}); } } onClick({x, y}) { if (this.state.variantsView === variantsView.variantsViewExpanded) { const checkPositionResult = this.renderer.checkPosition(this.viewport, this.cache, {x, y}, false); if (checkPositionResult && checkPositionResult.length > 0) { const variant = checkPositionResult[0].feature; const self = this; const mapFn = function (m) { return { chromosome: m.mate.chromosome, chromosomeId: self.dataConfig.chromosomeId, position: m.mate.position, interChromosome: !m.mate.intraChromosome }; }; const alts = variant.alternativeAllelesInfo.filter(x => x.mate).map(mapFn); if (alts.length === 0 && !variant.interChromosome) { alts.push({ chromosome: this.config.chromosome.name, chromosomeId: this.dataConfig.chromosomeId, position: variant.endIndex, interChromosome: false }); } const filterFn = a => a.interChromosome || a.position < self.viewport.brush.start || a.position > self.viewport.brush.end; const [endPoints] = alts.filter(filterFn); const variantRequest = new EventVariationInfo( { chromosome: { id: this.config.chromosome.id, name: this.config.chromosome.name }, endPoints, id: variant.identifier, position: variant.serverStartIndex, type: variant.type, vcfFileId: this.dataConfig.id, projectId: this.config.projectId, projectIdNumber: this.config.project.id } ); if (this.dataItemClicked !== null && this.dataItemClicked !== undefined) { this.dataItemClicked(this, variantRequest, {name: 'variant-request', position: {x, y}}); } } } else { const variantContainer = this.renderer.onClick({x, y}); if (variantContainer && variantContainer instanceof StatisticsContainer) { this.onStatisticsClicked(variantContainer); } else if (variantContainer instanceof VariantContainer) { this.onVariantContainerClicked(variantContainer, {x, y}); } } } onHover({x, y}) { if (this.state.variantsView === variantsView.variantsViewExpanded) { return super.onHover({x, y}); } const hoveredItem = this.renderer.onMove({x, y}); if (hoveredItem && !(hoveredItem instanceof StatisticsContainer) && this.shouldDisplayTooltips) { const tooltip = [ ['Chromosome', this.config.chromosome.name], ['Start', hoveredItem.variant.startIndex], ['End', hoveredItem.variant.endIndex], ['ID', hoveredItem.variant.identifier], ['Type', hoveredItem.variant.type], ['REF', hoveredItem.variant.referenceAllele], ['ALT', hoveredItem.variant.alternativeAlleles.join(', ')], ['Quality', hoveredItem.variant.quality && hoveredItem.variant.quality.toFixed(2)] ]; const filters = hoveredItem.variant.failedFilters || []; if (filters.length > 0) { tooltip.push(['Filter', filters.map(filter => filter.value).join(', ')]); } this.tooltip.setContent(tooltip); this.tooltip.move({x, y}); this.tooltip.show({x, y}); } else { this.tooltip.hide(); } if (hoveredItem !== this._lastHovered) { this._lastHovered = hoveredItem; this.requestAnimation(); return false; } return true; } unhoverRenderer () { this._lastHovered = null; if (this.state.variantsView === variantsView.variantsViewCollapsed) { this.renderer.onMove(); } } onMouseOut() { super.onMouseOut(); this.unhoverRenderer(); // for correct animations this.requestAnimation(); } animate(time) { if (this.state.variantsView === variantsView.variantsViewCollapsed) { return this.renderer.animate(time); } return false; } getTooltipDataObject(isHistogram, data) { if (isHistogram) { return [ ['Count', data[0].value] ]; } else if (data.length > 0) { const variant = data[0].feature; const tooltip = [ ['Chromosome', this.config.chromosome.name], ['Start', variant.startIndex], ['End', variant.endIndex], ['ID', variant.identifier], ['Type', variant.type], ['REF', variant.referenceAllele], ['ALT', variant.alternativeAlleles.join(', ')], ['Quality', variant.quality && variant.quality.toFixed(2)] ]; const filters = variant.failedFilters || []; if (filters.length > 0) { tooltip.push(['Filter', filters.map(filter => filter.value).join(', ')]); } return tooltip; } } prevVariation() { const position = Math.floor((this.viewport.brush.end + this.viewport.brush.start) / 2); this._dataService.getPreviousVariations(this.config, this.projectContext.currentChromosome.id, position - 1).then( (data) => { this.viewport.selectPosition(data.startIndex); } ); } nextVariation() { const position = Math.floor((this.viewport.brush.end + this.viewport.brush.start) / 2); this._dataService.getNextVariations(this.config, this.projectContext.currentChromosome.id, position + 1).then( (data) => { this.viewport.selectPosition(data.startIndex); } ); } clearData() { if (typeof this._removeHotKeyListener === 'function') { this._removeHotKeyListener(); } super.clearData(); } }