UNPKG

ideogram

Version:

Chromosome visualization with D3.js

697 lines (568 loc) 18 kB
/** * @fileoveriew Methods for initialization */ import * as d3fetch from 'd3-fetch'; import * as d3selection from 'd3-selection'; import {Ploidy} from './ploidy'; import {Layout} from './layouts/layout'; import {Object} from './lib.js'; var d3 = Object.assign({}, d3fetch, d3selection); /** * High-level helper method for Ideogram constructor. * * @param config Configuration object. Enables setting Ideogram properties. */ function configure(config) { var orientation, chrWidth, chrHeight, container, rect; // Clone the config object, to allow multiple instantiations // without picking up prior ideogram's settings this.config = JSON.parse(JSON.stringify(config)); if (!this.config.debug) { this.config.debug = false; } if (!this.config.dataDir) { this.config.dataDir = this.getDataDir(); } if (!this.config.ploidy) { this.config.ploidy = 1; } if (this.config.ploidy > 1) { this.sexChromosomes = {}; if (!this.config.sex) { // Default to 'male' per human, mouse reference genomes. // TODO: The default sex value should probably be the heterogametic sex, // i.e. whichever sex has allosomes that differ in morphology. // In mammals and most insects that is the male. // However, in birds and reptiles, that is female. this.config.sex = 'male'; } if (this.config.ploidy === 2 && !this.config.ancestors) { this.config.ancestors = {M: '#ffb6c1', P: '#add8e6'}; this.config.ploidyDesc = 'MP'; } } if (!this.config.container) { this.config.container = 'body'; } this.selector = this.config.container + ' #_ideogram'; if (!this.config.resolution) { this.config.resolution = ''; } if ('showChromosomeLabels' in this.config === false) { this.config.showChromosomeLabels = true; } if (!this.config.orientation) { orientation = 'vertical'; this.config.orientation = orientation; } if (!this.config.chrHeight) { container = this.config.container; rect = document.querySelector(container).getBoundingClientRect(); if (orientation === 'vertical') { chrHeight = rect.height; } else { chrHeight = rect.width; } if (container === 'body') { chrHeight = 400; } this.config.chrHeight = chrHeight; } if (!this.config.chrWidth) { chrWidth = 10; chrHeight = this.config.chrHeight; if (chrHeight < 900 && chrHeight > 500) { chrWidth = Math.round(chrHeight / 40); } else if (chrHeight >= 900) { chrWidth = Math.round(chrHeight / 45); } this.config.chrWidth = chrWidth; } if (!this.config.chrMargin) { if (this.config.ploidy === 1) { this.config.chrMargin = 10; } else { // Defaults polyploid chromosomes to relatively small interchromatid gap this.config.chrMargin = Math.round(this.config.chrWidth / 4); } } if (!this.config.showBandLabels) { this.config.showBandLabels = false; } if ('showFullyBanded' in this.config) { this.config.showFullyBanded = config.showFullyBanded; } else { this.config.showFullyBanded = true; } if (!this.config.brush) { this.config.brush = null; } if (!this.config.rows) { this.config.rows = 1; } this.bump = Math.round(this.config.chrHeight / 125); this.adjustedBump = false; if (this.config.chrHeight < 200) { this.adjustedBump = true; this.bump = 4; } if (config.showBandLabels) { this.config.chrMargin += 20; } if (config.chromosome) { this.config.chromosomes = [config.chromosome]; if ('showBandLabels' in config === false) { this.config.showBandLabels = true; } if ('rotatable' in config === false) { this.config.rotatable = false; } } if (!this.config.showNonNuclearChromosomes) { this.config.showNonNuclearChromosomes = false; } this.initAnnotSettings(); this.config.chrMargin = ( this.config.chrMargin + this.config.chrWidth + this.config.annotTracksHeight * 2 ); if (config.onLoad) { this.onLoadCallback = config.onLoad; } if (config.onLoadAnnots) { this.onLoadAnnotsCallback = config.onLoadAnnots; } if (config.onDrawAnnots) { this.onDrawAnnotsCallback = config.onDrawAnnots; } if (config.onBrushMove) { this.onBrushMoveCallback = config.onBrushMove; } if (config.onWillShowAnnotTooltip) { this.onWillShowAnnotTooltipCallback = config.onWillShowAnnotTooltip; } if (config.onDidRotate) { this.onDidRotateCallback = config.onDidRotate; } this.coordinateSystem = 'iscn'; this.maxLength = { bp: 0, iscn: 0 }; this.organisms = { 9606: { commonName: 'Human', scientificName: 'Homo sapiens', scientificNameAbbr: 'H. sapiens', assemblies: { default: 'GCF_000001405.26', // GRCh38 GRCh38: 'GCF_000001405.26', GRCh37: 'GCF_000001405.13' } }, 10090: { commonName: 'Mouse', scientificName: 'Mus musculus', scientificNameAbbr: 'M. musculus', assemblies: { default: 'GCF_000001635.20' } }, 4641: { commonName: 'banana', scientificName: 'Musa acuminata', scientificNameAbbr: 'M. acuminata', assemblies: { default: 'mock' } } }; // A flat array of chromosomes // (this.chromosomes is an object of // arrays of chromosomes, keyed by organism) this.chromosomesArray = []; this.bandsToShow = []; this.chromosomes = {}; this.numChromosomes = 0; this.bandData = {}; this.init(); } /** * Configures chromosome data and calls downstream chromosome drawing functions */ function initDrawChromosomes(bandsArray) { var ideo = this, taxids = ideo.config.taxids, ploidy = ideo.config.ploidy, chrIndex = 0, taxid, bands, i, j, chrs, chromosome, chrModel; if (bandsArray.length > 0) { ideo.bandsArray = {}; } for (i = 0; i < taxids.length; i++) { taxid = taxids[i]; chrs = ideo.config.chromosomes[taxid]; if ( typeof chrBands !== 'undefined' && chrs.length >= chrBands.length / 2 ) { ideo.coordinateSystem = 'bp'; } ideo.chromosomes[taxid] = {}; ideo.setSexChromosomes(chrs); if ('bandsArray' in ideo) { ideo.bandsArray[taxid] = bandsArray; } for (j = 0; j < chrs.length; j++) { chromosome = chrs[j]; if ('bandsArray' in ideo) { bands = bandsArray[chrIndex]; } chrModel = ideo.getChromosomeModel(bands, chromosome, taxid, chrIndex); chrIndex += 1; if (typeof chromosome !== 'string') { chromosome = chromosome.name; } ideo.chromosomes[taxid][chromosome] = chrModel; ideo.chromosomesArray.push(chrModel); if ( 'sex' in ideo.config && ( ploidy === 2 && ideo.sexChromosomes.index + 2 === chrIndex || ideo.config.sex === 'female' && chrModel.name === 'Y' ) ) { continue; } ideo.drawChromosome(chrModel); } if (ideo.config.showBandLabels === true) { ideo.drawBandLabels(ideo.chromosomes); } ideo.handleRotateOnClick(); ideo._gotChrModels = true; // Prevent issue with errant rat centromeres } } /** * Attach any click handlers to rotate and toggle chromosomes */ function handleRotateOnClick() { var ideo = this; if (!('rotatable' in ideo.config && ideo.config.rotatable === false)) { d3.selectAll(ideo.selector + ' .chromosome').on('click', function () { ideo.rotateAndToggleDisplay(this); }); } else { d3.selectAll(ideo.selector + ' .chromosome') .style('cursor', 'default'); } } /** * Called when Ideogram has finished initializing. * Accounts for certain ideogram properties not being set until * asynchronous requests succeed, etc. */ function onLoad() { call(this.onLoadCallback); } function setOverflowScroll() { var ideo, config, ideoWidth, ideoInnerWrap, ideoOuterWrap, ideoSvg, ploidy, ploidyPad; ideo = this; config = ideo.config; ideoSvg = d3.select(config.container + ' svg#_ideogram'); ideoInnerWrap = d3.select(config.container + ' #_ideogramInnerWrap'); ideoOuterWrap = d3.select(config.container + ' #_ideogramOuterWrap'); ploidy = config.ploidy; ploidyPad = (ploidy - 1); if ( config.orientation === 'vertical' && config.perspective !== 'comparative' ) { ideoWidth = (ideo.numChromosomes + 2) * (config.chrWidth + config.chrMargin + ploidyPad); } else { return; // chrOffset = ideoSvg.select('.chromosome').nodes()[0].getBoundingClientRect(); // ideoWidth = config.chrHeight + chrOffset.x + 1; } ideoWidth = Math.round(ideoWidth * ploidy / config.rows); // Ensures absolutely-positioned elements, e.g. heatmap overlaps, display // properly if ideogram container also has position: absolute ideoOuterWrap .style('height', ideo._layout.getHeight() + 'px') ideoInnerWrap .style('max-width', ideoWidth + 'px') .style('overflow-x', 'scroll') .style('position', 'absolute'); ideoSvg.style('min-width', (ideoWidth - 5) + 'px'); } /** * Initializes an ideogram. * Sets some high-level properties based on instance configuration, * fetches band and annotation data if needed, and * writes an SVG element to the document to contain the ideogram */ function init() { var taxid, i, svgClass; var ideo = this; var t0 = new Date().getTime(); var bandsArray = [], numBandDataResponses = 0, resolution = this.config.resolution, accession; var promise = new Promise(function(resolve) { if (typeof ideo.config.organism === 'number') { // 'organism' is a taxid, e.g. 9606 ideo.getOrganismFromEutils(function() { ideo.getTaxids(resolve); }); } else { ideo.getTaxids(resolve); } }); promise.then(function(taxids) { taxid = taxids[0]; ideo.config.taxid = taxid; ideo.config.taxids = taxids; var assemblies, bandFileName; var bandDataFileNames = { 9606: '', 10090: '' }; for (i = 0; i < taxids.length; i++) { taxid = String(taxids[i]); if (!ideo.config.assembly) { ideo.config.assembly = 'default'; } assemblies = ideo.organisms[taxid].assemblies; if (ideo.assemblyIsAccession()) { accession = ideo.config.assembly; } else { accession = assemblies[ideo.config.assembly]; } bandFileName = []; bandFileName.push( Ideogram.slugify(ideo.organisms[taxid].scientificName) ); if (accession !== assemblies.default) { bandFileName.push(accession); } if ( taxid === '9606' && (accession in assemblies === 'false' && Object.values(assemblies).indexOf(ideo.config.assembly) === -1 || (resolution !== '' && resolution !== 850)) ) { bandFileName.push(resolution); } bandFileName = bandFileName.join('-') + '.js'; if (taxid === '9606' || taxid === '10090') { bandDataFileNames[taxid] = bandFileName; } if ( typeof accession !== 'undefined' && /GCA_/.test(ideo.config.assembly) === false && typeof chrBands === 'undefined' && taxid in bandDataFileNames ) { var bandDataUrl = ideo.config.dataDir + bandDataFileNames[taxid]; fetch(bandDataUrl) .then(function(response) { return response.text().then(function(text) { // Fetched data is a JavaScript variable, so assign it eval(text); // Ensures correct taxid is processed // in response callback; using simply upstream 'taxid' variable // gives the last *requested* taxid, which fails when dealing // with multiple taxa. var fetchedTaxid, tid, bandDataFileName; for (tid in bandDataFileNames) { bandDataFileName = bandDataFileNames[tid]; if ( response.url.includes(bandDataFileName) && bandDataFileName !== '' ) { fetchedTaxid = tid; } } ideo.bandData[fetchedTaxid] = chrBands; numBandDataResponses += 1; if (numBandDataResponses === taxids.length) { bandsArray = ideo.processBandData(); writeContainer(); } }); }); } else { if (typeof chrBands !== 'undefined') { // If bands already available, // e.g. via <script> tag in initial page load ideo.bandData[taxid] = chrBands; } bandsArray = ideo.processBandData(); writeContainer(); } } }); /** * Writes the HTML elements that contain this ideogram instance. */ function writeContainer() { if (ideo.config.annotationsPath) { ideo.fetchAnnots(ideo.config.annotationsPath); } // If ploidy description is a string, then convert it to the canonical // array format. String ploidyDesc is used when depicting e.g. parental // origin each member of chromosome pair in a human genome. // See ploidy-basic.html for usage example. if ( 'ploidyDesc' in ideo.config && typeof ideo.config.ploidyDesc === 'string' ) { var tmp = []; for (var i = 0; i < ideo.numChromosomes; i++) { tmp.push(ideo.config.ploidyDesc); } ideo.config.ploidyDesc = tmp; } // Organism ploidy description ideo._ploidy = new Ploidy(ideo.config); // Chromosome's layout ideo._layout = Layout.getInstance(ideo.config, ideo); svgClass = ''; if (ideo.config.showChromosomeLabels) { if (ideo.config.orientation === 'horizontal') { svgClass += 'labeledLeft '; } else { svgClass += 'labeled '; } } if ( ideo.config.annotationsLayout && ideo.config.annotationsLayout === 'overlay' ) { svgClass += 'faint'; } var gradients = ideo.getBandColorGradients(); var svgWidth = ideo._layout.getWidth(taxid); var svgHeight = ideo._layout.getHeight(taxid); d3.select(ideo.config.container) .append('div') .attr('id', '_ideogramOuterWrap') .append('div') .attr('id', '_ideogramInnerWrap') .append('svg') .attr('id', '_ideogram') .attr('class', svgClass) .attr('width', svgWidth) .attr('height', svgHeight) .html(gradients); ideo.isOnlyIdeogram = document.querySelectorAll('#_ideogram').length === 1; // Tooltip div setup w/ default styling. d3.select(ideo.config.container + ' #_ideogramOuterWrap').append("div") .attr('class', 'tooltip') .attr('id', 'tooltip') .style('opacity', 0) .style('position', 'absolute') .style('text-align', 'center') .style('padding', '4px') .style('font', '12px sans-serif') .style('background', 'white') .style('border', '1px solid black') .style('border-radius', '5px'); finishInit(); } /** * Completes high-level initialization. * Draws chromosomes and band labels, rotating as needed; * processes and draws annotations; * creates brush, emits notification of load completion, etc. */ function finishInit() { try { var t0A = new Date().getTime(); var i, config; config = ideo.config; ideo.initDrawChromosomes(bandsArray); // Waits for potentially large annotation dataset // to be received by the client, then triggers annotation processing if (config.annotationsPath) { function pa() { if (typeof ideo.timeout !== 'undefined') { window.clearTimeout(ideo.timeout); } ideo.rawAnnots = ideo.setOriginalTrackIndexes(ideo.rawAnnots); if (config.annotationsDisplayedTracks) { ideo.annots = ideo.updateDisplayedTracks(config.annotationsDisplayedTracks); } else { ideo.annots = ideo.processAnnotData(ideo.rawAnnots); if (ideo.config.filterable) { ideo.initCrossFilter(); } ideo.drawProcessedAnnots(ideo.annots); } } if (ideo.rawAnnots) { pa(); } else { (function checkAnnotData() { ideo.timeout = setTimeout(function() { if (!ideo.rawAnnots) { checkAnnotData(); } else { pa(); } }, 50 ); })(); } } if (ideo.config.showBandLabels === true) { ideo.hideUnshownBandLabels(); var t1C = new Date().getTime(); if (config.debug) { console.log('Time in showing bands: ' + (t1C - t0C) + ' ms'); } if (config.orientation === 'vertical') { var chrID; for (i = 0; i < ideo.chromosomesArray.length; i++) { chrID = '#' + ideo.chromosomesArray[i].id; ideo.rotateChromosomeLabels(d3.select(chrID), i); } } } if (config.showChromosomeLabels === true) { ideo.drawChromosomeLabels(ideo.chromosomes); } if (config.brush) { ideo.createBrush(config.brush); } if (config.annotations) { ideo.drawAnnots(config.annotations); } var t1A = new Date().getTime(); if (config.debug) { console.log('Time in drawChromosome: ' + (t1A - t0A) + ' ms'); } var t1 = new Date().getTime(); if (config.debug) { console.log('Time constructing ideogram: ' + (t1 - t0) + ' ms'); } ideo.setOverflowScroll(); if (ideo.onLoadCallback) { ideo.onLoadCallback(); } } catch (e) { // console.log(e); throw e; } } } export { configure, initDrawChromosomes, handleRotateOnClick, setOverflowScroll, onLoad, init }