Skip to content

Commit

Permalink
Merge pull request #182 from eweitz/differential-expression
Browse files Browse the repository at this point in the history
Enable filtering by numeric range
  • Loading branch information
eweitz committed Dec 18, 2019
2 parents eb43ec7 + 9593db8 commit 3dfabe3
Show file tree
Hide file tree
Showing 4 changed files with 586 additions and 16 deletions.
358 changes: 358 additions & 0 deletions examples/vanilla/differential-expression.html
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>

0 comments on commit 3dfabe3

Please sign in to comment.