-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #182 from eweitz/differential-expression
Enable filtering by numeric range
- Loading branch information
Showing
4 changed files
with
586 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,358 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>Differential expression of genes | Ideogram</title> | ||
<script type="text/javascript" src="../../dist/js/ideogram.min.js"></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> | ||
<link href="https://cdn.jsdelivr.net/npm/nouislider@14.1.0/distribute/nouislider.css" rel="stylesheet"> | ||
<link rel="icon" type="image/x-icon" href="img/ideogram_favicon.ico"> | ||
<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> | ||
ul { | ||
list-style: none; | ||
padding-left: 10px; | ||
float: left; | ||
} | ||
|
||
#ideogram-container { | ||
position: relative; | ||
top: 180px; | ||
border-top: 1px solid #EEE; | ||
} | ||
|
||
li {padding: 2px 0;} | ||
li.hidden {display: none;} | ||
|
||
input {margin-right: 5px;} | ||
|
||
#gene-type {padding-left: 30px;} | ||
|
||
.note { | ||
font-style: italic; | ||
padding-left: 10px; | ||
clear: left; | ||
} | ||
|
||
ul.category-facet { | ||
padding-right: 15px; | ||
margin-right: 15px; | ||
border-right: 1px solid #EEE; | ||
} | ||
|
||
.category-facet li { | ||
background: #FFF; | ||
} | ||
|
||
.noUi-value-horizontal {transform: translate(-50%, 70%);} | ||
|
||
.inactive .noUi-connect {background: #AAA;} | ||
.inactive .noUi-tooltip {color: #AAA;} | ||
</style> | ||
</head> | ||
<body> | ||
<h1>Differential expression of genes | Ideogram</h1> | ||
<a href="../">Overview</a> | | ||
<a href="annotations-file-url">Previous</a> | | ||
<a href="annotations-animated">Next</a> | | ||
<a href="https://github.com/eweitz/ideogram/blob/gh-pages/annotations-histogram.html" target="_blank">Source</a> | ||
<br/><br/> | ||
<div id="summary"></div> | ||
<div id="facets" style="position: absolute; z-index: 9999;"></div> | ||
<div id="ideogram-container" style="display: none;"></div> | ||
<script type="text/javascript"> | ||
|
||
const d3 = Ideogram.d3; | ||
|
||
/** | ||
* 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': [-4], | ||
'max': 4 | ||
}, | ||
start: [-4, -1, 1, 4], | ||
sliderDecimals: 1, | ||
pipDecimals: 0, | ||
connect: [false, true, false, true, false], | ||
values: [0, 12.5, 25, 37.5, 50, 62.5, 75, 87.5, 100], | ||
density: 4 | ||
} | ||
}; | ||
|
||
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 left = i * 400; | ||
const style = `left: ${left + 20}px; top: 45px`; | ||
const metricLabels = { | ||
'log2fc': 'log<sub>2</sub>(fold change)', | ||
'adj-p-value': 'Adjusted <i>p</i>-value' | ||
} | ||
const metricLabel = metricLabels[metric]; | ||
document.querySelector(`#${comparisonId}`).innerHTML += | ||
`<div style="position: absolute;"> | ||
<div style="width: 300px; position: relative; left: ${left}px; 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 inactive" style="${style}"></div> | ||
</div>`; | ||
document.querySelectorAll('.slider-checkbox').forEach(checkbox => { | ||
checkbox.addEventListener('change', function() { | ||
var sliderId = this.id.replace('slider-checkbox-', ''); | ||
var slider = document.querySelector(`#${sliderId}`); | ||
if (this.checked) { | ||
slider.classList.remove('inactive'); | ||
} else { | ||
slider.classList.add('inactive'); | ||
} | ||
}); | ||
}) | ||
} | ||
|
||
/** | ||
* Adds slider widgets for all numerical metrics in this comparison | ||
*/ | ||
function writeSliders(comparison, labels) { | ||
const comparisonLabel = labels[`log2fc-${comparison}`].replace('Log2fc_', ''); | ||
const metrics = ['log2fc', 'adj-p-value']; | ||
const comparisonId = comparison.toLowerCase().replace(/[()\s]/g, ''); | ||
document.querySelector('#facets').innerHTML += ` | ||
<div id="${comparisonId}" style="float: left"> | ||
<div style="margin-bottom: 15px; width: 400px; margin-top: 15px;">${comparisonLabel}</div> | ||
<div>`; | ||
|
||
metrics.forEach((metric, i) => writeSliderContainer(metric, i, comparisonId)); | ||
document.querySelectorAll('.ideogramSlider').forEach((slider, i) => { | ||
noUiSlider.create(slider, getSliderConfig(metrics[i])); | ||
slider.noUiSlider.on('change', filterGenes); | ||
}); | ||
} | ||
|
||
/** | ||
* Adds facets, i.e. groups of filters, to query Ideogram annotations | ||
**/ | ||
function writeFacets() { | ||
const metadata = this.rawAnnots.metadata; | ||
const labels = metadata.labels; | ||
let filters = []; | ||
|
||
document.querySelector('#ideogram-container').style.display = ''; | ||
|
||
document.querySelector('#summary').innerHTML = | ||
`Distribution of all | ||
<span id="organismName" style="font-style: italic">${metadata.organism}</span> | ||
genes throughout the genome. Filter genes below. | ||
Visualized with <a href="https://github.com/eweitz/ideogram">Ideogram.js</a>.`; | ||
|
||
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>'); | ||
} | ||
} | ||
} | ||
|
||
filters = '<ul class="category-facet">' + filters.join('\n') + '</ul>'; | ||
document.querySelector('#facets').innerHTML = filters; | ||
|
||
document.querySelector('#organismName').innerHTML = metadata.organism; | ||
|
||
const comparison = 'space-flight-v-ground-control' | ||
writeSliders(comparison, labels); | ||
|
||
d3.selectAll('input').on('click', function() { | ||
filterGenes(); | ||
}); | ||
|
||
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...'; | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* 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 [min, max] = slider.noUiSlider.get().map(d => parseFloat(d)); | ||
selections[slider.id] = [min, max]; | ||
}); | ||
|
||
return selections; | ||
} | ||
|
||
/** | ||
* Applies all selected categorical and numerical filters | ||
**/ | ||
function filterGenes() { | ||
var selections = {}; | ||
|
||
selections = getCategorySelections(); | ||
|
||
rangeSelections = getRangeSelections(); | ||
|
||
Object.assign(selections, rangeSelections); | ||
|
||
counts = ideogram.filterAnnots(selections); | ||
|
||
writeCounts(counts); | ||
} | ||
|
||
// Fetch annotations, then create Ideogram | ||
const annotationsPath = '../../data/annotations/GLDS-4_array_differential_expression_ideogram_annots.json'; | ||
fetch(annotationsPath) | ||
.then(response => response.json()) | ||
.then(rawAnnots => { | ||
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: 380, | ||
chrWidth: 12, | ||
chrMargin: 20, | ||
annotations: rawAnnots, | ||
annotationsLayout: 'histogram', | ||
barWidth: 3, | ||
filterable: true, | ||
onLoad: writeFacets | ||
}; | ||
window.ideogram = new Ideogram(config); | ||
}); | ||
|
||
</script> | ||
</body> | ||
</html> |
Oops, something went wrong.