ideogram
Version:
Chromosome visualization for the web
736 lines (635 loc) • 23.8 kB
HTML
<html>
<head>
<title>Orthologs | Ideogram</title>
<style>
body {font: 14px Arial; line-height: 19.6px; padding: 0 15px;}
a, a:visited {text-decoration: none;}
a:hover {text-decoration: underline;}
a, a:hover, a:visited, a:active {color: #0366d6;}
label {display: block; margin-bottom: 10px;}
.left-select {position: absolute; left: 140px;}
#status-container {display: inline-block; margin-left: 150px;}
#error-container {color: red;}
#ideogram-container {margin-left: 125px; z-index: -1}
#search-container {
height: 26px;
}
#search {
width: 290px;
height: 20px;
font-size: 14px;
border-radius: 3px;
border: 1px solid #888;
}
#search-button {
height: 23px;
width: 23px;
font-size: 24px;
background: #58F;
color: #FFF;
display: inline-block;
position: relative;
top: -23.5px;
left: 325px;
border-radius: 3px;
text-align: center;
padding-top: 1px;
cursor: pointer;
}
select {height: 22px; font-size: 14px;}
#options-toggle {cursor: pointer;}
#options {
z-index: 9999;
background: white;
border: 1px solid #DDD;
}
#options label {display: inline;}
/* .option {text-decoration: underline;} */
ul {padding: 2px 10px 10px 10px;}
ul li {list-style-type: none;}
</style>
<script type="text/javascript" src="../../dist/js/ideogram.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/homology@0.6.0/dist/homology.min.js"></script>
<!-- <script type="text/javascript" src="https://eweitz.github.io/homology/dist/homology.min.js"></script> -->
<!-- <script type="text/javascript" src="http://localhost/homology/dist/homology.min.js"></script> -->
<link rel="icon" type="image/x-icon" href="img/ideogram_favicon.ico">
</head>
<body>
<h1>Orthologs | Ideogram</h1>
<a href="../">Overview</a> |
<a href="eukaryotes">Previous</a> |
<a href="homology-basic">Next</a> |
<a href="https://github.com/eweitz/ideogram/blob/gh-pages/orthologs.html" target="_blank">Source</a>
<p>
Compare gene locations across organisms. See also <a href="related-genes">Related genes</a>.
</p>
<div style="float: left; width: 350px;">
<label for="search" id="search-container">Search:
<input id="search" autocomplete="off" placeholder="Gene or location"/>
<span id="search-button">
<svg width="12" height="13"><g stroke-width="2" stroke="#FFF" fill="none"><path d="M11.29 11.71l-4-4"/><circle cx="5" cy="5" r="4"/></g></svg>
</span>
</label>
<div style="font-size: 12px; position: relative; top: -7px;">
Examples:
<a href="?genes=MTOR&org=homo-sapiens&org2=mus-musculus&backend=orthodb">MTOR</a> |
<a href="?genes=MTOR,BRCA1&org=homo-sapiens&org2=mus-musculus&backend=orthodb">MTOR, BRCA1</a> |
<a href="?loci=2:150000000,5:20000000&org=homo-sapiens&org2=mus-musculus">2:150000000, 5:200000000</a>
</div>
<label for="org">
Source organism:
<select class="left-select org-select" id="org">
<optgroup label="Primates">
<option>Human (Homo sapiens)</option>
<option>Chimpanzee (Pan troglodytes)</option>
<option>Cynomolgus monkey (Macaca fascicularis)</option>
<option>Rhesus macaque (Macaca mulatta)</option>
</optgroup>
<optgroup label="Rodents">
<option>Mouse (Mus musculus)</option>
<option>Rat (Rattus norvegicus)</option>
</optgroup>
<optgroup label="Other animals">
<option>Dog (Canis lupus familiaris)</option>
<option>Cat (Felis catus)</option>
<!-- <option>Chicken (Gallus gallus)</option> -->
<option>Zebrafish (Danio rerio)</option>
<option>Fly (Drosophila melanogaster)</option>
<option>Mosquito (Anopheles gambiae)</option>
<option>Worm (Caenorhabditis elegans)</option>
</optgroup>
<optgroup label="Plants">
<option>Thale cress (Arabidopsis thaliana)</option>
<option>Corn (Zea mays)</option>
<option>Rice (Oryza sativa)</option>
<option>Tomato (Solanum lycopersicum)</option>
<option>Pepper (Capsicum annuum)</option>
</optgroup>
<!-- <option>Yeast (Saccharomyces cerevisiae)</option> -->
</select>
</label>
<label for="org2">
Target organism:
<select class="left-select org-select" id="org2">
<optgroup label="Primates">
<option>Human (Homo sapiens)</option>
<option>Chimpanzee (Pan troglodytes)</option>
<option>Cynomolgus monkey (Macaca fascicularis)</option>
<option>Rhesus macaque (Macaca mulatta)</option>
</optgroup>
<optgroup label="Rodents">
<option>Mouse (Mus musculus)</option>
<option>Rat (Rattus norvegicus)</option>
</optgroup>
<optgroup label="Other animals">
<option>Dog (Canis lupus familiaris)</option>
<option>Cat (Felis catus)</option>
<!-- <option>Chicken (Gallus gallus)</option> -->
<option>Zebrafish (Danio rerio)</option>
<option>Fly (Drosophila melanogaster)</option>
<option>Mosquito (Anopheles gambiae)</option>
<option>Worm (Caenorhabditis elegans)</option>
</optgroup>
<optgroup label="Plants">
<option>Thale cress (Arabidopsis thaliana)</option>
<option>Corn (Zea mays)</option>
<option>Rice (Oryza sativa)</option>
<option>Tomato (Solanum lycopersicum)</option>
<option>Pepper (Capsicum annuum)</option>
</optgroup>
<!-- <option>Yeast (Saccharomyces cerevisiae)</option> -->
</select>
</label>
<a id="options-toggle">Options</a>
<div id="options" style="display: none">
<ul>
<label class="option" for="chr-height">Chromosome height</label>
<li>
<input type="number" id="chr-height">
</li>
<br/>
<span class="option">Chromosome scale</span>
<li>
<input type="radio" name="chr-scale" id="absolute" value="absolute" checked>
<label for="absolute">Absolute</label>
</li>
<li>
<input type="radio" name="chr-scale" id="relative" value="relative">
<label for="relative">Relative</label>
</li>
<br/>
<span class="option">Orientation</span>
<li>
<input type="radio" name="orientation" id="vertical" value="vertical" checked>
<label for="vertical">Vertical</label>
</li>
<li>
<input type="radio" name="orientation" id="horizontal" value="horizontal">
<label for="horizontal">Horizontal</label>
</li>
<br/>
<span class="option">Orthology backend</span>
<li>
<input type="radio" name="backend" id="orthodb" value="orthodb" checked>
<label for="orthodb">OrthoDB</label>
</li>
<li>
<input type="radio" name="backend" id="oma" value="oma">
<label for="oma">OMA Browser</label>
</li>
</ul>
</div>
</div>
<div id="ideogram-container"></div>
<div id="status-container"></div>
<script type="text/javascript">
var prevState = {};
var orthologs = []; // arrays of [loci1, loci2]
function shouldUpdateState() {
return JSON.stringify(urlParams) !== JSON.stringify(prevState);
}
function updateGenesParams(geneNames) {
if (typeof genes !== 'undefined') {
if (typeof geneNames === 'undefined') {
geneNames = genes;
}
delete urlParams['loci']
urlParams['genes'] = geneNames;
}
updateUrl();
}
function updateLociParams(loci) {
delete urlParams['genes']
urlParams['loci'] = loci
updateUrl()
}
// Process text input for the "Gene" field.
async function handleSearch(event) {
// Ignore non-"Enter" keyups
if (event.type === 'keyup' && event.keyCode !== 13) return;
var searchInput = event.target.value.trim();
if (isGenomicLocation(searchInput)) {
updateLociParams(searchInput)
} else {
// Ignore when input value is unchanged
if (typeof geneNames !== 'undefined' && urlParams['genes'] === geneNames) return;
updateGenesParams(searchInput);
}
createIdeogram();
}
// Process selections in "Organism" drop-down menus
async function handleOrganism(event) {
var organism =
event.target.value.split('(')[1].split(')')[0]
.replace(/ /g, '-').toLowerCase();
urlParams[event.target.id] = organism;
updateUrl()
createIdeogram();
}
function splitList(csvList) {
return csvList
.replace(/%20/g, '')
.replace(/^\s+|\s+$/g, '')
.split(',');
}
/** Returns [geneList, genePropList] */
function parseGenesParam(genesParam) {
let geneList = []
let genePropList = []
splitList(genesParam).map(rawValue => {
const splitValue = rawValue.split('!')
geneList.push(splitValue[0])
if (splitValue.length > 1) {
genePropList.push(splitValue[1])
}
});
return [geneList, genePropList]
}
/**
* Determines if string value has the format of a genomic location
*/
function isGenomicLocation(value) {
return value.includes(':')
}
// Process selections in "Orthology backend" drop-down menu
async function handleBackend(event) {
var backend = document.querySelector('input[name=backend]:checked').id;
urlParams['backend'] = backend;
updateGenesParams();
createIdeogram();
}
// Process selections in "Orthology source" radio inputs
async function handleBackend(event) {
var backend = document.querySelector('input[name=backend]:checked').id;
urlParams['backend'] = backend;
updateGenesParams();
createIdeogram();
}
// Process selections in "Orientation" radio inputs
async function handleOrientation(event) {
var orientation = document.querySelector('input[name=orientation]:checked').id;
urlParams['orientation'] = orientation;
updateGenesParams();
createIdeogram();
}
// Process selections in "Orthology backend" drop-down menu
async function handleChromosomeHeight(event) {
var chrHeight = document.querySelector('#chr-height').value;
urlParams['chr-height'] = chrHeight;
updateGenesParams();
createIdeogram();
}
// Process selections in "Orientation" radio inputs
async function handleChromosomeScale(event) {
var scale = document.querySelector('input[name=chr-scale]:checked').id;
urlParams['chr-scale'] = scale;
updateGenesParams();
createIdeogram();
}
// Record app state in URL
function updateUrl() {
var params = Object.keys(urlParams).map(key => {
return key + '=' + urlParams[key];
}).join('&');
history.pushState(null, null, '?' + params);
}
// Set the 'Organism 1' or 'Organism 2' menu using a scientific organism name
function updateOrganismMenu(orgParam, orgValue) {
var selectedOrg = document.querySelector('#' + orgParam + ' option:checked').text;
selectedOrg = selectedOrg.split('(')[1].split(')')[0].toLowerCase();
if (orgValue !== selectedOrg) {
document.querySelectorAll('#' + orgParam + ' option').forEach(option => {
if (option.text.toLowerCase().includes(orgValue)) {
document.querySelector('#' + orgParam).value = option.text;
}
});
}
}
function parseUrlParams() {
var rawParams = document.location.search;
urlParams = {};
var param, key, value;
if (rawParams !== '') {
rawParams = rawParams.split('?')[1].split('&');
rawParams.forEach(rawParam => {
param = rawParam.split('=');
key = param[0];
value = param[1];
urlParams[key] = value;
});
}
}
/**
* Parses orthologs specified via "loci" URL parameter
*
* Examples:
*
* Single ortholog:
* https://eweitz.github.io/ideogram/orthologs?loci=1:11106531-11262557!name:MTOR,4:148448582-148557685!name:Mtor&org=homo-sapiens&org2=mus-musculus
*
* Single ortholog with custom color and width:
* https://eweitz.github.io/ideogram/orthologs?loci=6:40611225-77560424,1:111464150-67465953!!color:pink&org=canis-lupus-familiaris&org2=homo-sapiens
*
* Multiple orthologs:
* https://eweitz.github.io/ideogram/orthologs?loci=1:11106531-11262557!name:MTOR,4:148448582-148557685!name:Mtor;10:11106531-11262557!name:FOO,X:148448582-148557685!name:Foo&org=homo-sapiens&org2=mus-musculus
*
* Synteny block:
* https://eweitz.github.io/ideogram/orthologs?loci=6:40611225-77560424,1:111464150-67465953!!color:pink&org=canis-lupus-familiaris&org2=homo-sapiens
*/
function parseLociParam(lociParam) {
return lociParam.split(';').map(ortholog => {
let region = {}
// Two exclamation points ("!!") demarcate a pair of ranges from its props
let [rawRanges, regionProps] = ortholog.split('!!')
ranges = rawRanges.split(',').map(loc => {
// One exclamation point ("!") demarcates one range from its props
const entries = loc.split('!')
locus = {location: entries[0].trim()}
if (entries.length > 1) {
entries.splice(1).map(entry => {
const [key, value] = entry.split(':').map(v => v.trim())
locus[key] = value
})
}
if ('name' in locus === false) locus['name'] = ''
return locus
})
if (regionProps) {
regionProps.split(',').map(prop => {
const [key, value] = prop.split(':').map(v => v.trim())
region[key] = value
})
// TODO: Refactor orthologs code to enable specifying arbitrary props
if ('color' in region) ranges.push(region.color)
if ('width' in region) ranges.push(region.width)
}
return ranges
})
}
async function processUrl() {
var geneList, genePropList, hasGenes, hasLoci, loci, lociList, numAnnots;
document.querySelector('#ideogram-container').innerHTML = '';
document.querySelector('#status-container').innerHTML = 'Loading...';
parseUrlParams();
// Replace 'gene' with 'genes'
if ('gene' in urlParams) {
urlParams['genes'] = urlParams['gene'];
delete urlParams['gene'];
}
hasGenes = 'genes' in urlParams;
hasLoci = 'loci' in urlParams;
// Set default parameters if none are provided.
if ('org' in urlParams === false) {
if (!hasLoci) {
urlParams['genes'] = 'MTOR'
hasGenes = true
}
urlParams['org'] = 'homo-sapiens';
urlParams['org2'] = 'mus-musculus';
urlParams['backend'] = 'orthodb';
// urlParams['loci'] = '1:11106531-11262557,4:148448582-148557685';
}
// If "org2" is omitted, set it to the value of "org"
if ('org2' in urlParams === false) {
org2 = urlParams['org'];
urlParams['org2'] = org2;
}
org1 = urlParams['org'].replace(/-/g, ' ');
org2 = urlParams['org2'].replace(/-/g, ' ');
showBandLabels = 'band-labels' in urlParams;
updateOrganismMenu('org', org1);
updateOrganismMenu('org2', org2);
if (hasGenes) {
[geneList, genePropList] = parseGenesParam(urlParams['genes']);
genes = geneList.toString()
document.querySelector('#search').value = genes
updateGenesParams(genes);
} else {
loci = urlParams['loci'].replace(/%20/g, ' ')
document.querySelector('#search').value = loci
}
if ('backend' in urlParams === false) urlParams['backend'] = 'orthodb';
backend = urlParams['backend'];
document.querySelector('input[name=backend]#' + backend).checked = true;
if ('orientation' in urlParams === false) urlParams['orientation'] = 'vertical';
orientation = urlParams['orientation'];
orientation = urlParams['orientation'];
document.querySelector('input[name=orientation]#' + orientation).checked = true;
if ('chr-scale' in urlParams === false) urlParams['chr-scale'] = 'absolute';
scale = urlParams['chr-scale'];
document.querySelector('input[name=chr-scale]#' + scale).checked = true;
if (hasGenes) {
numAnnots = geneList.length;
} else {
lociList = parseLociParam(loci);
numAnnots = lociList.length;
}
if ('chr-height' in urlParams === false) {
urlParams['chr-height'] = getChromosomeHeight(org1, numAnnots);
}
chrHeight = urlParams['chr-height'];
document.querySelector('#chr-height').value = chrHeight;
if (shouldUpdateState()) {
try {
if (hasGenes) {
const t0 = performance.now()
rawOrthologs = await fetchOrthologs(geneList, org1, [org2], backend);
const t1 = performance.now()
const ms = Math.round(t1 - t0)
console.log(
`Orthologs fetched for ${geneList.length} genes ` +
`in ${ms} ms`
)
orthologs = rawOrthologs.map((rawOrtholog, i) => {
if (genePropList.length === 0) return rawOrtholog
const propVals = genePropList[i].split(',').map(prop => {
return prop.split(':')[1]
})
return rawOrtholog.concat(propVals)
})
} else {
orthologs = lociList;
}
} catch (error) {
document.querySelector('#status-container').innerHTML =
`<span id="error-container">${error}</span>`;
throw error;
}
};
prevState = Object.assign({}, urlParams);
}
function parseSyntenicRegion(ortholog) {
var [loci1, loci2, color, width] = ortholog;
var [loci1Chr, loci1Range] = loci1.location.split(':');
var [loci2Chr, loci2Range] = loci2.location.split(':');
var [loci1Start, loci1Stop] = loci1Range.split('-');
var [loci2Start, loci2Stop] = loci2Range.split('-');
if (typeof loci1Stop === 'undefined') loci1Stop = loci1Start
if (typeof loci2Stop === 'undefined') loci2Stop = loci2Start
if ('gene' in loci1) {
loci1.name = loci1.gene
delete loci1.gene
}
if ('gene' in loci2) {
loci2.name = loci2.gene
delete loci2.gene
}
r1 = {
chr: loci1Chr,
start: loci1Start,
stop: loci1Stop,
name: loci1.name
};
r2 = {
chr: loci2Chr,
start: loci2Start,
stop: loci2Stop,
name: loci2.name
};
let region = {r1, r2};
if (typeof color !== 'undefined') region.color = color
if (typeof width !== 'undefined') region.width = width
return region;
}
function drawSynteny() {
var chrs, org1Taxid, org2Taxid, ideoContainer, i, rawRange1, rawRange2,
syntenicRegions = [];
document.querySelector('#status-container').innerHTML = '';
chrs = ideogram.chromosomes;
for (i = 0; i < orthologs.length; i++) {
const region = parseSyntenicRegion(orthologs[i]);
syntenicRegions.push(region);
}
ideogram.drawSynteny(syntenicRegions);
// If showing multiple orthologs, then adjust ideogram position
// to account for whole-genome rendering
ideoContainer = document.querySelector('#ideogram-container');
if (orthologs.length > 1) {
if (orientation === 'vertical') {
width = 700
left = 200
topPx = -110
} else {
width = 2000
left = -350
topPx = 150
}
document.querySelector('#_ideogram').setAttribute('width', width);
ideoContainer.setAttribute(
'style',
'position: relative; top: ' + topPx + 'px; left: ' + left + 'px; width: ' + width + 'px;'
);
} else {
ideoContainer.setAttribute('style', 'position: relative; top: -60px;');
}
}
function onIdeogramLoad() {
drawSynteny();
}
function getChromosomeHeight(organism, numAnnots) {
var chrHeight;
if ('chr-height' in urlParams) {
return urlParams['chr-height'];
}
if (numAnnots < 2) {
return orientation === 'vertical' ? 600 : 700
}
if (
(
organism.includes('canis lupus familiaris') ||
organism.includes('danio rerio')
) &&
orientation === 'horizontal' &&
scale === 'relative'
) {
chrHeight = 65;
} else if (orientation === 'horizontal') {
chrHeight = 95;
} else {
// vertical
chrHeight = 44;
}
return chrHeight;
}
async function createIdeogram() {
await processUrl();
if (document.querySelector('#error-container') !== null) {
return;
}
chrHeight = getChromosomeHeight(org1);
var config = {
container: '#ideogram-container',
showBandLabels: showBandLabels,
rotatable: false,
chrWidth: 10,
onLoad: onIdeogramLoad,
orientation: orientation,
chrHeight: chrHeight,
chrLabelSize: 11
};
if (org1 !== org2) {
// Orthology
config.organism = [org1, org2];
} else {
// Paralogy
config.organism = org1;
config.chromosomesConfig = [loci1Chr, loci2Chr];
}
if (orthologs.length > 1) {
// For (likely) multiple chromosomes in each genome
organism = config.organism;
Object.assign(config, {
chromosomeScale: scale,
geometry: 'collinear',
chrMargin: 6
});
} else {
// For one chromosome in each genome
region = parseSyntenicRegion(orthologs[0]);
chromosomesConfig = {}
chromosomesConfig[org1] = [region.r1.chr];
chromosomesConfig[org2] = [region.r2.chr];
Object.assign(config, {
chromosomes: chromosomesConfig,
fullChromosomeLabels: true,
perspective: 'comparative'
});
}
delete chrBands;
ideogram = new Ideogram(config);
}
document.querySelector('#search-button').addEventListener('click', handleSearch);
document.querySelector('#search').addEventListener('blur', handleSearch);
document.querySelector('#search').addEventListener('keyup', handleSearch);
document.querySelectorAll('.org-select').forEach(select => {
select.addEventListener('change', handleOrganism);
});
document.querySelector('#options-toggle').addEventListener('click', event => {
var options = document.querySelector('#options');
if (options.style.display === 'none') {
options.style.display = '';
} else {
options.style.display = 'none';
}
});
document.querySelector('#chr-height').addEventListener('change', event => {
handleChromosomeHeight(event)
})
radioButtons = document.querySelectorAll('input[type=radio]');
radioButtons.forEach(function(radioButton) {
radioButton.addEventListener('click', function(event) {
var name = event.target.name;
if (name === 'backend') {
handleBackend(event);
} else if (name === 'orientation') {
handleOrientation(event);
} else if (name === 'chr-scale') {
handleChromosomeScale(event);
}
});
});
createIdeogram();
</script>
</body>
</html>