'use strict';

(function() {
  var app = angular.module('a3d', ['input-form', 'index-chart']);

  app.controller('NavigationController', ['$scope', '$log', '$window',
    function($scope, $log, $window) {
      var navCtrl = this;

      $window.onpopstate = function(e) {
        /**
         * onpopstate is fired when we leave the input page
         * and when the user hits the back button.  The apply()
         * should only be invoked on the back button case because
         * that is not run in AngularJS, it's a JavaScript "turn"
         * that's run outside of AngularJS.
         *
         * "turns" described at http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
         * error you'll get without the if: https://docs.angularjs.org/error/$rootScope/inprog?p0=$digest
         *
         */
        if (navCtrl.shouldShowOutputChart()) {
          $scope.$apply(function() {
            navCtrl.flipMode();
          });
        }
      };

      navCtrl.initInputMode = function() {
        navCtrl.inputMode = true;
      };

      navCtrl.shouldShowInputForm = function() {
        return navCtrl.inputMode;
      };

      navCtrl.shouldShowOutputChart = function() {
        return !navCtrl.inputMode;
      };

      navCtrl.flipMode = function() {
        navCtrl.inputMode = !navCtrl.inputMode;
      };

      navCtrl.initInputMode();

      function compareHelper(topic1Name, topic1Total, topic1Data, topic2Name, topic2Total, topic2Data, insightType, countType) {
        $log.debug("Processing compareHelper");
        var compareServletRequestURL =
          'http://audience-marketing.appspot.com/' +
          'audiencemarketing/compareServlet';

        var inputData = {
          insightType: insightType,
          countType: countType,
          topic1Name: topic1Name,
          topic1Total: topic1Total,
          topic1Data: topic1Data,
          topic2Name: topic2Name,
          topic2Total: topic2Total,
          topic2Data: topic2Data
        };

        $.post(
          compareServletRequestURL, {
            inputData: JSON.stringify(inputData)
          },
          function(data) {
            navCtrl.comparisonJSONObj = $.parseJSON(data);
          }).fail(
          function(jqXHR, textStatus, errorThrown) {
            // Documentation http://api.jquery.com/jQuery.ajax/
            throw "Server could not generate chart because " + errorThrown;
          });
      };

      $scope.$watch('navCtrl.topic1Name', function(newValue) {
        if (angular.isDefined(newValue)) {

          try {
            $log.debug("Calling compareHelper");
            compareHelper(navCtrl.topic1Name,
              navCtrl.topic1Total,
              navCtrl.topic1Data,
              navCtrl.topic2Name,
              navCtrl.topic2Total,
              navCtrl.topic2Data,
              navCtrl.insightType,
              navCtrl.countType);
          }
          catch (error) {
            alert(error);
          }

          $window.location = "#";

          navCtrl.flipMode();
        }
      });
    }
  ]);
})();
<!DOCTYPE html>
<html ng-app="a3d">

<head>
  <meta charset="utf-8" />
  <title>AngularJS Plunker</title>
  <script>
    document.write('<base href="' + document.location + '" />');
  </script>
  <link rel="stylesheet" href="style.css" />
  <script data-require="angular.js@1.4.x" src="https://code.angularjs.org/1.4.0-rc.0/angular.js" data-semver="1.4.0-rc.0"></script>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
  <script src="http://code.highcharts.com/highcharts.js"></script>
  <script src="http://code.highcharts.com/modules/exporting.js"></script>
  <script src="http://blacklabel.github.io/annotations/js/annotations.js"></script>
  <script src="app.js"></script>
  <script type="text/javascript" src="inputForm.js"></script>
  <script type="text/javascript" src="indexChart.js"></script>
  <link rel="stylesheet" type="text/css" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
</head>

