UNPKG

ideogram

Version:

Chromosome visualization for the web

736 lines (635 loc) 23.8 kB
<!DOCTYPE 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>