ideogram
Version:
Chromosome visualization for the web
325 lines (271 loc) • 8.97 kB
JavaScript
/**
* @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 {BedParser} from '../parsers/bed-parser';
import {TsvParser} from '../parsers/tsv-parser';
import {drawHeatmaps, deserializeAnnotsForHeatmap} from './heatmap';
import {inflateThresholds} from './heatmap-lib';
import {inflateHeatmaps} from './heatmap-collinear';
import {
onLoadAnnots, onDrawAnnots, startHideAnnotTooltipTimeout,
onWillShowAnnotTooltip, showAnnotTooltip, onClickAnnot
} from './events';
import {
addAnnotLabel, removeAnnotLabel, fillAnnotLabels, clearAnnotLabels
// fadeOutAnnotLabels
} from './labels';
import {drawAnnots, drawProcessedAnnots} from './draw';
import {getHistogramBars} from './histogram';
import {drawSynteny} from './synteny';
import {
restoreDefaultTracks, setOriginalTrackIndexes, updateDisplayedTracks
} from './filter';
import {processAnnotData} from './process';
import {ExpressionMatrixParser} from '../parsers/expression-matrix-parser';
import {downloadAnnotations} from './download';
function initNumTracksAndBarWidth(ideo, config) {
if (config.annotationTracks) {
ideo.config.numAnnotTracks = config.annotationTracks.length;
} else if (config.annotationsNumTracks) {
ideo.config.numAnnotTracks = config.annotationsNumTracks;
} else {
ideo.config.numAnnotTracks = 1;
}
ideo.config.annotTracksHeight =
config.annotationHeight * config.numAnnotTracks;
if (typeof config.barWidth === 'undefined') {
ideo.config.barWidth = 3;
}
}
function initTooltip(ideo, config) {
if (config.showAnnotTooltip !== false) {
ideo.config.showAnnotTooltip = true;
}
if (config.onWillShowAnnotTooltip) {
ideo.onWillShowAnnotTooltipCallback = config.onWillShowAnnotTooltip;
}
}
function initAnnotLabel(ideo, config) {
if (config.addAnnotLabel !== false) {
ideo.config.addAnnotLabel = true;
}
if (config.onWillAddAnnotLabel) {
ideo.onWillAddAnnotLabelCallback = config.onWillAddAnnotLabel;
}
}
function initAnnotHeight(ideo) {
var config = ideo.config;
var annotHeight;
if (!config.annotationHeight) {
if (config.annotationsLayout === 'heatmap') {
annotHeight = config.chrWidth - 1;
} else {
annotHeight = Math.round(config.chrHeight / 100);
if (annotHeight < 3) annotHeight = 3;
}
ideo.config.annotationHeight = annotHeight;
}
}
/**
* Initializes various annotation settings. Constructor help function.
*/
function initAnnotSettings() {
var ideo = this,
config = ideo.config;
initAnnotHeight(ideo);
if (
config.annotationsPath || config.localAnnotationsPath ||
ideo.annots || config.annotations
) {
initNumTracksAndBarWidth(ideo, config);
} else {
ideo.config.annotTracksHeight = 0;
ideo.config.numAnnotTracks = 0;
}
if (typeof config.annotationsColor === 'undefined') {
ideo.config.annotationsColor = '#F00';
}
if (config.onClickAnnot) {
ideo.onClickAnnotCallback = config.onClickAnnot;
}
initTooltip(ideo, config);
initAnnotLabel(ideo, config);
}
function validateAnnotsUrl(annotsUrl) {
var tmp, extension;
tmp = annotsUrl.split('?')[0].split('.');
extension = tmp[tmp.length - 1];
if (['bed', 'json', 'tsv'].includes(extension) === false) {
extension = extension.toUpperCase();
alert(
'Ideogram.js only supports BED and Ideogram JSON and TSV ' +
'at the moment. ' +
'Sorry, check back soon for ' + extension + ' support!'
);
return;
}
return extension;
}
/** Find redundant chromosomes in raw annotations */
function detectDuplicateChrsInRawAnnots(ideo) {
const seen = {};
const duplicates = [];
const chrs = ideo.rawAnnots.annots.map(annot => annot.chr);
chrs.forEach((chr) => {
if (chr in seen) duplicates.push(chr);
seen[chr] = 1;
});
if (duplicates.length > 0) {
const message =
`Duplicate chromosomes detected.\n` +
`Chromosome list: ${chrs}. Duplicates: ${duplicates}.\n` +
`To fix this, edit your raw annotations JSON data to remove redundant ` +
`chromosomes.`;
throw Error(message);
}
}
function afterRawAnnots() {
var ideo = this,
config = ideo.config;
// Ensure annots are ordered by chromosome
ideo.rawAnnots.annots = ideo.rawAnnots.annots.sort(Ideogram.sortChromosomes);
if (ideo.onLoadAnnotsCallback) {
ideo.onLoadAnnotsCallback();
}
if (
'heatmapThresholds' in config ||
'metadata' in ideo.rawAnnots &&
'heatmapThresholds' in ideo.rawAnnots.metadata
) {
if (config.annotationsLayout === 'heatmap') {
inflateHeatmaps(ideo);
} else if (config.annotationsLayout === 'heatmap-2d') {
ideo.config.heatmapThresholds = inflateThresholds(ideo);
}
}
if (config.heatmaps) {
ideo.deserializeAnnotsForHeatmap(ideo.rawAnnots);
}
detectDuplicateChrsInRawAnnots(ideo);
}
/**
* Converts list of annotation-by-chromosome objects to list of annot objects
*/
function flattenAnnots() {
const ideo = this;
return ideo.annots.reduce((accumulator, annots) => {
return [...accumulator, ...annots.annots];
}, []);
}
/**
* Requests annotations URL via HTTP, sets ideo.rawAnnots for downstream
* processing.
*
* @param annotsUrl Absolute or relative URL for native or BED annotations file
*/
function fetchAnnots(annotsUrl) {
var extension, is2dHeatmap,
ideo = this,
config = ideo.config;
is2dHeatmap = config.annotationsLayout === 'heatmap-2d';
var extension = validateAnnotsUrl(annotsUrl);
if (annotsUrl.slice(0, 4) !== 'http' && !is2dHeatmap && extension !== 'tsv') {
ideo.fetch(annotsUrl)
.then(function(data) {
ideo.rawAnnotsResponse = data; // Preserve truly raw response content
ideo.rawAnnots = data; // Sometimes gets partially processed
ideo.afterRawAnnots();
});
return;
}
extension = (is2dHeatmap ? '' : extension);
ideo.fetch(annotsUrl, 'text')
.then(function(text) {
ideo.rawAnnotsResponse = text;
if (is2dHeatmap) {
var parser = new ExpressionMatrixParser(text, ideo);
parser.setRawAnnots().then(function(d) {
ideo.rawAnnots = d;
ideo.afterRawAnnots();
});
} else {
if (extension === 'tsv') {
ideo.rawAnnots = new TsvParser(text, ideo).rawAnnots;
} else if (extension === 'bed') {
ideo.rawAnnots = new BedParser(text, ideo).rawAnnots;
} else {
ideo.rawAnnots = JSON.parse(text);
}
ideo.afterRawAnnots();
}
});
}
/**
* 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 function applyRankCutoff(annots, cutoff, ideo) {
const rankedAnnots = sortAnnotsByRank(annots, ideo);
// Take the top N ranked genes, where N is `cutoff`
annots = rankedAnnots.slice(0, cutoff);
return annots;
}
export function setAnnotRanks(annots, ideo) {
if ('geneCache' in ideo === false) return annots;
const ranks = ideo.geneCache.interestingNames;
return annots.map(annot => {
if (ranks.includes(annot.name)) {
annot.rank = ranks.indexOf(annot.name) + 1;
} else {
annot.rank = 1E10;
}
return annot;
});
}
export function sortAnnotsByRank(annots, ideo) {
if (ideo) {
annots = setAnnotRanks(annots, ideo);
}
// Ranks annots by popularity
return annots.sort((a, b) => {
// // Search gene is most important, regardless of popularity
// if (a.color === 'red') return -1;
// if (b.color === 'red') return 1;
// Rank 3 is more important than rank 30
return a.rank - b.rank;
});
}
export {
onLoadAnnots, onDrawAnnots, processAnnotData, restoreDefaultTracks,
updateDisplayedTracks, initAnnotSettings, fetchAnnots, drawAnnots,
getHistogramBars, drawHeatmaps, deserializeAnnotsForHeatmap, fillAnnots,
drawProcessedAnnots, drawSynteny, startHideAnnotTooltipTimeout,
showAnnotTooltip, onWillShowAnnotTooltip, setOriginalTrackIndexes,
afterRawAnnots, onClickAnnot, downloadAnnotations, addAnnotLabel,
removeAnnotLabel, fillAnnotLabels, clearAnnotLabels, flattenAnnots
// fadeOutAnnotLabels
};