<body>

  <nav class="navbar navbar-default">
    <div class="container-fluid">
      <div class="navbar-header">
        <a class="navbar-brand">
          <img alt="NetBase" src="https://pbs.twimg.com/profile_images/1860441508/NB_Powered_RGB_400x400.jpg" width="30px" float="left">
        </a>
      </div>
    </div>
  </nav>

  <h4>How to expose behavior from Element directive? (<a href="http://stackoverflow.com/questions/29638426/how-to-expose-behavior-from-element-directive/29638518">stackoverflow 29638426</a>)</h4>
  <div ng-controller="NavigationController as navCtrl">
    <input-form topic-1-name="navCtrl.topic1Name" topic-1-total="navCtrl.topic1Total" topic-1-data="navCtrl.topic1Data" topic-2-name="navCtrl.topic2Name" topic-2-total="navCtrl.topic2Total" topic-2-data="navCtrl.topic2Data" insight-type="navCtrl.insightType"
    count-type="navCtrl.countType" ng-show="navCtrl.shouldShowInputForm()"></input-form>
    <index-chart comparison-json-obj="navCtrl.comparisonJSONObj" ng-show="navCtrl.shouldShowOutputChart()"></index-chart>
</body>

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

'use strict';

(function(){
	var inputForm = angular.module('input-form', [ ]);
	
	inputForm.directive('inputForm', function(){
		return {
			restrict: 'E',
			templateUrl: 'input-form.html',
		    scope: {
	                topic1Name: "=",
	                topic1Total: "=",
	                topic1Data: "=",
	                topic2Name: "=",
	                topic2Total: "=",
	                topic2Data: "=",
	                insightType: "=",
	                countType: "="
		    },
		    controllerAs: 'inputCtrl',
			bindToController: true,
			controller: ['$log', '$scope', function($log, $scope){
			  var inputCtrl = this;
			  inputCtrl.inputValues = millennialsDefault;
			      
			  inputCtrl.emitData = function() {
                  inputCtrl.topic1Name = inputCtrl.inputValues.topic1Name;
                  inputCtrl.topic1Total = inputCtrl.inputValues.topic1Total;
                  inputCtrl.topic1Data = tsvJSON(inputCtrl.inputValues.topic1Data);
                  inputCtrl.topic2Name = inputCtrl.inputValues.topic2Name;
                  inputCtrl.topic2Total = inputCtrl.inputValues.topic2Total;
                  inputCtrl.topic2Data = tsvJSON(inputCtrl.inputValues.topic2Data);
                  inputCtrl.insightType = getInsightType(inputCtrl.inputValues.topic1Data);
                  inputCtrl.countType = getCountType(inputCtrl.inputValues.topic1Data);
			    
				  $log.debug("Emitting '" + inputCtrl.topic1Name + "' to '" + inputCtrl.topic2Name + "'");
			  };
			  
			  inputCtrl.swapInput = function(){
				  var swappedInputValues = {
						  topic1Name: inputCtrl.inputValues.topic2Name,
						  topic1Total: inputCtrl.inputValues.topic2Total,
						  topic1Data: inputCtrl.inputValues.topic2Data,
						  topic2Name: inputCtrl.inputValues.topic1Name,
						  topic2Total: inputCtrl.inputValues.topic1Total,
						  topic2Data: inputCtrl.inputValues.topic1Data,
				  };
	
				  inputCtrl.inputValues = swappedInputValues;
			  };

			  inputCtrl.clearInput = function(){
				  inputCtrl.inputValues = 
				  {
						  topic1Name: "",
						  topic1Total: undefined,
						  topic1Data: "",
						  topic2Name: "",
						  topic2Total: undefined,
						  topic2Data: "",
				  };
				  $scope.inputForm.$setUntouched();
				  $scope.inputForm.$setPristine();
			  };
			  
			}]
		};
	});
	
	function tsvJSON(tsv) {
		var lines = tsv.split("\n");

		var result = [];

		var headersSplit = lines[0].split("\t");
		var headers = [];
		for (var h = 0; h < headersSplit.length; h++) {
			var header = headersSplit[h];
			if (-1 != $.inArray(header, headers)) {
				header = header + "2";
			}
			headers.push(header);
		}

		for (var i = 1; i < lines.length; i++) {
			var obj = {};
			var currentline = lines[i].split("\t");

			for (var j = 0; j < headers.length; j++) {
				if (!isEmpty(headers[j])) {
					obj[headers[j]] = currentline[j];
				}
			}

			result.push(obj);
		}

		return result;
	};
	
  function isEmpty(str) {
  	return (!str || 0 === str.length);
  }
	
	
	function getInsightType(tsv) {	
		var lines = tsv.split("\n");
		var headersSplit = lines[0].split("\t");
		return headersSplit[0];			
	}

	function getCountType(tsv) {
		var lines = tsv.split("\n");
		var headersSplit = lines[0].split("\t");
		return headersSplit[1];			
	}
	
	var INTEGER_REGEXP = /^\-?\d+$/;
	inputForm.directive('integer', function() {
		return {
			require: 'ngModel',
			link: function(scope, elm, attrs, ctrl) {
				ctrl.$validators.integer = function(modelValue, viewValue) {
					if (ctrl.$isEmpty(modelValue)) {
						// consider empty models to be valid
						return true;
					}

					if (INTEGER_REGEXP.test(viewValue)) {
						// it is valid
						return true;
					}

					// it is invalid
					return false;
				};
			}
		};
	});
	
	inputForm.directive('hasHeaders', function() {
		return {
			require: 'ngModel',     
			link: function(scope, elm, attrs, ctrl) {
				ctrl.$validators.hasHeaders = function(modelValue, viewValue) {
					if (ctrl.$isEmpty(modelValue)) {
						// consider empty models to be valid
						return true;
					}

					function hasHeaders(tsv) {
						var lines = tsv.split("\n");
						var headersSplit = lines[0].split("\t");
						return 2 <= headersSplit.length;
					}

					return hasHeaders(viewValue);
				};
			}
		};
	});
	
	inputForm.directive('hasMatchingHeaders', function() {
		return {
			require: 'ngModel',     
			link: function(scope, elm, attrs, ctrl) {
				ctrl.$validators.hasMatchingHeaders = function(modelValue, viewValue) {
					if (ctrl.$isEmpty(modelValue)) {
						// consider empty models to be valid
						return true;
					}

					function hasMatchingHeaders(tsv2) {
						var tsv1 = scope.inputCtrl.inputValues.topic1Data;
						
						var lines1 = tsv1.split("\n");
						var lines2 = tsv2.split("\n");
						
						var headers1 = lines1[0];
						var headers2 = lines2[0];
						
						return (headers1.trim() === headers2.trim());
					}

					return hasMatchingHeaders(viewValue);
				};
			}
		};
	});
	
	inputForm.directive('rowsHaveSameNumberOfColumns', function() {
		return {
			require: 'ngModel',     
			link: function(scope, elm, attrs, ctrl) {
				ctrl.$validators.rowsHaveSameNumberOfColumns = function(modelValue, viewValue) {
					if (ctrl.$isEmpty(modelValue)) {
						// consider empty models to be valid
						return true;
					}

					function rowsHaveSameNumberOfColumns(tsv) {
						var lines = tsv.split("\n");

						var headers = lines[0].split("\t");

						for (var i = 1; i < lines.length; i++) {
							var currentline = lines[i].split("\t");

							if (headers.length != currentline.length) {
								return false;
							}
						}
						
						return true;
					}
					
					return rowsHaveSameNumberOfColumns(viewValue);
				};
			}
		};
	});	
	
	inputForm.directive('removeBlanksLines', function(){
		return {
			require: 'ngModel',
			link: function(scope, elm, attrs, ctrl) {
				ctrl.$parsers.unshift(removeBlanksLines);
				
				function removeBlanksLines(viewValue) {
					var tsv = viewValue;					
					var newTSV = "";
					
					var lines = tsv.split("\n");
					for (var i=0; i<lines.length; i++) {
						var line = lines[i];
						line = line.trim();
						if (0 < line.length) {
							if (0 < newTSV.length) {
								newTSV += "\n";
							}
							newTSV += line;
						}
					}
					
					return newTSV;					
				};
			}
		};
	});
	
	var millennialsDefault = {
		topic1Name: "Millennials 18-24",
		topic1Total: 5147435,
		topic1Data: "Things	Mentions" + "\n" +
			"Friday	17547" + "\n" +
			"NFL	23634" + "\n" +
			"dude	18755" + "\n" +
			"look	98501" + "\n" +
			"face	23052" + "\n" +
			"tweet	34526" + "\n" +
			"feeling	25845" + "\n" +
			"video	52488" + "\n" +
			"free online collection	0" + "\n" +
			"show	48485" + "\n" +
			"Hope	37806" + "\n" +
			"time	141334" + "\n" +
			"people	121795" + "\n" +
			"@YouTube video	11938" + "\n" +
			"life	96724" + "\n" +
			"@YouTube	24310" + "\n" +
			"birthday	41297" + "\n" +
			"man	68962" + "\n" +
			"game	102044" + "\n" +
			"guy	58816" + "\n" +
			"friend	62649" + "\n" +
			"God	41503" + "\n" +
			"girl	48589" + "\n" +
			"Twitter	939919" + "\n" +
			"day	151985" + "\n" +
			"happy birthday	23764" + "\n" +
			"world	40408" + "\n" +
			"love	181383" + "\n" +
			"shit	61837" + "\n" +
			"girls	28500" + "\n" +
			"work	82074" + "\n" +
			"Christmas	33649" + "\n" +
			"school	41076" + "\n" +
			"food	18298" + "\n" +
			"bed	21816" + "\n" +
			"home	44114" + "\n" +
			"mom	24294" + "\n" +
			"team	42350" + "\n" +
			"boy	31835" + "\n" +
			"music	28180" + "\n" +
			"movie	23160" + "\n" +
			"best friend	12861" + "\n" +
			"baby	31806" + "\n" +
			"nigga	27114" + "\n" +
			"pizza	9687" + "\n" +
			"family	24566" + "\n" +
			"kid	28026" + "\n" +
			"song	25637" + "\n" +
			"phone	23038" + "\n" +
			"ass	30307" + "\n" +
			"bae	18023" + "\n" +
			"car	20130",
		topic2Name: "Millennials 25-34",
		topic2Total: 3782799,
		topic2Data: "Things	Mentions" + "\n" +
	    	"Friday	13941" + "\n" +
	    	"NFL	22275" + "\n" +
	    	"dude	11397" + "\n" +
	    	"look	79031" + "\n" +
	    	"face	16036" + "\n" +
	    	"tweet	28851" + "\n" +
	    	"feeling	14192" + "\n" +
	    	"video	48205" + "\n" +
	    	"free online collection	1901" + "\n" +
	    	"show	47222" + "\n" +
	    	"Hope	33659" + "\n" +
	    	"time	107275" + "\n" +
	    	"people	70984" + "\n" +
	    	"@YouTube video	10140" + "\n" +
	    	"life	52829" + "\n" +
	    	"@YouTube	22706" + "\n" +
	    	"birthday	21503" + "\n" +
	    	"man	52046" + "\n" +
	    	"game	85687" + "\n" +
	    	"guy	42496" + "\n" +
	    	"friend	32881" + "\n" +
	    	"God	29877" + "\n" +
	    	"girl	20879" + "\n" +
	    	"Twitter	577782" + "\n" +
	    	"day	106387" + "\n" +
	    	"happy birthday	11359" + "\n" +
	    	"world	31577" + "\n" +
	    	"love	123367" + "\n" +
	    	"shit	25489" + "\n" +
	    	"girls	11603" + "\n" +
	    	"work	63940" + "\n" +
	    	"Christmas	24114" + "\n" +
	    	"school	23438" + "\n" +
	    	"food	16698" + "\n" +
	    	"bed	8612" + "\n" +
	    	"home	32249" + "\n" +
	    	"mom	10986" + "\n" +
	    	"team	41043" + "\n" +
	    	"boy	17465" + "\n" +
	    	"music	25431" + "\n" +
	    	"movie	16726" + "\n" +
	    	"best friend	3720" + "\n" +
	    	"baby	20000" + "\n" +
	    	"nigga	9279" + "\n" +
	    	"pizza	5240" + "\n" +
	    	"family	21076" + "\n" +
	    	"kid	24646" + "\n" +
	    	"song	18002" + "\n" +
	    	"phone	12290" + "\n" +
	    	"ass	13585" + "\n" +
	    	"bae	4235" + "\n" +
	    	"car	12723"
	};
})();
<style type="text/css">
  .css-form input.ng-invalid.ng-dirty {
    background-color: #FA787E;
  }

  .css-form input.ng-valid.ng-dirty {
    background-color: #78FA89;
  }
