interface/src/app/gene/network/network.js
/**
* "adage.gene.network" module.
*/
angular.module('adage.gene.network', [
'ui.router',
'ui.bootstrap',
'ngResource',
'rzModule',
'adage.utils',
'adage.signature.resources',
'adage.gene.resource'
])
.config(['$stateProvider', function($stateProvider) {
$stateProvider.state('gene_network', {
url: '/gene_network/?genes&mlmodel&base_group&comp_group',
views: {
main: {
templateUrl: 'gene/network/network.tpl.html',
controller: 'GeneNetworkCtrl as ctrl'
}
},
// When "gene_network" state exits, remove "gene-tip" and "edge-tip"
// elements from the DOM. (Without this function, the tips window will
// be visible in other states too.)
onExit: function() {
var removeTips = function(className) {
var elements = document.getElementsByClassName(className);
var i, n;
if (elements) { // This should be always true.
n = elements.length;
for (i = 0; i < n; ++i) {
elements[i].parentNode.removeChild(elements[i]);
}
}
};
removeTips('gene-tip');
removeTips('edge-tip');
},
data: {pageTitle: 'Gene Network'}
});
}])
.factory('Edge', ['$resource', 'ApiBasePath',
function($resource, ApiBasePath) {
return $resource(ApiBasePath + 'edge');
}
])
.factory('ExpressionValue', ['$resource', 'ApiBasePath',
function($resource, ApiBasePath) {
return $resource(
ApiBasePath + 'expressionvalue/',
{},
{post: {
method: 'POST',
// Setting Content-Type is required. Django will not process the
// POST data the way Angular's defaults send it.
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}}
);
}
])
.controller('GeneNetworkCtrl',
['$stateParams', 'Edge', 'Signature', 'Gene', 'ExpressionValue', '$log',
'errGen', '$httpParamSerializerJQLike', '$scope', 'MlModelTracker',
'$timeout',
function GeneNetworkController(
$stateParams, Edge, Signature, Gene, ExpressionValue, $log, errGen,
$httpParamSerializerJQLike, $scope, MlModelTracker, $timeout
) {
var self = this;
self.isValidModel = false;
// Do nothing if mlmodel in URL is falsey. The error will be taken
// care of by "<ml-model-validator>" component.
if (!$stateParams.mlmodel) {
return;
}
self.modelInUrl = $stateParams.mlmodel;
// Do nothing if no genes are specified in URL.
if (!$stateParams.genes || !$stateParams.genes.split(',').length) {
self.statusMessage = 'No genes are specified.';
return;
}
var baseSamples = $stateParams.base_group;
var compSamples = $stateParams.comp_group;
var geneColored = true;
if (!baseSamples || !compSamples) {
geneColored = false;
}
// The following properties of "self" will be available to HTML.
self.edgeSign = 'both';
self.statusMessage = 'Connecting to the server ...';
self.filterInfo = {
totalEdges: 0,
totalGenes: 0,
showingEdges: 0,
showingGenes: 0,
showingEdgesText: 'all',
showingGenesText: 'all'
};
var minCorrelation = -1.0, maxCorrelation = 1.0;
var midPoint = (minCorrelation + maxCorrelation) / 2.0;
var setEdgeColor = d3.scale.linear()
.domain([minCorrelation, midPoint, maxCorrelation])
.range(['green', 'orange', 'red']);
var network = d3.network() // Initialize the network.
.minEdge(minCorrelation)
.maxEdge(maxCorrelation)
.edgeColor(setEdgeColor)
.geneText(function(d) {
return d.label;
})
.edgeLegendStart(minCorrelation)
.edgeLegendEnd(maxCorrelation)
.edgeLegendText('Edge Correlation')
.forceSimulation(false) // disable force simulation
.bgCorrection(false);
// See the following discussion on why background correction is
// disabled in adage-server:
// https://github.com/greenelab/adage-server/issues/295
// This method is very helpful for debugging internal network state
self.getNetwork = function() {
return network;
};
var geneTip, edgeTip;
var updateGeneDownload = function() {
// format a list of genes for download & update self.geneDownload
if (!!self.geneDownload) {
// release a previously-created Blob
URL.revokeObjectURL(self.geneDownload);
}
self.geneDownload = URL.createObjectURL(new Blob(
network.drawGenes().map(function(g, index) {
var geneStr = g.label + ',' + g.entrezid + '\n';
if (index === 0) {
// prepend a column header
geneStr = 'Gene,EntrezID\n' + geneStr;
}
return geneStr;
})
));
};
var updateEdgeDownload = function() {
// format a list of edges for download & update self.edgeDownload
if (!!self.edgeDownload) {
// release a previously-created Blob
URL.revokeObjectURL(self.edgeDownload);
}
self.edgeDownload = URL.createObjectURL(new Blob(
network.drawEdges().map(function(e, index) {
var edgeStr = (
e.gene1.label + ',' + e.gene1.entrezid + ',' +
e.gene2.label + ',' + e.gene2.entrezid + ',' +
e.weight + '\n'
);
if (index === 0) {
// prepend a column header
edgeStr = 'Gene1,EntrezID1,Gene2,EntrezID2,Weight\n' + edgeStr;
}
return edgeStr;
})
));
};
var updateFilterInfo = function() {
var f = self.filterInfo;
f.totalEdges = network.edges().length;
f.totalGenes = network.genes().length;
f.showingEdges = network.drawEdges().length;
f.showingGenes = network.drawGenes().length;
if (f.showingEdges === f.totalEdges) {
f.showingEdgesText = 'all ';
} else {
f.showingEdgesText = f.showingEdges + ' of ';
}
f.showingEdgesText = f.showingEdgesText + f.totalEdges;
if (f.showingGenes === f.totalGenes) {
f.showingGenesText = 'all ';
} else {
f.showingGenesText = f.showingGenes + ' of ';
}
f.showingGenesText = f.showingGenesText + f.totalGenes;
};
self.renderNetwork = function() {
geneTip.hide();
edgeTip.hide();
var correlationSign;
if (self.edgeSign === 'both') {
correlationSign = 0;
} else if (self.edgeSign === 'negOnly') {
correlationSign = -1;
} else {
correlationSign = 1;
}
network.filterWithWeightSign(
self.minEdgeWeightSlider.value, maxCorrelation, correlationSign,
self.maxGeneNumSlider.value);
network.draw();
updateGeneDownload();
updateEdgeDownload();
updateFilterInfo();
};
self.minEdgeWeightSlider = { // slider that controls min edge weight
value: 0, // initial position of slider
options: {
floor: 0, // minimum of the slider bar
ceil: maxCorrelation, // maximum of the slider bar
step: 0.01,
precision: 2,
showSelectionBarEnd: true,
onEnd: function() {
self.renderNetwork();
}
}
};
self.maxGeneNumSlider = { // slider that controls max number of genes
value: 0, // initial position of slider
options: {
floor: 1, // minimum of the slider bar
ceil: 100, // maximum of the slider bar
step: 1,
showSelectionBar: true,
onEnd: function() {
self.renderNetwork();
}
}
};
// Function that returns unique numerical gene IDs included in the URL.
var getGenesInURL = function(genesParam) {
var arr = [];
genesParam.split(',').forEach(function(token) {
var id = parseInt(token);
if (!isNaN(id) && arr.indexOf(id) === -1) {
arr.push(id);
}
});
return arr;
};
var genesInURL = getGenesInURL($stateParams.genes);
var genes = [];
var edges = [];
// Function that finds index of an input gene ID in "genes".
var findGeneIndex = function(gid) {
for (var i = 0; i < genes.length; ++i) {
if (genes[i].id === gid) {
return i;
}
}
return -1;
};
// Function that returns the list of genes that will be queried.
var getQueriedGenes = function(edgeList) {
// The genes that will be queried on the server should be determined
// by both genesInURL and edgeList, because even if a gene in the URL
// doesn't have any edge in the database, it still should be displayed
// in the network.
var geneList = genesInURL.slice(0); // Copy all gene IDs from the URL.
// Collect unique gene IDs in edgeList.
edgeList.forEach(function(val) {
if (geneList.indexOf(val.gene1) === -1) {
geneList.push(val.gene1);
}
if (geneList.indexOf(val.gene2) === -1) {
geneList.push(val.gene2);
}
});
return geneList;
};
// Function that sets genes in the network.
var setGenes = function(responseObject) {
genes = responseObject.objects;
genes.forEach(function(g) {
g.label = g.standard_name ? g.standard_name : g.systematic_name;
if (genesInURL.indexOf(g.id) !== -1) {
g.query = true;
}
});
};
// Function that sets edges in the network.
var setEdges = function(edgeList) {
// Build a hash with gene objects indexed by pk.
var geneObjectHash = {};
genes.forEach(function(geneObj) {
geneObjectHash[geneObj.pk] = geneObj;
});
// Now we can process all edges
for (var i = 0, n = edgeList.length; i < n; ++i) {
var currEdge = edgeList[i];
currEdge.gene1 = geneObjectHash[currEdge.gene1]; // gene1
var idx = findGeneIndex(currEdge.gene1.id);
currEdge['source'] = idx;
currEdge.gene2 = geneObjectHash[currEdge.gene2]; // gene2
idx = findGeneIndex(currEdge.gene2.id);
currEdge['target'] = idx;
edges.push(currEdge); // Add current edge
}
// IMPORTANT implementation detail: d3.network library will reset
// edge.source and edge.target to the gene object later, so the values
// of "source" and "target" properties of the edge won't be integers
// any more.
};
var renderSliders = function() {
// Reset default position and floor of minEdgeWeightSlider.
self.minEdgeWeightSlider.value = MlModelTracker.g2gEdgeCutoff;
self.minEdgeWeightSlider.options.floor = MlModelTracker.g2gEdgeCutoff;
// Reset default position and ceiling of maxGeneNumSlider.
self.maxGeneNumSlider.value = Math.min(genes.length, 50);
self.maxGeneNumSlider.options.ceil = genes.length;
// Reconfigure the slider and force render it. See the discussion at:
// https://github.com/angular-slider/angularjs-slider/issues/79#issuecomment-225438841
// (Also tried $scope.$$postDigest, not work.)
$timeout(function() {
$scope.$broadcast('rzSliderForceRender');
});
};
/**
* Draw gene-gene network.
* @param {void} null;
* @return {void}.
*/
function drawNetwork() {
// Calculate svg size.
var minSvgSize = 600; // Minimum size of svg.
var maxSvgSize = 1280; // Maximum size of svg.
var svgSize = genes.length * 10;
if (svgSize < minSvgSize) {
svgSize = minSvgSize;
} else if (svgSize > maxSvgSize) {
svgSize = maxSvgSize;
}
// Initialize tips on gene and edge.
geneTip = d3.tip()
.attr('class', 'gene-tip')
.offset([-20, 0]);
edgeTip = d3.tip()
.attr('class', 'edge-tip')
.offset([-20, 0]);
/**
* Compile gene information tips.
* @param {selected_gene_data} data;
* @return {string} The string in HTML format.
*/
function getGeneInfo(data) {
var result = '<div id="title">' + data.label + '</div>';
result += '<br>Entrez ID: ' + data.entrezid;
if (data.systematic_name) {
result += '<br>Systematic name: ' + data.systematic_name;
}
if (data.standard_name) {
result += '<br>Standard name: ' + data.standard_name;
}
if (data.description) {
result += '<br>Description: ' + data.description;
}
if (data.aliases) {
result += '<br>aliases: ' + data.aliases;
}
return result;
}
/**
* Callback function to show gene tips when a gene is clicked.
* @param {gene_data} data;
* @return {void}.
*/
function showGeneTip(data) {
edgeTip.hide(); // Hide edge-tip window (if any) first.
geneTip.show(data);
}
/**
* Callback function to show edge tips when an edge is clicked.
* @param {edge_data} data;
* @return {void}.
*/
function showEdgeTip(data) {
geneTip.hide(); // Hide gene-tip window (if any) first.
var weightPrecision = 3;
var htmlText = 'Edge weight: ';
htmlText += data.weight.toFixed(weightPrecision);
var heavyGenes = [data.gene1.id, data.gene2.id].join(',');
var target = d3.event.target;
Signature.get(
{'heavy_genes': heavyGenes,
'mlmodel': MlModelTracker.id,
'order_by': 'name',
'limit': 0
},
function success(response) {
var i = 0, n = response.objects.length;
var anchorTag;
htmlText += '<br>' + n +
(n > 1 ? ' signatures are ' : ' signature is ');
htmlText += 'related to both genes' + (n > 0 ? ':' : '.');
for (; i < n; ++i) {
anchorTag = '<a href="#/signature/' + response.objects[i].id +
'" target="_blank">';
htmlText += '<br>* ' + anchorTag + response.objects[i].name;
htmlText += '</a>';
}
edgeTip.html(htmlText);
edgeTip.show(data, target);
},
function error(response) {
var message = errGen(
'Failed to get signature info for gene edge', response);
$log.error(message);
htmlText += '<br>' + message + '. Please try again later.';
edgeTip.html(htmlText);
edgeTip.show(data, target);
}
);
}
network.genes(genes).edges(edges);
d3.select('#chart').append('svg') // Initialize SVG
.attr('width', svgSize)
.attr('height', svgSize)
.call(network)
.call(geneTip)
.call(edgeTip);
geneTip.html(getGeneInfo);
network.onGene('click.custom', showGeneTip);
network.onEdge('click.custom', showEdgeTip);
renderSliders();
// Draw network svg with edge and gene legend bars.
network.showEdgeLegend();
if (geneColored) {
// Function to set the color of gene circles in d3 network:
var setGeneColor = d3.scale.linear()
.domain([-1.0, 0, 1.0]).range(['blue', 'white', 'red']);
network.geneColor(setGeneColor).showGeneLegend();
}
self.renderNetwork();
// Add event handlers to gene-tip and edge-tip so that they will be
// hidden when clicked:
var addClickHandler = function(className, handler) {
var elements = document.getElementsByClassName(className);
var i, n;
if (elements) {
n = elements.length;
for (i = 0; i < n; ++i) {
elements[i].onclick = handler;
}
}
};
addClickHandler('gene-tip', geneTip.hide);
addClickHandler('edge-tip', edgeTip.hide);
} // End of drawNetwork()
Edge.get(
{genes: $stateParams.genes, mlmodel: $stateParams.mlmodel, limit: 0},
function success(responseObject) {
var edgeList = responseObject.objects;
// Collect a list of distinct genes for query.
var geneList = getQueriedGenes(edgeList);
// Now retrieve the Gene objects using the geneList.
Gene.post(
{},
$httpParamSerializerJQLike({
'pk__in': geneList.join(),
'limit': 0
}),
function success(responseObject) {
setGenes(responseObject);
if (genes.length === 0) {
self.statusMessage =
'Gene(s) not found, please check the gene ID.';
return;
}
setEdges(edgeList);
// If genes in the network do not need to be colored, draw
// the network and we are done.
if (!geneColored) {
self.statusMessage = '';
drawNetwork();
return;
}
// If gene circles should be colored, calculate each gene's
// expression value difference between comp_group and base_group.
var baseSampleIDs = baseSamples.split(',');
baseSampleIDs = baseSampleIDs.map(function(x) {
return parseInt(x);
});
var compSampleIDs = compSamples.split(',');
compSampleIDs = compSampleIDs.map(function(x) {
return parseInt(x);
});
var sampleID = baseSampleIDs.concat(compSampleIDs);
var geneID = genes.map(function(g) {
return g.id;
});
ExpressionValue.post(
{},
// Angular does not serialize POST data the way we would expect
// so we have to do it manually here. For more details, see
// https://github.com/angular/angular.js/issues/6039#issuecomment-113502695
$httpParamSerializerJQLike({
'sample__in': sampleID.join(),
'order_by': 'gene',
'gene__in': geneID.join()
}),
function success(responseObject) {
// Function that calculates input gene's expression value
// based on the input array of base and comparison sums.
var calcGeneExpr = function(geneID, exprTracker) {
// Do nothing if the length of exprTracker is not 4, or one
// of the two groups doesn't have any expression values.
if (geneID === null || exprTracker.length !== 4 ||
!exprTracker[1] || !exprTracker[3]) {
return;
}
// Do nothing if input geneID is not found in genes.
var geneIndex = findGeneIndex(geneID);
if (geneIndex === -1) {
return;
}
var baseAvg = exprTracker[0] / exprTracker[1];
var compAvg = exprTracker[2] / exprTracker[3];
genes[geneIndex].exprVal = compAvg - baseAvg;
};
var i = 0, n = responseObject.objects.length;
var geneID = n > 0 ? responseObject.objects[0].gene : null;
// exprTracker[0]: sum of expression values in base_group;
// exprTracker[1]: # of expression values in base_group;
// exprTracker[2]: sum of expression values in comp_group;
// exprTracker[3]: # of expression values in comp_group;
var exprTracker = [0, 0, 0, 0];
for (; i < n; ++i) {
var currObj = responseObject.objects[i];
if (currObj.gene !== geneID) { // reset exprTracker
calcGeneExpr(geneID, exprTracker);
geneID = currObj.gene;
exprTracker = [0, 0, 0, 0];
}
// Current record belongs to base_group
if (baseSampleIDs.indexOf(currObj.sample) !== -1) {
exprTracker[0] += currObj.value;
++exprTracker[1];
} else { // Current record belongs to comp_group
exprTracker[2] += currObj.value;
++exprTracker[3];
}
}
calcGeneExpr(geneID, exprTracker); // The last gene!
self.statusMessage = '';
drawNetwork();
},
function error(response) {
var message = errGen('Failed to get gene expression value',
response);
$log.error(message);
self.statusMessage = message + '. Please try again later.';
}
);
},
function error(response) {
var message = errGen('Failed to get genes', response);
$log.error(message);
self.statusMessage = message + '. Please try again later.';
}
);
},
function error(response) {
var message = errGen('Failed to get edges', response);
$log.error(message);
self.statusMessage = message + '. Please try again later.';
}
);
}
]
);