'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>