UNPKG

ideogram

Version:

Chromosome visualization for the web

792 lines (676 loc) 26.8 kB
<!DOCTYPE html> <html> <head> <title>Differential expression of genes | Ideogram</title> <link rel="icon" type="image/x-icon" href="img/ideogram_favicon.ico"> <link href="https://cdn.jsdelivr.net/npm/nouislider@14.1.0/distribute/nouislider.css" rel="stylesheet"> <link href="https://cdn.datatables.net/1.10.20/css/jquery.dataTables.min.css" rel="stylesheet"> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/buttons/1.6.1/css/buttons.dataTables.min.css"> <!-- <link href="https://cdn.datatables.net/responsive/2.2.3/css/responsive.dataTables.min.css" rel="stylesheet"> --> <script type="text/javascript" src="../../dist/js/ideogram.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/datatables.net@1.10.20/js/jquery.dataTables.min.js"></script> <script type="text/javascript" src="https://cdn.datatables.net/buttons/1.6.1/js/dataTables.buttons.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jszip@3.2.2/dist/jszip.min.js"></script> <script type="text/javascript" src="https://cdn.datatables.net/buttons/1.6.1/js/buttons.html5.min.js"></script> <!-- <script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.3/js/dataTables.responsive.min.js"></script> --> <!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/c3@0.7.11"></script> --> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/nouislider@14.1.0"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/wnumb@1.2.0/wNumb.min.js"></script> <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;} </style> <style> select {font: 14px Arial;} ul { list-style: none; padding-left: 10px; } #summary {margin-bottom: 20px;} #ideogram-container {margin-top: -20px;} li {padding: 2px 0;} li.hidden {display: none;} input {margin-right: 5px;} #gene-type {padding-left: 30px;} #left-content { float: left; padding-right: 30px; border-right: 1px solid #EEE; margin-right: 15px; width: 275px; } .category-facet li { background: #FFF; } .comparison-dropdown { max-width: 285px; } /* Vertically shortens the 2 in log2(fold change)*/ sub { font-size: 0.83em; vertical-align: sub; line-height: 0; } #table-container { position: absolute; left: 324px; margin-left: 5px; border-top: 1px solid #EEE; padding-top: 10px; } .dataTables_length {margin-left: 10px;} table.dataTable tbody td {padding: 2px 10px;} table.dataTable thead th {padding: 8px 18px 4px 18px;} .noUi-value-horizontal {transform: translate(-50%, 70%);} .noUi-horizontal {height: 12px;} .noUi-horizontal .noUi-handle {width: 20px; right: -10px; height: 22px; top: -6px;} .noUi-handle:before, .noUi-handle:before {left: 7px; top: 3px;} .noUi-handle:after, .noUi-handle:after {left: 10px; top: 3px;} .noUi-marker-horizontal.noUi-marker-large {height: 12px;} .noUi-marker .noUi-marker-horizontal .noUi-marker-normal {height: 3px;} .noUi-horizontal .noUi-tooltip {bottom: 115%;} .noUi-tooltip {padding-top: 2px; padding-bottom: 0px; font-size: 12px;} .inactive .noUi-connect {background: #AAA;} .inactive .noUi-tooltip {color: #AAA;} #table-container .dt-button { padding: 2px 6px; border-radius: 4px; margin-left: 20px; top: -1px; } #table-container .paginate_button {padding: 3px 9px;} #table-container .dataTables_info {padding-top: 0.3em;} #table-container .dataTables_paginate {margin-right: 10px;} #table-container table.dataTable thead th {padding-top: 4px;} </style> </head> <body> <h1>Differential expression of genes | Ideogram</h1> <a href="../">Overview</a> | <a href="related-genes">Previous</a> | <a href="geometry-collinear">Next</a> | <a href="https://github.com/eweitz/ideogram/blob/gh-pages/differential-expression.html" target="_blank">Source</a> <br/><br/> <div id="summary"></div> <div id="left-content"> <div id="facets" style="z-index: 9999;"></div> </div> <div id="content"> <div id="ideogram-container"></div> <div id="table-container"></div> </div> <script type="text/javascript"> const d3 = Ideogram.d3; const metricLabels = { 'log2fc': 'log<sub>2</sub>(fold change)', 'adj-p-value': 'Adjusted <i>p</i>-value' } /** * Provides "noUiSlider" configuration object for the given metric * * noUiSlider docs: https://refreshless.com/nouislider/ **/ function getSliderConfig(metric) { const props = { 'adj-p-value': { range: { 'min': [0, 0.001], '50%': [0.05, 0.01], 'max': 1 }, start: [0, 0.05], sliderDecimals: 3, pipDecimals: 3, connect: true, values: [0, 25, 50, 73.5, 100], density: 4 }, 'log2fc': { range: { 'min': [-1.5], 'max': 1.5 }, start: [-1.5, -0.26, 0.26, 1.5], sliderDecimals: 2, pipDecimals: 1, connect: [false, true, false, true, false], values: [0, 16.7, 34.4, 50, 66.7, 84.4, 100], density: 3 } }; const pipDecimals = wNumb({ decimals: props[metric].pipDecimals }); const sliderDecimals = wNumb({ decimals: props[metric].sliderDecimals }); return { range: props[metric].range, // Handles start at ... start: props[metric].start, connect: props[metric].connect, // Move handle on tap, bars are draggable behaviour: 'tap-drag', tooltips: true, format: sliderDecimals, // Show a scale with the slider pips: { mode: 'positions', values: props[metric].values, stepped: true, density: props[metric].density, format: pipDecimals } } } /** * Adds slider widget for a numerical metric **/ function writeSliderContainer(metric, i, comparisonId) { const metricId = metric.replace(/\./g, ''); const sliderId = metricId + '-' + comparisonId; const metricLabel = metricLabels[metric]; document.querySelector(`#${comparisonId}`).innerHTML += `<div style="margin-bottom: 115px; margin-left: 15px;"> <div style="margin-left: -15px; z-index: 2;"> <input type="checkbox" class="slider-checkbox" id="slider-checkbox-${sliderId}"/> <label for="slider-checkbox-${sliderId}">${metricLabel}</label> </div> <div id="${sliderId}" class="ideogramSlider" style="top: 40px"></div> </div>`; } /** * Adds dropdown menus for available comparison groups **/ function writeComparisonDropdowns() { let dropdown1 = []; let dropdown2 = []; const metadata = ideogram.rawAnnots.metadata const groups = metadata.dge.groups; const group1Index = groups.indexOf(window.group1); const group2Index = groups.indexOf(window.group2); groups.forEach((group, i) => { let otherGroup = i < groups.length - 1 ? groups[i + 1] : groups[0]; let s1 = (i === group1Index) ? 'selected' : ''; let s2 = (groups.indexOf(otherGroup) === group2Index) ? 'selected' : ''; let groupLabel = metadata.labels[group]; let otherGroupLabel = metadata.labels[otherGroup]; dropdown1.push(`<option id="option-${group}" ${s1}>${groupLabel}</option>`); dropdown2.push(`<option id="option-${otherGroup}" ${s2}>${otherGroupLabel}</option>`); }); dropdown1 = `<select id="comparison-dropdown-1" class="comparison-dropdown">${dropdown1.join()}</select>`; dropdown2 = `<select id="comparison-dropdown-2" class="comparison-dropdown">${dropdown2.join()}</select>`; document.querySelector('.comparison-dropdowns > div').innerHTML += dropdown1 + ' <div style="padding: 4px 2px;">compared to</div> ' + dropdown2; } /** * Adds slider widgets for all numerical metrics in this comparison */ function writeSliders(comparison, labels, metrics) { const comparisonLabel = labels[`log2fc-${comparison}`].replace('Log2fc_', ''); const comparisonId = comparison.toLowerCase().replace(/[()\s]/g, ''); let prevDiv = document.querySelector('.comparison-dropdowns > div'); if (prevDiv !== null) prevDiv.innerHTML = ''; // clear any previous content document.querySelector('#facets').innerHTML += ` <div class="comparison-dropdowns" id="${comparisonId}"> <div style="margin-bottom: 30px;"></div> <div>`; writeComparisonDropdowns(); metrics.forEach((metric, i) => writeSliderContainer(metric, i, comparisonId)); } function getSummary(metadata) { const intro = `Genomic distribution of differentially expressed <span id="organismName" style="font-style: italic">${metadata.organism}</span> genes, viewed with <a href="https://github.com/eweitz/ideogram">Ideogram.js</a>.`; if ('annots-url' in urlParams) { return intro; } else { const acc = window.accession; return `${intro} NASA GeneLab <a href="https://genelab-data.ndc.nasa.gov/genelab/accession/${acc}/" target="_blank">${acc}</a>, "${window.studyTitle}".`; } } /** * Adds facets, i.e. groups of filters, to query Ideogram annotations **/ function writeFacets() { const metadata = ideogram.rawAnnots.metadata; const labels = metadata.labels; let filters = []; document.querySelector('#facets').innerHTML = ''; document.querySelector('#summary').innerHTML = getSummary(metadata); // Build category facets for (const key in labels) { if (Array.isArray(labels[key])) { var editedKey = key[0].toUpperCase() + key.slice(1).replace(/-/g, ' ') filters.push(editedKey); labels[key].forEach((label, i) => { const editedLabel = label[0].toUpperCase() + label.slice(1).replace(/-/g, ' ') const lowerLabel = label.toLowerCase(); const filterID = `filter_${key}_${label}`; const toggleClass = i >= 5 ? 'hidden' : ''; const filter = `<li class="${toggleClass}"> <label for="${filterID}"> <input type="checkbox" id="${filterID}">${label}</input> <span class="count"></span> </label> </li>`; filters.push(filter); }); if (filters.length >= 5) { filters.push('<a href="#" class="facet-toggler">More...</a>'); } } } if (typeof group1 === 'undefined') { window.group1 = metadata.dge.thisGroup; window.group2 = metadata.dge.otherGroup; } const comparison = window.group1 + '-v-' + window.group2; // const comparison = 'flt-c1-v-cc-c1'; const metrics = ['log2fc', 'adj-p-value']; writeSliders(comparison, labels, metrics); filters = '<ul class="category-facet">' + filters.join('\n') + '</ul>'; document.querySelector('#facets').innerHTML += filters; // Filter genes upon clicking a category filter or slider checkbox d3.selectAll('input').on('click', function() { filterGenes(metrics, comparison); }); // Toggle "More..." and "Less..." for category facet upon click d3.selectAll('.facet-toggler').on('click', function() { if (this.text === 'More...') { var hiddenItems = document.querySelectorAll('li.hidden'); hiddenItems.forEach(item => item.classList.remove('hidden')); this.text = 'Less...'; } else { var extraItems = document.querySelectorAll('li:nth-child(n+6)'); extraItems.forEach(item => item.classList.add('hidden')); this.text = 'More...'; } }); // Toggle slider color upon changing slider checkbox document.querySelectorAll('.slider-checkbox').forEach(checkbox => { checkbox.addEventListener('change', function() { const sliderId = this.id.replace('slider-checkbox-', ''); let slider = document.querySelector(`#${sliderId}`); if (this.checked) { slider.classList.remove('inactive'); } else { slider.classList.add('inactive'); } }); }); // Update annotations upon changing selection in either comparison dropdown document.querySelectorAll('.comparison-dropdown').forEach(dropdown => { dropdown.addEventListener('change', function() { window.group1 = slug(document.querySelector('#comparison-dropdown-1').value).replace(/_/g, '-'); window.group2 = slug(document.querySelector('#comparison-dropdown-2').value).replace(/_/g, '-'); if (group1 === group2) return; if ( this.id === 'comparison-dropdown-1' || group1 !== ideogram.rawAnnots.metadata.dge.thisGroup ) { initIdeogram(baseAnnotsPath + '_' + window.group1 + '.json?alt=media'); } else { writeFacets(); } }); }); // Filter genes upon dragging slider let sliders = document.querySelectorAll('.ideogramSlider'); sliders.forEach((slider, i) => { noUiSlider.create(slider, getSliderConfig(metrics[i])); if (i === 0) { let val1 = document.querySelector(`#${slider.id} .noUi-value:nth-child(2)`); val1.innerHTML = '≤ ' + val1.innerHTML; let val2 = document.querySelector(`#${slider.id} .noUi-value:last-child`); val2.innerHTML = '≥ ' + val2.innerHTML; } slider.noUiSlider.on('change', function(){filterGenes(metrics, comparison);}); }); // Filter by log2fc and adjusted p-value upon calling writeFacets() let sliderCheckboxes = document.querySelectorAll('.slider-checkbox'); for (let i = 0; i < sliderCheckboxes.length; i++) { let checkbox = sliderCheckboxes[i]; if (i < sliderCheckboxes.length - 1) { checkbox.checked = true; // mark as checked, but don't trigger filter } else { checkbox.click(); // trigger filter for all checked (as a batch) } } } /** * Writes count of how many annotations match each filter * * TODO: Finish implementation **/ function writeCounts(counts) { var facet, count, key, value; for (facet in counts) { for (i = 0; i < counts[facet].length; i++) { count = counts[facet][i]; key = count.key - 1; value = '(' + count.value + ')'; // document.querySelectorAll('#' + facet + ' .count')[key].innerHTML = value; } } } /** * Reads which categorical filters are selected **/ function getCategorySelections() { var tmp, checkedFilter, checkedFilters, i, facet, counts, count, filterID, key, filterDomId, labels, selections = {}; checkedFilters = d3.selectAll('.category-facet input:checked').nodes(); for (i = 0; i < checkedFilters.length; i++) { filterDomId = checkedFilters[i].id; tmp = filterDomId.split('_'); facet = tmp[1]; checkedFilter = tmp.slice(2).join('_'); labels = ideogram.rawAnnots.metadata.labels[facet]; filterID = labels.indexOf(checkedFilter); if (facet in selections === false) { selections[facet] = {}; } selections[facet][filterID] = 1; } return selections; } /** * Reads which numerical filters are selected **/ function getRangeSelections() { var selections = {} document.querySelectorAll('.ideogramSlider').forEach(slider => { // Don't apply range filter if this slider is unchecked var checkbox = document.querySelector(`#slider-checkbox-${slider.id}`); if (checkbox.checked === false) return; var range = slider.noUiSlider.get().map(d => parseFloat(d)); if (slider.id.includes('log2fc')) { range = range.map(v => { if (v === -1.5) return -Infinity; if (v === 1.5) return Infinity; return v; }) } selections[slider.id] = range; }); return selections; } function getLocation(annot) { let start = annot.start.toLocaleString(); let end = (annot.start + annot.length).toLocaleString(); return `chr${annot.chr}:${start}-${end}`; } function getRoughLocation(annot) { let start = d3.formatPrefix('.2s', annot.start)(annot.start); return `chr${annot.chr}:${start}`; } function deslug(string) { return string[0].toUpperCase() + string.slice(1).replace(/-/g, ' '); } function slug(string) { return string.toLowerCase().replace(/ /g, '-').replace(/\&/g, 'and'); } function hyperlinkGene(annot) { let org = ideogram.rawAnnots.metadata.organism.replace(/ /g, '+'); var url = 'https://ncbi.nlm.nih.gov/gene/' + annot.entrezid; var link = '<a target="_blank" href="' + url + '">' + annot.name + '</a>'; return link; } function writeTable(metrics, comparison) { const annotsContainer = ideogram.filteredAnnots; let labels = ideogram.rawAnnots.metadata.labels; let rows = []; let headers = ['name', 'genename']; metrics.forEach(metric => headers.push(metric + '-' + comparison)); headers.push('Location'); const sortKey = metrics[0] + '-' + comparison; window.annotsContainer = annotsContainer; let annotsList = []; for (let i = 0; i < annotsContainer.length; i++) { annotsList = annotsList.concat(annotsContainer[i].annots); } for (let i = 0; i < annotsList.length; i++) { let annot = annotsList[i]; let cells = []; for (let j = 0; j < headers.length; j++) { let header = headers[j]; let value = annot[header]; if (header === 'name') { cells.push(hyperlinkGene(annot)); } else if (header === 'Location') { cells.push(getRoughLocation(annot)); } else if (header === 'gene-type') { cells.push(labels[header][value]); } else if (header.includes(metrics[1])) { const dispVal = value === 0 ? '< 10e-18' : value; cells.push({display: dispVal, sortVal: value}) } else { cells.push(value); } } rows.push(cells); }; const displayHeaders = headers.map((h, i) => { if (h === 'name') return 'Symbol'; if (h === 'genename') return 'Gene name'; if (h in labels === false) return deslug(h); if (Array.isArray(labels[h])) { // e.g. gene-type -> Gene type return deslug(h); } else if (i === 2 || i === 3) { const metric = metrics.filter(m => h.includes(m))[0]; return metricLabels[metric]; } else { return labels[h]; } }); const head = '<thead><tr><th>' + displayHeaders.join('</th><th>') + '</th></tr></thead>'; const table = `<table id="ideogram-annots-table" class="display">${head}</table>`; document.querySelector('#table-container').innerHTML = table; // Show more rows in each page for larger screens const screenIsTall = window.innerHeight >= 850; const screenIsWide = window.innerWidth >= 1600; const pageLength = screenIsTall ? 15 : 10; const colWidths = { symbol: 50, geneName: 515, log2fc: 120, adjustedPValue: 115, location: 80 } if (screenIsWide) { for (key in colWidths) {colWidths[key] += 50;} } $('#ideogram-annots-table').DataTable({ data: rows, dom: 'lBfrtip', // Docs: https://datatables.net/reference/option/dom buttons: [{extend: 'csv', text: 'Export'}], order: [[headers.indexOf(sortKey), 'desc']], pageLength: pageLength, lengthMenu: [10, 15, 25, 50, 100, 500, 1000], deferRender: true, // columnDefs: [null, null, null, null, {render: }] columns: [ {width: colWidths.symbol}, {width: colWidths.geneName}, {width: colWidths.log2fc}, {width: colWidths.adjustedPValue, render: {_: 'display', sort: 'sortVal'}}, {width: colWidths.location, type: 'genomeorder'} ] }); } /** * Applies all selected categorical and numerical filters **/ function filterGenes(metrics, comparison) { var selections = {}; selections = getCategorySelections(); rangeSelections = getRangeSelections(); Object.assign(selections, rangeSelections); counts = ideogram.filterAnnots(selections); writeCounts(counts); writeTable(metrics, comparison); } /** * Returns chromosome margin value that best fits overall ideogram width * * TODO: Merge this into Ideogram.js library **/ function getChrMargin(ideoWidth, numChrs, chrWidth) { var chrMargin; const calcChrMargin = ideoWidth/numChrs - chrWidth - chrWidth * 2; const maxChrMargin = 50; const minChrMargin = 13; if (minChrMargin <= calcChrMargin && calcChrMargin <= maxChrMargin) { chrMargin = calcChrMargin; } else if (minChrMargin > calcChrMargin) { chrMargin = minChrMargin; } else { chrMargin = maxChrMargin; } return chrMargin } /** * Fetch annotations, then create Ideogram **/ function initIdeogram(annotationsPath) { fetch(annotationsPath) .then(response => response.json()) .then(rawAnnots => { const numChrs = rawAnnots.annots.length; const ideoWidth = window.outerWidth - 340; const chrWidth = 11; const chrMargin = getChrMargin(ideoWidth, numChrs, chrWidth); // TODO: Make chromosome height responsive to larger screens // const tableHeight = 490; // const headerHeight = 120; // const calcChrHeight = window.outerHeight - tableHeight - headerHeight; // // const maxChrHeight = 225; // // const chrHeight = calcChrHeight > maxChrHeight ? maxChrHeight : calcChrHeight; const config = { container: '#ideogram-container', orientation: 'vertical', organism: rawAnnots.metadata.organism, // This is why we need to fetch annots first assembly: rawAnnots.metadata.assembly, chrHeight: 225, chrWidth: chrWidth, chrMargin: chrMargin, annotations: rawAnnots, annotationsLayout: 'histogram', barWidth: 3, chrLabelSize: 12, filterable: true, onLoad: writeFacets }; window.ideogram = new Ideogram(config); }); } function parseUrlParams() { var rawParams = document.location.search; var 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; }); } return urlParams; } window.basePath = 'https://www.googleapis.com/storage/v1/b/ideogram/o/nasa%2fannots%2f'; window.datasets = { 'GLDS-21': { assay: 'array_differential_expression', studyTitle: 'Effects of spaceflight on murine skeletal muscle gene expression' }, 'GLDS-25': { assay: 'array_differential_expression', studyTitle: 'STS-135 Liver Transcriptomics' }, // 'GLDS-242': { // assay: 'rna_seq_ERCCnorm_differential_expression', // studyTitle: 'Effect of spaceflight on liver from mice flown on the ISS for 33 days: transcriptional analysis' // }, 'GLDS-242': { assay: 'rna_seq_differential_expression', studyTitle: 'Effect of spaceflight on liver from mice flown on the ISS for 33 days: transcriptional analysis' }, 'GLDS-168': { assay: 'rna_seq_ERCCnorm_differential_expression', studyTitle: 'RR-1 and RR-3 mouse liver transcriptomics with and without ERCC control RNA spike-ins' }, 'GLDS-188': { assay: 'array_differential_expression', studyTitle: 'Dynamic gene expression response to altered gravity in human T cells (sounding rocket flight)' }, 'GLDS-37': { assay: 'rna_seq_differential_expression', studyTitle: 'Comparison of the spaceflight transcriptome of four commonly used Arabidopsis thaliana ecotypes' }, 'GLDS-42': { assay: 'array_differential_expression', studyTitle: 'Expression data from C. elegans' }, 'GLDS-3': { assay: 'array_differential_expression', studyTitle: 'Drosophila melanogaster gene expression changes after spaceflight' }, 'GLDS-19': { assay: 'array_differential_expression', studyTitle: 'Transcription profiling of rat to study the effect of hindlimb unloading on healing of medial collateral ligaments 3 weeks after injury' } } window.datasetsByOrganism = { 'mus-musculus': 'GLDS-21', 'homo-sapiens': 'GLDS-188', 'arabidopsis-thaliana': 'GLDS-37', 'rattus-norvegicus': 'GLDS-19' } window.urlParams = parseUrlParams(); window.group = ''; if ('annots-url' in urlParams) { // e.g. 'https://www.googleapis.com/storage/v1/b/degenome/o/GLDS-4_array_differential_expression_ideogram_annots.json'; window.assay = 'array_differential_expression'; window.studyTitle = ''; window.annotsPath = urlParams['annots-url'] + '?alt=media'; window.baseAnnotsPath = annotsPath.split('ideogram_annots')[0] + 'ideogram_annots'; } else { if ('acc' in urlParams) { window.accession = urlParams.acc; } else if ('org' in urlParams) { window.accession = datasetsByOrganism[urlParams.org]; } else { window.accession = 'GLDS-21'; } window.assay = datasets[accession].assay; window.studyTitle = datasets[accession].studyTitle; window.baseAnnotsPath = `${basePath}${accession}_${assay}_ideogram_annots`; window.annotsPath = `${baseAnnotsPath}${group}.json?alt=media`; } initIdeogram(annotsPath); function orderByGenomicPosition(a, b) { const splitA = a.split(':'); const splitB = b.split(':'); const chrNameA = splitA[0].slice(3); const chrNameB = splitB[0].slice(3); if (chrNameA === chrNameB) { const posA = parseFloat(splitA[1].slice(0, -1)); const posB = parseFloat(splitB[1].slice(0, -1)); return (posA < posB) ? -1 : (posA > posB) ? 1 : 0; } else { return chrNameA.localeCompare(chrNameB, undefined, {numeric: true, sensitivity: 'base'}) } } jQuery.extend(jQuery.fn.dataTable.ext.type.order, { 'genomeorder-asc': function(a, b) {return orderByGenomicPosition(a, b);}, 'genomeorder-desc': function(a, b) {return -1 * orderByGenomicPosition(a, b)} }); </script> </body> </html>