greenelab/adage-server

View on GitHub
interface/src/app/gene/enriched_signatures.js

Summary

Maintainability
D
1 day
Test Coverage
/**
 * "adage.gene.enrichedSignatures" module.
 */

angular.module('adage.gene.enrichedSignatures', [
  'adage.signature.resources',
  'adage.participation.resources',
  'adage.gene.resource',
  'adage.utils',
  'greenelab.stats'
])

.config(['$stateProvider', function($stateProvider) {
  $stateProvider.state('enriched_signatures', {
    url: '/gene/enriched_signatures?mlmodel&genes',
    views: {
      main: {
        templateUrl: 'gene/enriched_signatures.tpl.html',
        controller: 'EnrichedSignaturesCtrl as ctrl'
      }
    },
    data: {pageTitle: 'Enriched Signatures'}
  });
}])

.controller('EnrichedSignaturesCtrl', ['$stateParams', 'Signature',
  'Participation', 'Gene', 'MlModelTracker', 'MathFuncts', 'pValueCutoff',
  'pValueDigits', '$scope', '$q', '$log', 'errGen',
  function EnrichedSignatureController($stateParams, Signature, Participation,
    Gene, MlModelTracker, MathFuncts, pValueCutoff, pValueDigits, $scope, $q,
    $log, errGen) {
    var self = this;
    self.isValidModel = false;
    $scope.mlmodel = MlModelTracker;
    // Do nothing if the model ID in URL is falsey. The error will be taken
    // care of by "<ml-model-validator>" component.
    if (!$stateParams.mlmodel) {
      return;
    }

    self.modelInUrl = $stateParams.mlmodel;
    self.statusMessage = 'Connecting to the server ...';
    self.inProgress = true;
    self.pValueCutoff = pValueCutoff;
    self.enrichedSignatures = [];

    // Do nothing if no genes are specified in URL.
    if (!$stateParams.genes || !$stateParams.genes.split(',').length) {
      self.statusMessage = 'No genes are specified.';
      self.inProgress = false;
      return;
    }

    var genesInUrl = [];
    $stateParams.genes.split(',').forEach(function(token) {
      var id = parseInt(token);
      if (!isNaN(id) && genesInUrl.indexOf(id) === -1) {
        genesInUrl.push(id);
      }
    });

    // Promise that gets all signatures of given mlmodel in URL:
    var apiPromises = [];
    apiPromises.push(Signature.get(
      {mlmodel: self.modelInUrl, limit: 0},
      function success(response) {
        self.signatures = Object.create(null);
        response.objects.forEach(function(element) {
          self.signatures[element.id] = element.name;
        });
      },
      function error(err) {
        var message = errGen('Failed to get signatures: ', err);
        throw new Error(message);
      }
    ).$promise);

    var participations;
    // Promise that gets all participation records that are related to
    // the genes in URL:
    apiPromises.push(Participation.get(
      {'related_genes': $stateParams.genes, 'limit': 0},
      function success(response) {
        participations = response.objects;
      },
      function error(err) {
        var message = errGen(
          'Failed to get signature-gene participations: ', err);
        throw new Error(message);
      }
    ).$promise);

    // Helper function that calculates the enrichment for each signature
    // that includes at least one gene in the url.
    var calculateEnrichments = function(typeName, genesBySignatures) {
      var m = genesInUrl.length,
        N = self.geneNum,
        pValueArray = [],
        matchedGenesBySignature = [],
        signatureIDs = Object.keys(genesBySignatures);
      signatureIDs.forEach(function(sigID) {
        var genes = genesBySignatures[sigID],
          n = Object.keys(genes).length,
          k = 0,
          matchedGenes = [],
          selectedGene,
          pValue;
        for (var i = 0; i < genesInUrl.length; i++) {
          selectedGene = genesInUrl[i];
          if (genes[selectedGene]) {
            k++;
            matchedGenes.push(genes[selectedGene]);
          }
        }
        matchedGenesBySignature.push(matchedGenes);

        // Four parameters are needed in the formula:
        //     hyperGeometricTest(k, m, n, N)
        // k: the number of genes related to sigID in genesInUrl;
        // m: the number of genes in genesInUrl;
        // n: the total number of genes related to sigID;
        // N: the total number of genes.
        pValue = 1 - MathFuncts.hyperGeometricTest(k, m, n, N);
        pValueArray.push(pValue);
      });

      var significantSignatures = [];
      var correctedPValues = MathFuncts.multTest.fdr(pValueArray);
      correctedPValues.forEach(function(element, index) {
        var correctedPValue, sigID;
        if (element < self.pValueCutoff) {
          correctedPValue = element.toPrecision(pValueDigits);
          sigID = signatureIDs[index];
          significantSignatures.push({
            'url': '#/signature/' + sigID,
            'name': self.signatures[sigID],
            'participationType': typeName,
            'genes': matchedGenesBySignature[index].join(', '),
            'pValue': correctedPValue
          });
        }
      });

      significantSignatures.sort(function(a, b) {
        return a.pValue - b.pValue;
      });

      return significantSignatures;
    };

    // groupedGenes is an object that behaves like a 2-D table of genes.
    // groupedGenes[participationTypeID][signatureID] is an object whose
    // keys are gene IDs and values are gene names of those genes that
    // participate with signatureID and with participationTypeID.
    var groupedGenes = Object.create(null);

    // Main function to calculate the enrichment.
    var getEnrichedSignatures = function() {
      participations.forEach(function(element) {
        var sigID = element.signature;
        if (!self.signatures[sigID]) {
          return;
        } // ignore signatures that are not in current mlmodel

        var typeName = element.participation_type.name;
        if (!groupedGenes[typeName]) {
          groupedGenes[typeName] = Object.create(null);
        }

        if (!groupedGenes[typeName][sigID]) {
          groupedGenes[typeName][sigID] = Object.create(null);
        }
        geneID = element.gene.id;
        geneName = (element.gene.standard_name ?
                    element.gene.standard_name :
                    element.gene.systematic_name);
        groupedGenes[typeName][sigID][geneID] = geneName;
      });

      var participationTypes = Object.keys(groupedGenes);
      participationTypes.forEach(function(typeName) {
        var genesBySignatures = groupedGenes[typeName];
        var enrichment = calculateEnrichments(typeName, genesBySignatures);
        for (var i = 0; i < enrichment.length; i++) {
          self.enrichedSignatures.push(enrichment[i]);
        }
      });
    };

    // Do not retrieve genes from backend until mlmodel is ready,
    // because current mlmodel's organism ID is needed in the query.
    $scope.$watch('mlmodel.organism', function() {
      if ($scope.mlmodel.organism) {
        // Promise that gets the total number of genes.
        // For the moment, the total number of genes in our universe to
        // perform this enrichment analysis, is the total number of
        // Entrez IDs in our database for Pseudomonas aeruginosa.
        // The only field we care in response is 'total_count', so
        // 'limit' is set to 1 to save some bandwidth.
        apiPromises.push(Gene.get(
          {organism: $scope.mlmodel.organism.id, limit: 1},
          function success(response) {
            self.geneNum = response.meta.total_count;
          },
          function error(err) {
            var message = errGen('Failed to get total number of genes: ', err);
            throw new Error(message);
          }
        ).$promise);
        // Wait for all promises to finish before calculating the
        // enriched signatures.
        $q.all(apiPromises)
          .then(getEnrichedSignatures)
          .then(function() {
            self.statusMessage = '';
          })
          .catch(function(e) {
            $log.error(e.message);
            self.statusMessage = e.message + '. Please try again later.';
          })
          .finally(function() {
            self.inProgress = false;
          });
      }
    });
  }
])
;