</style>

<form name="inputForm" ng-submit="inputForm.$valid && inputCtrl.emitData()" class="css-form" novalidate>
	<div class="row">
		<!-- ------------- -->
		<!--  Topic Names  -->
		<!-- ------------- -->		
	
		<div class="col-xs-6 form-group">
			<label for="topic1Name">Topic 1 Name:</label>
			<input name="topic1Name" ng-model="inputCtrl.inputValues.topic1Name" type="text" required>
			<span class="error" ng-show="inputForm.topic1Name.$error.required">Required</span>
		</div>
		<div class="col-xs-6 form-group">
			<label for="topic2Name">Topic 1 Name:</label>
			<input name="topic2Name" ng-model="inputCtrl.inputValues.topic2Name" type="text" required>
			<span class="error" ng-show="inputForm.topic2Name.$error.required">Required</span>
		</div>	
	</div>						  
	
	<div class="row">
		<!-- -------------- -->
		<!--  Topic Totals  -->
		<!-- -------------- -->		

		<div class="col-xs-6 form-group">
			<label for="topic1Total">Topic 1 Total:</label>
			<input name="topic1Total" ng-model="inputCtrl.inputValues.topic1Total" type="number" required min=0 integer>
			<span class="error" ng-show="inputForm.topic1Total.$error.required">Required</span>
			<span class="error" ng-show="inputForm.topic1Total.$error.integer">Integer required</span>
			<span class="error" ng-show="inputForm.topic1Total.$error.min">Positive integer required</span>
		</div>
		<div class="col-xs-6 form-group">
			<label for="topic2Total">Topic 2 Total:</label>
			<input name="topic2Total" ng-model="inputCtrl.inputValues.topic2Total" type="number" required min=0 integer>
			<span class="error" ng-show="inputForm.topic2Total.$error.required">Required</span>
			<span class="error" ng-show="inputForm.topic2Total.$error.integer">Integer required</span>
			<span class="error" ng-show="inputForm.topic2Total.$error.min">Positive integer required</span>
		</div>
	</div>
	
	<div class="row">
		<!-- ------------ -->
		<!--  Topic Data  -->
		<!-- ------------ -->
		
		<div class="col-xs-6 form-group">
			<label for="topic1Data">Topic 1 Data:</label>
			<textarea name="topic1Data" ng-model="inputCtrl.inputValues.topic1Data" rows="10" cols="30" 
				required
				remove-blanks-lines 
				has-headers
				rows-have-same-number-of-columns></textarea>
      		<span class="error" ng-show="inputForm.topic1Data.$error.required">Required</span>
            <span class="error" ng-show="inputForm.topic1Data.$error.hasHeaders">Data should have at least 2 header columns</span>
            <span class="error" ng-show="inputForm.topic1Data.$error.rowsHaveSameNumberOfColumns">All rows should have the same number of columns</span>
		</div>		
		<div class="col-xs-6 form-group">
			<label for="topic2Data">Topic 2 Data:</label>
			<textarea name="topic2Data" ng-model="inputCtrl.inputValues.topic2Data" rows="10" cols="30" 
				required
				remove-blanks-lines
				has-headers
				rows-have-same-number-of-columns
				has-matching-headers></textarea>
			<span class="error" ng-show="inputForm.topic2Data.$error.required">Required</span>
			<span class="error" ng-show="inputForm.topic2Data.$error.hasHeaders">Data should have at least 2 header columns</span>
			<span class="error" ng-show="inputForm.topic2Data.$error.hasMatchingHeaders">Data set headers should match each other</span>
			<span class="error" ng-show="inputForm.topic2Data.$error.rowsHaveSameNumberOfColumns">All rows should have the same number of columns</span>
		</div>		
	</div>	
	
	<button type="submit" class="btn btn-info btn-lg" ng-disabled="!inputForm.$valid">Compare</button>
	<button type="button" class="btn btn-default btn-lg" ng-click="inputCtrl.swapInput()">Swap</button>
	<button type="button" class="btn btn-default btn-lg" ng-click="inputCtrl.clearInput()">Clear Input</button>
