UNPKG

ideogram

Version:

Chromosome visualization with D3.js

496 lines (411 loc) 13.3 kB
/** * @fileoverview A collection of Ideogram methods that don't fit elsewhere. */ import * as d3selection from 'd3-selection'; import {ModelAdapter} from './model-adapter'; import {Chromosome} from './views/chromosome'; var d3 = Object.assign({}, d3selection); /** * Is the assembly in this.config an NCBI Assembly accession? * * @returns {boolean} */ function assemblyIsAccession() { return ( 'assembly' in this.config && /(GCF_|GCA_)/.test(this.config.assembly) ); } /** * Returns directory used to fetch data for bands and annotations * * This simplifies ideogram configuration. By default, the dataDir is * set to an external CDN unless we're serving from the local host, in * which case dataDir is deduced from the "src" attribute of the ideogram * script loaded in the document. * * @returns {String} */ function getDataDir() { var scripts = document.scripts, host = location.host.split(':')[0], version = Ideogram.version, script, tmp, protocol, dataDir; if (host !== 'localhost' && host !== '127.0.0.1') { return ( 'https://unpkg.com/ideogram@' + version + '/dist/data/bands/native/' ); } for (var i = 0; i < scripts.length; i++) { script = scripts[i]; if ( 'src' in script && /ideogram/.test(script.src.split('/').slice(-1)) ) { tmp = script.src.split('//'); protocol = tmp[0]; tmp = '/' + tmp[1].split('/').slice(0,-2).join('/'); dataDir = protocol + '//' + tmp + '/data/bands/native/'; return dataDir; } } return '../data/bands/native/'; } function getChromosomePixels(chr) { var bands, chrHeight, pxStop, hasBands, maxLength, band, cs, csLength, width, chrLength; bands = chr.bands; chrHeight = this.config.chrHeight; pxStop = 0; cs = this.coordinateSystem; hasBands = (typeof bands !== 'undefined'); maxLength = this.maxLength; chrLength = chr.length; if (hasBands) { for (var i = 0; i < bands.length; i++) { band = bands[i]; csLength = band[cs].stop - band[cs].start; // If ideogram is rotated (and thus showing only one chromosome), // then set its width independent of the longest chromosome in this // genome. if (this._layout._isRotated) { width = chrHeight * csLength / chrLength; } else { width = chrHeight * chr.length / maxLength[cs] * csLength / chrLength; } bands[i].px = { start: pxStop, stop: pxStop + width, width: width }; pxStop = bands[i].px.stop; if (hasBands && band.stain === 'acen' && band.name[0] === 'p') { chr.pcenIndex = i; } } } else { pxStop = chrHeight * chr.length / maxLength[cs]; } chr.width = pxStop; chr.scale = {}; // TODO: // // A chromosome-level scale property is likely // nonsensical for any chromosomes that have cytogenetic band data. // Different bands tend to have ratios between number of base pairs // and physical length. // // However, a chromosome-level scale property is likely // necessary for chromosomes that do not have band data. // // This needs further review. if (this.config.multiorganism === true) { chr.scale.bp = 1; // chr.scale.bp = band.iscn.stop / band.bp.stop; chr.scale.iscn = chrHeight * chrLength / maxLength.bp; } else { chr.scale.bp = chrHeight / maxLength.bp; if (hasBands) { chr.scale.iscn = chrHeight / maxLength.iscn; } } chr.bands = bands; return chr; } /** * Generates a model object for each chromosome containing information on * its name, DOM ID, length in base pairs or ISCN coordinates, cytogenetic * bands, centromere position, etc. */ function getChromosomeModel(bands, chrName, taxid, chrIndex) { var chr = {}, cs, hasBands; cs = this.coordinateSystem; hasBands = (typeof bands !== 'undefined'); if (hasBands) { chr.name = chrName; chr.length = bands[bands.length - 1][cs].stop; chr.type = 'nuclear'; } else { chr = chrName; } chr.chrIndex = chrIndex; chr.id = 'chr' + chr.name + '-' + taxid; if (this.config.fullChromosomeLabels === true) { var orgName = this.organisms[taxid].scientificNameAbbr; chr.name = orgName + ' chr' + chr.name; } chr.bands = bands; chr = this.getChromosomePixels(chr); chr.centromerePosition = ''; if ( hasBands && bands[0].name[0] === 'p' && bands[1].name[0] === 'q' && bands[0].bp.stop - bands[0].bp.start < 2E6 ) { // As with almost all mouse chromosome, chimpanzee chr22 chr.centromerePosition = 'telocentric'; } if (hasBands && chr.bands.length === 1) { // Encountered when processing an assembly that has chromosomes with // centromere data, but this chromosome does not. // Example: chromosome F1 in Felis catus. delete chr.bands; } return chr; } /** * Draws labels for each chromosome, e.g. "1", "2", "X". * If ideogram configuration has 'fullChromosomeLabels: True', * then labels includes name of taxon, which can help when * depicting orthologs. */ function drawChromosomeLabels() { var ideo = this; var chromosomeLabelClass = ideo._layout.getChromosomeLabelClass(); var chrSetLabelXPosition = ideo._layout.getChromosomeSetLabelXPosition(); var chrSetLabelTranslate = ideo._layout.getChromosomeSetLabelTranslate(); // Append chromosome set's labels d3.selectAll(ideo.selector + ' .chromosome-set-container') .insert('text', ':first-child') .data(ideo.chromosomesArray) .attr('class', chromosomeLabelClass) .attr('transform', chrSetLabelTranslate) .attr('x', chrSetLabelXPosition) .attr('y', function(d, i) { return ideo._layout.getChromosomeSetLabelYPosition(i); }) .attr('text-anchor', ideo._layout.getChromosomeSetLabelAnchor()) .each(function(d, i) { // Get label lines var lines; if (d.name.indexOf(' ') === -1) { lines = [d.name]; } else { lines = d.name.match(/^(.*)\s+([^\s]+)$/).slice(1).reverse(); } if ( 'sex' in ideo.config && ideo.config.ploidy === 2 && i === ideo.sexChromosomes.index ) { if (ideo.config.sex === 'male') { lines = ['XY']; } else { lines = ['XX']; } } // Render label lines d3.select(this).selectAll('tspan') .data(lines) .enter() .append('tspan') .attr('dy', function(d, i) { return i * -1.2 + 'em'; }) .attr('x', ideo._layout.getChromosomeSetLabelXPosition()) .attr('class', function(a, i) { var fullLabels = ideo.config.fullChromosomeLabels; return i === 1 && fullLabels ? 'italic' : null; }) .text(String); }); var setLabelTranslate = ideo._layout.getChromosomeSetLabelTranslate(); // Append chromosomes labels d3.selectAll(ideo.selector + ' .chromosome-set-container') .each(function(a, chrSetIndex) { d3.select(this).selectAll('.chromosome') .append('text') .attr('class', 'chrLabel') .attr('transform', setLabelTranslate) .attr('x', function(d, i) { return ideo._layout.getChromosomeLabelXPosition(i); }) .attr('y', function(d, i) { return ideo._layout.getChromosomeLabelYPosition(i); }) .text(function(d, chrIndex) { return ideo._ploidy.getAncestor(chrSetIndex, chrIndex); }) .attr('text-anchor', 'middle'); }); } /** * Rotates chromosome labels by 90 degrees, e.g. upon clicking a chromosome to focus. */ function rotateChromosomeLabels(chr, chrIndex, orientation, scale) { var chrMargin, chrWidth, ideo, x, y, numAnnotTracks, scaleSvg, tracksHeight, chrMargin2; chrWidth = this.config.chrWidth; chrMargin = this.config.chrMargin * chrIndex; numAnnotTracks = this.config.numAnnotTracks; ideo = this; if ( typeof (scale) !== 'undefined' && scale.hasOwnProperty('x') && !(scale.x === 1 && scale.y === 1) ) { scaleSvg = 'scale(' + scale.x + ',' + scale.y + ')'; x = -6; y = (scale === '' ? -16 : -14); } else { x = -8; y = -16; scale = {x: 1, y: 1}; scaleSvg = ''; } if (orientation === 'vertical' || orientation === '') { var ci = chrIndex - 1; if (numAnnotTracks > 1 || orientation === '') { ci -= 1; } chrMargin2 = -4; if (ideo.config.showBandLabels === true) { chrMargin2 = ideo.config.chrMargin + chrWidth + 26; } chrMargin = ideo.config.chrMargin * ci; if (numAnnotTracks > 1 === false) { chrMargin += 1; } y = chrMargin + chrMargin2; chr.selectAll('text.chrLabel') .attr('transform', scaleSvg) .selectAll('tspan') .attr('x', x) .attr('y', y); } else { chrIndex -= 1; chrMargin2 = -chrWidth - 2; if (ideo.config.showBandLabels === true) { chrMargin2 = ideo.config.chrMargin + 8; } tracksHeight = ideo.config.annotTracksHeight; if (ideo.config.annotationsLayout !== 'overlay') { tracksHeight *= 2; } chrMargin = ideo.config.chrMargin * chrIndex; x = -(chrMargin + chrMargin2) + 3 + tracksHeight; x /= scale.x; chr.selectAll('text.chrLabel') .attr('transform', 'rotate(-90)' + scaleSvg) .selectAll('tspan') .attr('x', x) .attr('y', y); } } /** * Rounds an SVG coordinates to two decimal places * * @param coord SVG coordinate, e.g. 42.1234567890 * @returns {number} Rounded value, e.g. 42.12 */ function round(coord) { // Per http://stackoverflow.com/a/9453447, below method is fastest return Math.round(coord * 100) / 100; } /** * Adds a copy of a chromosome (i.e. a homologous chromosome, homolog) to DOM * * @param chrModel * @param chrIndex * @param homologIndex * @param container */ function appendHomolog(chrModel, chrIndex, homologIndex, container) { var homologOffset, chromosome, shape, defs, adapter; defs = d3.select(this.selector + ' defs'); // Get chromosome model adapter class adapter = ModelAdapter.getInstance(chrModel); // How far this copy of the chromosome is from another homologOffset = homologIndex * this.config.chrMargin; // Append chromosome's container chromosome = container .append('g') .attr('id', chrModel.id) .attr('class', 'chromosome ' + adapter.getCssClass()) .attr('transform', 'translate(0, ' + homologOffset + ')'); // Render chromosome shape = Chromosome.getInstance(adapter, this.config, this) .render(chromosome, chrIndex, homologIndex); d3.select('#' + chrModel.id + '-chromosome-set-clippath').remove(); defs.append('clipPath') .attr('id', chrModel.id + '-chromosome-set-clippath') .selectAll('path') .data(shape) .enter() .append('path') .attr('d', function (d) { return d.path; }) .attr('class', function (d) { return d.class; }); } /** * Renders all the bands and outlining boundaries of a chromosome. */ function drawChromosome(chrModel) { var chrIndex, container, numChrsInSet, transform, homologIndex, chrSetSelector; chrIndex = chrModel.chrIndex; transform = this._layout.getChromosomeSetTranslate(chrIndex); chrSetSelector = this.selector + ' #' + chrModel.id + '-chromosome-set'; d3.selectAll(chrSetSelector + ' g').remove(); container = d3.select(chrSetSelector); if (container.nodes().length === 0) { // Append chromosome set container container = d3.select(this.selector) .append('g') .attr('class', 'chromosome-set-container') .attr('data-set-number', chrIndex) .attr('transform', transform) .attr('id', chrModel.id + '-chromosome-set'); } if ( 'sex' in this.config && this.config.ploidy === 2 && this.sexChromosomes.index === chrIndex ) { this.drawSexChromosomes(container, chrIndex); return; } numChrsInSet = 1; if (this.config.ploidy > 1) { numChrsInSet = this._ploidy.getChromosomesNumber(chrIndex); } for (homologIndex = 0; homologIndex < numChrsInSet; homologIndex++) { this.appendHomolog(chrModel, chrIndex, homologIndex, container); } } /** * Rotates a chromosome 90 degrees and shows or hides all other chromosomes * Useful for focusing or defocusing a particular chromosome */ function rotateAndToggleDisplay(chrElement) { var chrName, chrModel, chrIndex, chrSetIndex; // Do nothing if taxid not defined. But it should be defined. // To fix that bug we should have a way to find chromosome set number. if (!this.config.taxid) { return; } chrName = chrElement.id.split('-')[0].replace('chr', ''); chrModel = this.chromosomes[this.config.taxid][chrName]; chrIndex = chrModel.chrIndex; chrSetIndex = Number(d3.select(chrElement.parentNode).attr('data-set-number')); this._layout.rotate(chrSetIndex, chrIndex, chrElement); } function onDidRotate(chrModel) { call(this.onDidRotateCallback, chrModel); } /** * Get ideogram SVG container */ function getSvg() { return d3.select(this.selector).node(); } export { assemblyIsAccession, getDataDir, getChromosomeModel, getChromosomePixels, drawChromosomeLabels, rotateChromosomeLabels, round, appendHomolog, drawChromosome, rotateAndToggleDisplay, onDidRotate, getSvg, Object };