(function() {
  let app = angular.module('chrisjsherm.app', [
    'ui.router',
    'chrisjsherm.mock-api',
    'chrisjsherm.service.web-worker',
  ])
})();
<!DOCTYPE html>
<html>

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <link data-require="bootstrap-css@3.3.7" data-semver="3.3.7" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" />
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.5.x" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.11/angular.min.js" data-semver="1.5.11"></script>
    <script data-require="angular-mocks@1.5.5" data-semver="1.5.5" src="https://code.angularjs.org/1.5.5/angular-mocks.js"></script>
    <script data-require="ui-router@0.3.1" data-semver="0.3.1" src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.3.1/angular-ui-router.js"></script>
    
    <script src="mock-api.js"></script>
    <script src="uuid.service.js"></script>
    <script src="web-worker.service.js"></script>
    <script src="app.module.js"></script>
    <script src="app.config.js"></script>
    <script src="app.controller.js"></script>
  </head>

  <body ng-app="chrisjsherm.app">
    <section ui-view></section>
  </body>

</html>
/* Put your css in here */

body {
  margin: 20px;
}
(function() {
  let app = angular.module('chrisjsherm.app');

  app.config(MyAppConfig);

  function MyAppConfig(
    $urlRouterProvider, $stateProvider
  ) {
    // For any unmatched url, redirect here.
    $urlRouterProvider.otherwise('/');

    $stateProvider
      .state('home', {
        url: '/',
        templateUrl: 'home.html',
        controller: 'AppController',
        controllerAs: 'appCtrl',
        resolve: {
          httpService: '$http',

          annualHomeValuesRequest: function getAnnualHomeValues(
            httpService
          ) {
            return httpService.get(
              '/api/region/washington-dc/home-value-index?years=2009-2013'  
            );
          },
        },
      });
  }
})();
(function() {
  let app = angular.module('chrisjsherm.app');

  app.controller('AppController', AppController);

  function AppController(
    annualHomeValuesRequest, WebWorkerService, $scope
  ) {
    // Private properties.
    let vm = this;
    let messageId, 
      eventListener;

    // Public properties.
    vm.cagrPercent = undefined;
  
    // Activation.
    activate();

    // Private methods.
    function activate() {
      messageId = WebWorkerService.calculateCompoundAnnualGrowthRate(
          annualHomeValuesRequest.data
      );
      eventListener = $scope.$on(messageId, onMessageReceived);
    }
    
    function onMessageReceived(event, args) {
        vm.cagrPercent = (args * 100).toFixed(2);

      // De-register event listener.
      eventListener();
    }
  }
})();
<div class="row">
  <div class="col-sm-12">
    <h1>
      Compound Annual Growth Rate
    </h1>
    
    <p>
      Washington, D.C. Home Value Index 2009-13:
      <span ng-bind="appCtrl.cagrPercent + '%'"></span>
    </p>
  </div>
</div>
(function() {
  let service = angular.module('chrisjsherm.service.web-worker', [
    'chrisjsherm.service.uuid',  
  ]);

  service.factory('WebWorkerService', WebWorkerService);

  function WebWorkerService(
    $rootScope, UuidService
  ) {

    // Private properties.
    let webWorker = new Worker('web-worker.js');

    // Activation.
    activate();

    // Public interface.
    let serviceInterface = {
      calculateCompoundAnnualGrowthRate: calculateCompoundAnnualGrowthRate,
    };
    
    return serviceInterface;

    // Private methods.
    function activate() {
      webWorker.addEventListener(
        'message',
        onWebWorkerResponse,
        false
      );
    }
    
    function calculateCompoundAnnualGrowthRate(values) {
      const MESSAGE_ID = UuidService.generate();
      
      webWorker.postMessage(
        JSON.stringify([
          MESSAGE_ID,
          'calculateCompoundAnnualGrowthRate',
          values
        ])  
      );
      
      return MESSAGE_ID;
    }
    
    function onWebWorkerResponse(message) {
      let data = JSON.parse(message.data),
        messageId = data[0],
        result = data[1];
        
        // Call $apply around async events, including anywhere it calls 
        // broadcast/emit.
        // Docs: https://github.com/angular/angular.js/wiki/When-to-use-$scope.$apply()
        $rootScope.$apply($rootScope.$broadcast(
          messageId,
          result
        ));
    }
  }
})();
(function () {
    let service = angular.module('chrisjsherm.service.uuid', []);
    
    service.factory('UuidService', UuidService);
    
        function UuidService() {

            /**
             * Generates a rfc4122 UUID based on:
             * http://stackoverflow.com/a/21963136/1579559.
             */
            const lut = Array(256).fill().map((_, i) => (i < 16 ? '0' : '') + (i).toString(16));
            const formatUuid = ({ d0, d1, d2, d3 }) =>
                lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + '-' +
                lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + '-' +
                lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + '-' +
                lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + '-' +
                lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] +
                lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] +
                lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff];

            const getRandomValuesFunc = window.crypto && window.crypto.getRandomValues ?
                () => {
                    const dvals = new Uint32Array(4);
                    window.crypto.getRandomValues(dvals);
                    return {
                        d0: dvals[0],
                        d1: dvals[1],
                        d2: dvals[2],
                        d3: dvals[3],
                    };
                } :
                () => ({
                    d0: Math.random() * 0x100000000 >>> 0,
                    d1: Math.random() * 0x100000000 >>> 0,
                    d2: Math.random() * 0x100000000 >>> 0,
                    d3: Math.random() * 0x100000000 >>> 0,
                });

            const uuid = () => formatUuid(getRandomValuesFunc());

            var service = {
                generate: generateUuid,
            };

            return service;

            // Private functions.
            function generateUuid() {
                return uuid();
            }
        }
})();
let self = this;

self.addEventListener('message', receiveMessage, false);

// Main.
function receiveMessage(message) {
  var data = JSON.parse(message.data),
    workerResult;
  const MESSAGE_ID = data[0];

  switch (data[1]) {

    // Web worker functions.
    case 'close':
      self.close();
      break;

      // Service functions.
    case 'calculateCompoundAnnualGrowthRate':
      workerResult = calculateCompoundAnnualGrowthRate(
        data[2]
      );
      break;
  }

  self.postMessage(JSON.stringify(
    [
      MESSAGE_ID,
      workerResult
    ]
  ));
}

// Private functions.
function calculateCompoundAnnualGrowthRate(
  values
) {
  if (!values) {
    return;
  }
  
  let cagr,
    endingValue,
    numberOfYears = values.length,
    startingValue = values[0];
    
    endingValue = values[numberOfYears - 1];
    
    cagr = Math.pow((endingValue / startingValue), (1 / numberOfYears)) - 1;

    return cagr;
}
(function() {
  let app = angular.module('chrisjsherm.mock-api', ['ngMockE2E']);

  app.run(RunFn);

  function RunFn($httpBackend) {

    $httpBackend.whenGET('/api/region/washington-dc/home-value-index?years=2009-2013')
      .respond([
        356000,
        355000,
        362000,
        364000,
        388000,
      ]);

    $httpBackend.whenGET(/.*/).passThrough();
  }
})();