UNPKG

ideogram

Version:

Chromosome visualization with D3.js

461 lines (388 loc) 13.2 kB
/** * @fileoverview Methods for ideogram annotations. * Annotations are graphical objects that represent features of interest * located on the chromosomes, e.g. genes or variations. They can * appear beside a chromosome, overlaid on top of it, or between multiple * chromosomes. */ import * as d3selection from 'd3-selection'; import * as d3fetch from 'd3-fetch'; import {BedParser} from '../parsers/bed-parser'; import {Object} from '../lib.js'; import { drawHeatmaps, deserializeAnnotsForHeatmap } from './heatmap'; import { onLoadAnnots, onDrawAnnots, startHideAnnotTooltipTimeout, onWillShowAnnotTooltip, showAnnotTooltip } from './events'; import {drawAnnots, drawProcessedAnnots} from './draw'; import {getHistogramBars} from './histogram'; import {drawSynteny} from './synteny'; var d3 = Object.assign({}, d3selection, d3fetch); function setOriginalTrackIndexes(rawAnnots) { var keys, annotsByChr, annots, annot, i, j, trackIndexOriginal, setAnnotsByChr, setAnnots, numAvailTracks; keys = rawAnnots.keys; // If this method is unnecessary, pass through if ( keys.length < 4 || keys[3] !== 'trackIndex' || keys[4] === 'trackIndexOriginal' ) { return rawAnnots; } numAvailTracks = 1; annotsByChr = rawAnnots.annots; setAnnotsByChr = []; for (i = 0; i < annotsByChr.length; i++) { annots = annotsByChr[i]; setAnnots = []; for (j = 0; j < annots.annots.length; j++) { annot = annots.annots[j].slice(); trackIndexOriginal = annot[3]; if (trackIndexOriginal + 1 > numAvailTracks) { numAvailTracks = trackIndexOriginal + 1; } annot.splice(4, 0, trackIndexOriginal); setAnnots.push(annot); } setAnnotsByChr.push({chr: annots.chr, annots: setAnnots}); } keys.splice(4, 0, 'trackIndexOriginal'); rawAnnots = {keys: keys, annots: setAnnotsByChr}; this.numAvailTracks = numAvailTracks; return rawAnnots; } /** * Proccesses genome annotation data. * * This method converts raw annotation data from server, which is structured as * an array of arrays, into a more verbose data structure consisting of an * array of objects. It also adds pixel offset information. */ function processAnnotData(rawAnnots) { var keys, numTracks, i, j, k, m, n, annot, annots, thisAnnot, annotsByChr, chr, chrs, chrModel, ra, startPx, stopPx, px, annotTrack, unorderedAnnots, colorMap, colors, omittedAnnots, numOmittedTracks, numAvailTracks, ideo = this, config = ideo.config; omittedAnnots = {}; colorMap = [ ['F00'], ['F00', '88F'], ['F00', 'CCC', '88F'], ['F00', 'FA0', '0AF', '88F'], ['F00', 'FA0', 'CCC', '0AF', '88F'], ['F00', 'FA0', '875', '578', '0AF', '88F'], ['F00', 'FA0', '875', 'CCC', '578', '0AF', '88F'], ['F00', 'FA0', '7A0', '875', '0A7', '578', '0AF', '88F'], ['F00', 'FA0', '7A0', '875', 'CCC', '0A7', '578', '0AF', '88F'], ['F00', 'FA0', '7A0', '875', '552', '255', '0A7', '578', '0AF', '88F'] ]; keys = rawAnnots.keys; rawAnnots = rawAnnots.annots; numTracks = config.numAnnotTracks; numAvailTracks = ideo.numAvailTracks; colors = colorMap[numAvailTracks - 1]; if (numTracks > 10) { console.error( 'Ideogram only displays up to 10 tracks at a time. ' + 'You specified ' + numTracks + ' tracks. ' + 'Perhaps consider a different way to visualize your data.' ); } annots = []; m = -1; for (i = 0; i < rawAnnots.length; i++) { annotsByChr = rawAnnots[i]; chr = annotsByChr.chr; chrModel = ideo.chromosomes[config.taxid][chr]; if (typeof chrModel === 'undefined') { console.warn( 'Chromosome "' + chr + '" undefined in ideogram; ' + annotsByChr.annots.length + ' annotations not shown' ); continue; } m++; annots.push({chr: annotsByChr.chr, annots: []}); for (j = 0; j < annotsByChr.annots.length; j++) { ra = annotsByChr.annots[j]; annot = {}; for (k = 0; k < keys.length; k++) { annot[keys[k]] = ra[k]; } annot.stop = annot.start + annot.length; startPx = ideo.convertBpToPx(chrModel, annot.start); stopPx = ideo.convertBpToPx(chrModel, annot.stop); px = Math.round((startPx + stopPx) / 2); annot.chr = chr; annot.chrIndex = i; annot.px = px; annot.startPx = startPx; annot.stopPx = stopPx; if (config.annotationTracks) { // Client annotations, as in annotations-tracks.html annot.trackIndex = ra[3]; annotTrack = config.annotationTracks[annot.trackIndex]; if (annotTrack.color) { annot.color = annotTrack.color; } if (annotTrack.shape) { annot.shape = annotTrack.shape; } annots[m].annots.push(annot); } else if (keys[3] === 'trackIndex' && numAvailTracks !== 1) { // Sparse server annotations, as in annotations-track-filters.html annot.trackIndex = ra[3]; annot.trackIndexOriginal = ra[4]; annot.color = '#' + colors[annot.trackIndexOriginal]; // Catch annots that will be omitted from display if (annot.trackIndex > numTracks - 1) { if (annot.trackIndex in omittedAnnots) { omittedAnnots[annot.trackIndex].push(annot); } else { omittedAnnots[annot.trackIndex] = [annot]; } continue; } annots[m].annots.push(annot); } else if ( keys.length > 3 && keys[3] in {trackIndex: 1, color: 1, shape: 1} === false && keys[4] === 'trackIndexOriginal' ) { // Dense server annotations for (n = 4; n < keys.length; n++) { thisAnnot = Object.assign({}, annot); // copy by value thisAnnot.trackIndex = n - 4; thisAnnot.trackIndexOriginal = n - 3; thisAnnot.color = '#' + colors[thisAnnot.trackIndexOriginal]; annots[m].annots.push(thisAnnot); } } else { // Basic client annotations, as in annotations-basic.html // and annotations-external.html annot.trackIndex = 0; if (!annot.color) { annot.color = config.annotationsColor; } if (!annot.shape) { annot.shape = 'triangle'; } annots[m].annots.push(annot); } } } numOmittedTracks = Object.keys(omittedAnnots).length; if (numOmittedTracks) { console.warn( 'Ideogram configuration specified ' + numTracks + ' tracks, ' + 'but loaded annotations contain ' + numOmittedTracks + ' ' + 'extra tracks.' ); } // Ensure annotation containers are ordered by chromosome unorderedAnnots = annots; annots = []; chrs = ideo.chromosomesArray; for (i = 0; i < chrs.length; i++) { chr = chrs[i].name; for (j = 0; j < unorderedAnnots.length; j++) { annot = unorderedAnnots[j]; if (annot.chr === chr) { annots.push(annot); } } } return annots; } /** * Reset displayed tracks to the originally displayed */ function restoreDefaultTracks() { var ideo = this; ideo.config.numAnnotTracks = ideo.config.annotationsNumTracks; d3.selectAll(ideo.selector + ' .annot').remove(); ideo.drawAnnots(ideo.processAnnotData(ideo.rawAnnots)); } /** * Adds or removes tracks from the displayed list of tracks. * Only works when raw annotations are dense. * * @param trackIndexes Array of indexes of tracks to display */ function updateDisplayedTracks(trackIndexes) { var displayedRawAnnotsByChr, displayedAnnots, i, j, rawAnnots, annots, annot, trackIndex, ideo = this, annotsByChr = ideo.rawAnnots.annots; displayedRawAnnotsByChr = []; ideo.config.numAnnotTracks = trackIndexes.length; // Filter displayed tracks by selected track indexes for (i = 0; i < annotsByChr.length; i++) { annots = annotsByChr[i]; displayedAnnots = []; for (j = 0; j < annots.annots.length; j++) { annot = annots.annots[j].slice(); // copy array by value trackIndex = annot[3] + 1; if (trackIndexes.includes(trackIndex)) { annot[3] = trackIndexes.indexOf(trackIndex); displayedAnnots.push(annot); } } displayedRawAnnotsByChr.push({chr: annots.chr, annots: displayedAnnots}); } rawAnnots = {keys: ideo.rawAnnots.keys, annots: displayedRawAnnotsByChr}; displayedAnnots = ideo.processAnnotData(rawAnnots); d3.selectAll(ideo.selector + ' .annot').remove(); ideo.drawAnnots(displayedAnnots); ideogram.displayedTrackIndexes = trackIndexes; return displayedAnnots; } /** * Initializes various annotation settings. Constructor help function. */ function initAnnotSettings() { var ideo = this, config = ideo.config; if ( config.annotationsPath || config.localAnnotationsPath || ideo.annots || config.annotations ) { if (!config.annotationHeight) { var annotHeight = Math.round(config.chrHeight / 100); this.config.annotationHeight = annotHeight; } if (config.annotationTracks) { this.config.numAnnotTracks = config.annotationTracks.length; } else if (config.annotationsNumTracks) { this.config.numAnnotTracks = config.annotationsNumTracks; } else { this.config.numAnnotTracks = 1; } this.config.annotTracksHeight = config.annotationHeight * config.numAnnotTracks; if (typeof config.barWidth === 'undefined') { this.config.barWidth = 3; } } else { this.config.annotTracksHeight = 0; } if (typeof config.annotationsColor === 'undefined') { this.config.annotationsColor = '#F00'; } if (config.showAnnotTooltip !== false) { this.config.showAnnotTooltip = true; } if (config.onWillShowAnnotTooltip) { this.onWillShowAnnotTooltipCallback = config.onWillShowAnnotTooltip; } if (config.annotationsLayout === 'heatmap') { // window.onresize = function() { // ideo.drawHeatmaps(ideo.annots); // }; // ideo.isScrolling = null; // Listen for scroll events // window.addEventListener('scroll', function ( event ) { // // // Clear our timeout throughout the scroll // window.clearTimeout( ideo.isScrolling ); // // // Set a timeout to run after scrolling ends // ideo.isScrolling = setTimeout(function() { // // // Run the callback // console.log('Scrolling has stopped.'); // ideo.drawHeatmaps(ideo.annots); // // }, 300); // // // }, false); // // window.onscroll = function() { // ideo.drawHeatmaps(ideo.annots); // // console.log('onscroll') // }; } } /** * Requests annotations URL via HTTP, sets ideo.rawAnnots for downstream * processing. * * @param annotsUrl Absolute or relative URL native or BED annotations file */ function fetchAnnots(annotsUrl) { var tmp, extension, ideo = this; function afterRawAnnots(rawAnnots) { // Ensure annots are ordered by chromosome ideo.rawAnnots.annots = rawAnnots.annots.sort(function(a, b) { return Ideogram.naturalSort(a.chr, b.chr); }); if (ideo.config.heatmaps) { ideo.deserializeAnnotsForHeatmap(rawAnnots); } if (ideo.onLoadAnnotsCallback) { ideo.onLoadAnnotsCallback(); } } if (annotsUrl.slice(0, 4) !== 'http') { d3.json(ideo.config.annotationsPath) .then(function(data) { ideo.rawAnnots = data; afterRawAnnots(ideo.rawAnnots); }); return; } tmp = annotsUrl.split('?')[0].split('.'); extension = tmp[tmp.length - 1]; if (extension !== 'bed' && extension !== 'json') { extension = extension.toUpperCase(); alert( 'Ideogram.js only supports BED and Ideogram JSON at the moment. ' + 'Sorry, check back soon for ' + extension + ' support!' ); return; } d3.text(annotsUrl).then(function(text) { if (extension === 'bed') { ideo.rawAnnots = new BedParser(text, ideo).rawAnnots; } else { ideo.rawAnnots = JSON.parse(text); } afterRawAnnots(ideo.rawAnnots); }); } /** * Fills out annotations data structure such that its top-level list of arrays * matches that of this ideogram's chromosomes list in order and number * Fixes https://github.com/eweitz/ideogram/issues/66 */ function fillAnnots(annots) { var filledAnnots, chrs, chrArray, i, chr, annot, chrIndex; filledAnnots = []; chrs = []; chrArray = this.chromosomesArray; for (i = 0; i < chrArray.length; i++) { chr = chrArray[i].name; chrs.push(chr); filledAnnots.push({chr: chr, annots: []}); } for (i = 0; i < annots.length; i++) { annot = annots[i]; chrIndex = chrs.indexOf(annot.chr); if (chrIndex !== -1) { filledAnnots[chrIndex] = annot; } } return filledAnnots; } export { onLoadAnnots, onDrawAnnots, processAnnotData, restoreDefaultTracks, updateDisplayedTracks, initAnnotSettings, fetchAnnots, drawAnnots, getHistogramBars, drawHeatmaps, deserializeAnnotsForHeatmap, fillAnnots, drawProcessedAnnots, drawSynteny, startHideAnnotTooltipTimeout, showAnnotTooltip, onWillShowAnnotTooltip, setOriginalTrackIndexes }