</form>
'use strict';

(function() {
  var indexChart = angular.module('index-chart', []);

  indexChart.directive('indexChart', function() {
    return {
      restrict: 'E',
      templateUrl: 'index-chart.html',
      scope: {
        comparisonJSONObj: "="
      },
      controllerAs: 'chartCtrl',
      bindToController: true,
      controller: ['$log', '$scope',
        function($log, $scope) {
          var chartCtrl = this;

          chartCtrl.renderChart = function() {
            var categories = chartCtrl.comparisonJSONObj.level1Labels;
            var title = chartCtrl.comparisonJSONObj.title;
            var subtitle = chartCtrl.comparisonJSONObj.subtitle;
            var series = chartCtrl.comparisonJSONObj.series;
            var indices = chartCtrl.comparisonJSONObj.indices;
            var insightType = chartCtrl.comparisonJSONObj.insightType;

            $log.debug("Rendering '" + title + "'");

            $("#indexchart").highcharts({
              chart: {
                type: 'bar',

                events: { 
                  load: function() {
                    for (var i = 0; i < indices.length; i++) {
                      var indexLabel = indices[i];
                      var indexValue = parseFloat(indexLabel);
                      var indexColor;
                      var indexThreshold = 1;
                      if (">1000x" == indexLabel || indexThreshold < indexValue) {
                        indexColor = 'green';
                      } else if (indexThreshold > indexValue) {
                        indexColor = 'red';
                      } else {
                        indexColor = 'gray';
                      }
                      var anAnnotation = {
                        title: {
                          text: indexLabel,
                          style: {
                            color: indexColor
                          },
                          y: 4 /* Extra adjustment to anchorY */
                        },
                        anchorX: "right",
                        anchorY: "bottom",
                        y: this.xAxis[0].toPixels(i),
                        /* Note y has to map to xAxis for bar chart */
                        x: $(this.container).width() - 15
                      };
                      this.addAnnotation(anAnnotation);
                    }

                  }
                }
              },

              title: {
                text: title
              },

              subtitle: {
                text: subtitle
              },

              xAxis: {
                categories: categories,
                title: {
                  text: insightType
                }
              },

              yAxis: {
                title: {
                  text: 'Share'
                }
              },

              tooltip: {
                shared: true,
                formatter: function() {
                  var s = '<b>' + this.x + '</b>';

                  for (var i = this.points.length - 1; i >= 0; i--) {
                    var point = this.points[i];
                    s += '<br/>' + point.series.name + ': ' + point.y.toFixed(6) + ' share';
                  }

                  return s;
                },
              },

              plotOptions: {
                bar: {
                  grouping: false,
                  shadow: false,
                  borderWidth: 0
                }
              },

              series: series,

              legend: {
                reversed: true
              },

              annotations: [],

              annotationsOptions: {
                enabledButtons: false
              },

              credits: {
                enabled: false
              }
            });

          }; 

          chartCtrl.renderChart();
        }
      ]
    };
  });
})();
{{chartCtrl.comparisonJSONObj}}<br><br>

<div id="indexchart" style="min-width: 310px; max-width: 800px; height: 900px; margin: 0 auto"></div>