diff --git a/.gitignore b/.gitignore
index 7c4f411..4f40dd6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ app/js/templates.js
app/data
*.py[cod]
karma.subunit
+package-lock.json
# C extensions
*.so
@@ -64,3 +65,27 @@ ChangeLog
# Debug
*.log
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 7058a03..632d849 100644
--- a/README.rst
+++ b/README.rst
@@ -22,11 +22,9 @@ Installation
============
Installation - Frontend
-----------------------
-Installation of the frontend requires Node.js and Gulp. On Ubuntu::
+Installation of the frontend requires Node.js (>=16.14.0) and Gulp. On Ubuntu::
- sudo apt-get install nodejs
- sudo apt-get install npm
- sudo npm install -g gulp
+ sudo snap install node --classic --channel=16
Then, install the Node modules by running, from the project directory::
@@ -45,7 +43,7 @@ Usage - Development
-------------------
A development server can be run as follows::
- gulp dev
+ npm run dev
This will open a web browser and reload code automatically as it changes on the
filesystem.
@@ -64,7 +62,7 @@ Usage - Production
------------------
The production application can be build using::
- gulp prod
+ npm run prod
This will automatically build portable html/javascript and python
utilities into ``dist/stackviz-VERSION.tar.gz``.
@@ -98,7 +96,7 @@ Data should be written to :code:`stackviz-html/data/` using
Testing
=======
* Python tests: :code:`tox -e py36`
-* JavaScript unit tests: :code:`gulp unit`
+* JavaScript unit tests: :code:`npm run test`
* JavaScript E2E tests: :code:`gulp e2e`
Manuals & Developer Docs
diff --git a/angular.json b/angular.json
new file mode 100644
index 0000000..bba9ab1
--- /dev/null
+++ b/angular.json
@@ -0,0 +1,106 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "stackviz": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "style": "scss"
+ }
+ },
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:application",
+ "options": {
+ "outputPath": "dist/stackviz",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": [
+ "zone.js"
+ ],
+ "tsConfig": "tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.scss"
+ ],
+ "scripts": [],
+ "server": "src/main.server.ts",
+ "prerender": true,
+ "ssr": {
+ "entry": "server.ts"
+ }
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "stackviz:build:production"
+ },
+ "development": {
+ "buildTarget": "stackviz:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "stackviz:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ],
+ "tsConfig": "tsconfig.spec.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.scss"
+ ],
+ "scripts": []
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/.eslintrc b/app/.eslintrc
deleted file mode 100644
index 81006b0..0000000
--- a/app/.eslintrc
+++ /dev/null
@@ -1,7 +0,0 @@
-# Enable eslint-plugin-angular
-plugins:
- - angular
-
-rules:
- angular/module-setter: 0
- angular/module-getter: 0
diff --git a/app/fonts/FontAwesome.otf b/app/fonts/FontAwesome.otf
deleted file mode 100644
index 681bdd4..0000000
Binary files a/app/fonts/FontAwesome.otf and /dev/null differ
diff --git a/app/fonts/fontawesome-webfont.eot b/app/fonts/fontawesome-webfont.eot
deleted file mode 100644
index a30335d..0000000
Binary files a/app/fonts/fontawesome-webfont.eot and /dev/null differ
diff --git a/app/fonts/fontawesome-webfont.svg b/app/fonts/fontawesome-webfont.svg
deleted file mode 100644
index 6fd19ab..0000000
--- a/app/fonts/fontawesome-webfont.svg
+++ /dev/null
@@ -1,640 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/fonts/fontawesome-webfont.ttf b/app/fonts/fontawesome-webfont.ttf
deleted file mode 100644
index d7994e1..0000000
Binary files a/app/fonts/fontawesome-webfont.ttf and /dev/null differ
diff --git a/app/fonts/fontawesome-webfont.woff b/app/fonts/fontawesome-webfont.woff
deleted file mode 100644
index 6fd4ede..0000000
Binary files a/app/fonts/fontawesome-webfont.woff and /dev/null differ
diff --git a/app/fonts/fontawesome-webfont.woff2 b/app/fonts/fontawesome-webfont.woff2
deleted file mode 100644
index 5560193..0000000
Binary files a/app/fonts/fontawesome-webfont.woff2 and /dev/null differ
diff --git a/app/index.html b/app/index.html
deleted file mode 100644
index 005dae7..0000000
--- a/app/index.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/js/constants.js b/app/js/constants.js
deleted file mode 100644
index f865a8f..0000000
--- a/app/js/constants.js
+++ /dev/null
@@ -1,8 +0,0 @@
-'use strict';
-
-var AppSettings = {
- appTitle: 'StackViz',
- healthRoot: 'http://status.openstack.org/openstack-health'
-};
-
-module.exports = AppSettings;
diff --git a/app/js/controllers/_index.js b/app/js/controllers/_index.js
deleted file mode 100644
index bdd98a9..0000000
--- a/app/js/controllers/_index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-'use strict';
-
-var angular = require('angular');
-var bulk = require('bulk-require');
-
-module.exports = angular.module('app.controllers', []);
-
-bulk(__dirname, ['./**/!(*_index|*.spec).js']);
diff --git a/app/js/controllers/console.js b/app/js/controllers/console.js
deleted file mode 100644
index 71370cb..0000000
--- a/app/js/controllers/console.js
+++ /dev/null
@@ -1,20 +0,0 @@
-'use strict';
-
-var controllersModule = require('./_index');
-
-var codemirror = require('codemirror');
-
-/**
- * @ngInject
- */
-function ConsoleController($scope, $location, $stateParams, datasetService) {
- var vm = this;
- vm.artifactName = $stateParams.artifactName;
- vm.show = $location.search().show;
-
- datasetService.artifact(vm.artifactName, 'console').then(function(response) {
- vm.data = response.data;
- });
-}
-
-controllersModule.controller('ConsoleController', ConsoleController);
diff --git a/app/js/controllers/home.js b/app/js/controllers/home.js
deleted file mode 100644
index ccbc98e..0000000
--- a/app/js/controllers/home.js
+++ /dev/null
@@ -1,33 +0,0 @@
-'use strict';
-
-var controllersModule = require('./_index');
-
-/**
- * @ngInject
- */
-function HomeCtrl($scope, $state, datasetService) {
-
- // ViewModel
- var vm = this;
- vm.focus = $state.params.artifactName;
-
- datasetService.groups().then(function(groups) {
- vm.groups = groups;
-
- if (!vm.focus) {
- vm.focus = groups[0];
- }
- });
-
- // update the page url as the focus id changes, but don't reload
- $scope.$watch(function() {
- return vm.focus;
- }, function(value, old) {
- if (value !== old) {
- $state.go('home', { artifactName: value }, { notify: false });
- }
- });
-
-}
-
-controllersModule.controller('HomeController', HomeCtrl);
diff --git a/app/js/controllers/main.js b/app/js/controllers/main.js
deleted file mode 100644
index 3f6163f..0000000
--- a/app/js/controllers/main.js
+++ /dev/null
@@ -1,15 +0,0 @@
-'use strict';
-
-var controllersModule = require('./_index');
-
-/**
- * @ngInject
- */
-function MainCtrl($window, $scope) {
- $window.addEventListener('resize', function () {
- $scope.$broadcast('windowResize');
- $scope.$apply();
- });
-}
-
-controllersModule.controller('MainController', MainCtrl);
diff --git a/app/js/controllers/test-details.js b/app/js/controllers/test-details.js
deleted file mode 100644
index 1f43c99..0000000
--- a/app/js/controllers/test-details.js
+++ /dev/null
@@ -1,93 +0,0 @@
-'use strict';
-
-var controllersModule = require('./_index');
-
-/**
- * Responsible for making three calls to the dataset service. First, the
- * dataset corresponding to the given int id is loaded, then the raw and details
- * JSON files are loaded and placed into state variables. Also note that a copy
- * of the details JSON is kept in `originalDetails` so that information is not
- * lost when parsing. Progress of the dataset service calls is recorded and
- * displayed in a progress bar on `test-details.html`.
- * @ngInject
- */
-function TestDetailsCtrl(
- $scope, $location, $stateParams, $log, $q,
- datasetService, progressService, AppSettings) {
- var vm = this;
- vm.artifactName = $stateParams.artifactName;
- vm.testName = $stateParams.test;
- vm.healthRoot = AppSettings.healthRoot;
-
- progressService.start({ parent: 'div[role="main"] .panel-body' });
-
- // load dataset, raw json, and details json
- var statsArtifact = datasetService.artifact(vm.artifactName, 'subunit-stats');
- var subunitArtifact = datasetService.artifact(vm.artifactName, 'subunit');
- var detailsArtifact = datasetService.artifact(vm.artifactName, 'subunit-details');
-
- var statsPromise = statsArtifact.then(function(response) {
- vm.stats = response.data;
- });
-
- var subunitPromise = subunitArtifact.then(function(response) {
- var item = null;
- for (var t in response.data) {
- if (response.data[t].name === vm.testName) {
- item = response.data[t];
- }
- }
- vm.item = item;
- progressService.inc();
- });
-
- var detailsPromise = detailsArtifact.then(function(details) {
- vm.details = details;
- vm.originalDetails = angular.copy(details.data[vm.testName]);
- vm.itemDetails = details.data[vm.testName];
- }).catch(function(ex) {
- // ignore errors, details won't exist for deployer
- });
-
- $q.all([statsPromise, subunitPromise, detailsPromise]).catch(function(ex) {
- $log.error(ex);
- }).finally(function() {
- progressService.done();
- });
-
- /**
- * This function changes the `itemDetails.pythonlogging` variable to only
- * show lines with the log levels specified by the four boolean parameters.
- * EX: If the `info` parameter is set to true, `itemDetails.pythonlogging`
- * will display lines that contain the text `INFO`.
- * @param {boolean} info
- * @param {boolean} debug
- * @param {boolean} warning
- * @param {boolean} error
- */
- vm.parsePythonLogging = function(info, debug, warning, error) {
- if (vm.originalDetails && vm.originalDetails.pythonlogging) {
- var log = vm.originalDetails.pythonlogging;
- var ret = [];
- var lines = log.split('\n');
- for (var i in lines) {
- var line = lines[i];
- if (info && line.includes("INFO")) {
- ret.push(line);
- }
- if (debug && line.includes("DEBUG")) {
- ret.push(line);
- }
- if (warning && line.includes("WARNING")) {
- ret.push(line);
- }
- if (error && line.includes("ERROR")) {
- ret.push(line);
- }
- }
- vm.itemDetails.pythonlogging = ret.join('\n');
- }
- };
-}
-
-controllersModule.controller('TestDetailsController', TestDetailsCtrl);
diff --git a/app/js/controllers/timeline.js b/app/js/controllers/timeline.js
deleted file mode 100644
index 04f0c6b..0000000
--- a/app/js/controllers/timeline.js
+++ /dev/null
@@ -1,32 +0,0 @@
-'use strict';
-
-var controllersModule = require('./_index');
-
-/**
- * @ngInject
- */
-function TimelineCtrl($scope, $location, $stateParams, datasetService) {
-
- // ViewModel
- var vm = this;
- vm.artifactName = $stateParams.artifactName;
-
- vm.hoveredItem = null;
- vm.selectedItem = null;
-
- vm.preselect = $location.search().test;
-
- $scope.$watch(function() {
- return vm.selectedItem;
- }, function(value) {
- if (value) {
- $location.search({ test: value.name });
- vm.preselect = null;
- } else if (vm.preselect === null) {
- $location.search({ test: null });
- }
- });
-
-}
-
-controllersModule.controller('TimelineController', TimelineCtrl);
diff --git a/app/js/directives/_index.js b/app/js/directives/_index.js
deleted file mode 100644
index 55e142c..0000000
--- a/app/js/directives/_index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-'use strict';
-
-var angular = require('angular');
-var bulk = require('bulk-require');
-
-module.exports = angular.module('app.directives', []);
-
-bulk(__dirname, ['./**/!(*_index|*.spec).js']);
diff --git a/app/js/directives/artifact-summary.js b/app/js/directives/artifact-summary.js
deleted file mode 100644
index 03b7f52..0000000
--- a/app/js/directives/artifact-summary.js
+++ /dev/null
@@ -1,37 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-/**
- * @ngInject
- */
-function artifactSummary($compile, datasetService, summaryService) {
- var link = function(scope, el, attrs, ctrl) {
- scope.$watch('artifactName', function(artifactName) {
- el.empty();
-
- datasetService.artifacts(artifactName).then(function(artifacts) {
- artifacts.forEach(function(artifact) {
- summaryService.directivesForType(artifact.artifact_type).forEach(function(d) {
- var name = d.directiveName;
- var tag = '<' + name + ' artifact-name="\'' + artifactName + '\'">' +
- '' + name + '>';
-
- var e = $compile(tag)(scope);
- el.append(e);
- });
- });
- });
- });
- };
-
- return {
- restrict: 'EA',
- scope: {
- 'artifactName': '='
- },
- link: link
- };
-}
-
-directivesModule.directive('artifactSummary', artifactSummary);
diff --git a/app/js/directives/codemirror-console.js b/app/js/directives/codemirror-console.js
deleted file mode 100644
index a46fdc4..0000000
--- a/app/js/directives/codemirror-console.js
+++ /dev/null
@@ -1,130 +0,0 @@
-'use strict';
-
-var CodeMirror = require('codemirror');
-require('codemirror/addon/mode/simple');
-require('codemirror/addon/fold/foldcode');
-require('codemirror/addon/fold/foldgutter');
-
-var directivesModule = require('./_index.js');
-
-CodeMirror.defineSimpleMode('console', {
- start: [
- {
- token: 'comment',
- regex: /[\d\-]+ [\d\:\.]+ \|/,
- sol: true
- }, {
- token: 'keyword',
- regex: /\[[a-z\-]+\] \$ .*/
- }
- ]
-});
-
-/**
- * @ngInject
- */
-function codemirrorConsole($compile, $window, datasetService, summaryService) {
- var instance = null;
- var element = null;
- var headers = new Map();
-
- var rangeFinder = function(cm, pos) {
- if (!headers.has(pos.line)) {
- return null;
- }
-
- var foundSelf = false;
- var foldEnd = null;
- headers.forEach(function(name, lineNo) {
- if (!foundSelf && lineNo === pos.line) {
- foundSelf = true;
- return;
- }
-
- if (foundSelf && foldEnd === null) {
- foldEnd = lineNo - 1;
- }
- });
-
- if (foldEnd === null) {
- foldEnd = cm.lastLine();
- }
-
- return {
- from: CodeMirror.Pos(pos.line, cm.getLine(pos.line).length),
- to: CodeMirror.Pos(foldEnd, cm.getLine(foldEnd).length)
- };
- };
-
- var link = function(scope, el, attrs, ctrl) {
- instance = CodeMirror(el[0], {
- lineNumbers: true,
- readOnly: true,
- value: 'test test test',
- mode: 'console',
- theme: 'neat',
- foldGutter: {
- rangeFinder: rangeFinder
- },
- gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]
- });
-
- element = el.find('div');
-
- var updateHeight = function() {
- var rect = element[0].getBoundingClientRect();
- element[0].style.height = ($window.innerHeight - rect.top) + 'px';
- };
-
- scope.$on('windowResize', updateHeight);
- updateHeight();
- };
-
- /**
- * @ngInject
- */
- var controller = function($scope) {
- $scope.$watch('data', function(data) {
- if (!data) {
- return;
- }
-
- var lines = [];
- var currentLine = 0;
- headers.clear();
- data.scripts.forEach(function(script) {
- headers.set(currentLine, script.name);
-
- script.lines.forEach(function(line) {
- lines.push(line.date + ' | ' + line.line);
- currentLine++;
- });
- });
-
- instance.setValue(lines.join('\n'));
-
- var doc = instance.getDoc();
- headers.forEach(function(text, lineNumber) {
- var element = angular.element('
' + text + '
');
- element.addClass('console-script-header');
-
- doc.addLineWidget(lineNumber, element[0], {
- above: true,
- noHScroll: true
- });
- });
- });
- };
-
- return {
- restrict: 'EA',
- scope: {
- 'data': '=',
- 'show': '='
- },
- link: link,
- controller: controller
- };
-}
-
-directivesModule.directive('codemirrorConsole', codemirrorConsole);
diff --git a/app/js/directives/console-summary.js b/app/js/directives/console-summary.js
deleted file mode 100644
index ae74750..0000000
--- a/app/js/directives/console-summary.js
+++ /dev/null
@@ -1,31 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-/**
- * @ngInject
- */
-function consoleSummary() {
-
- /**
- * @ngInject
- */
- var controller = function($scope, $attrs, datasetService) {
- $scope.$watch('artifactName', function(artifactName) {
- datasetService.artifact(artifactName, 'console').then(function(response) {
- $scope.console = response.data;
- });
- });
- };
-
- return {
- restrict: 'EA',
- scope: {
- 'artifactName': '='
- },
- controller: controller,
- templateUrl: 'directives/console-summary.html'
- };
-}
-
-directivesModule.directive('consoleSummary', consoleSummary);
diff --git a/app/js/directives/subunit-failures.js b/app/js/directives/subunit-failures.js
deleted file mode 100644
index f45a1f3..0000000
--- a/app/js/directives/subunit-failures.js
+++ /dev/null
@@ -1,31 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-/**
- * @ngInject
- */
-function subunitFailures() {
-
- /**
- * @ngInject
- */
- var controller = function($scope, $attrs, datasetService) {
- $scope.$watch('artifactName', function(artifactName) {
- datasetService.artifact(artifactName, 'subunit-stats').then(function(response) {
- $scope.stats = response.data;
- });
- });
- };
-
- return {
- restrict: 'EA',
- scope: {
- 'artifactName': '='
- },
- controller: controller,
- templateUrl: 'directives/subunit-failures.html'
- };
-}
-
-directivesModule.directive('subunitFailures', subunitFailures);
diff --git a/app/js/directives/subunit-summary.js b/app/js/directives/subunit-summary.js
deleted file mode 100644
index 26dd5cf..0000000
--- a/app/js/directives/subunit-summary.js
+++ /dev/null
@@ -1,36 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-/**
- * @ngInject
- */
-function subunitSummary() {
-
- /**
- * Responsible for getting the basic run summary stats via the dataset service.
- * Also calculates the duration of the run - `timeDiff` - by subtracting the
- * run's start and end timestamps.
- * @ngInject
- */
- var controller = function($scope, $attrs, datasetService) {
- $scope.$watch('artifactName', function(artifactName) {
- datasetService.artifact(artifactName, 'subunit-stats').then(function(response) {
- var stats = response.data;
- $scope.stats = stats;
- $scope.timeDiff = (new Date(stats.end) - new Date(stats.start)) / 1000;
- });
- });
- };
-
- return {
- restrict: 'EA',
- scope: {
- 'artifactName': '='
- },
- controller: controller,
- templateUrl: 'directives/subunit-summary.html'
- };
-}
-
-directivesModule.directive('subunitSummary', subunitSummary);
diff --git a/app/js/directives/test-details-search.js b/app/js/directives/test-details-search.js
deleted file mode 100644
index e27bea2..0000000
--- a/app/js/directives/test-details-search.js
+++ /dev/null
@@ -1,54 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-/**
- * @ngInject
- */
-function testDetailsSearch() {
-
- /**
- * @ngInject
- */
- var controller =
- /**
- * Responsible for calling the `parsePythonLogging` filter function in
- * `TestDetailsController` when the log level buttons change state. The
- * `filter` function is passed from `test-details` to `test-details-search`
- * when the directive is initially instantiated.
- */
- function($scope, $element) {
- var self = this;
- this.open = false;
- this.showINFO = true;
- this.showDEBUG = true;
- this.showWARNING = true;
- this.showERROR = true;
-
- // Wrapper for parent controller's filter function.
- var update = function() {
- $scope.filter(self.showINFO, self.showDEBUG, self.showWARNING, self.showERROR);
- };
-
- // Watchers to signal update function upon button state change.
- $scope.$watch(function() { return self.query; }, update);
- $scope.$watch(function() { return self.showINFO; }, update);
- $scope.$watch(function() { return self.showDEBUG; }, update);
- $scope.$watch(function() { return self.showWARNING; }, update);
- $scope.$watch(function() { return self.showERROR; }, update);
- };
-
- return {
- restrict: 'EA',
- require: ['^testDetailsSearch','^testDetails'],
- scope: {
- 'filter': '='
- },
- controller: controller,
- controllerAs: 'search',
- templateUrl: 'directives/test-details-search.html',
- transclude: true
- };
-}
-
-directivesModule.directive('testDetailsSearch', testDetailsSearch);
diff --git a/app/js/directives/timeline-details.js b/app/js/directives/timeline-details.js
deleted file mode 100644
index f8957e2..0000000
--- a/app/js/directives/timeline-details.js
+++ /dev/null
@@ -1,28 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-/**
- * @ngInject
- */
-function timelineDetails(AppSettings) {
-
- /**
- * @ngInject
- */
- var controller = function($scope) {
- $scope.healthRoot = AppSettings.healthRoot;
- };
-
- return {
- restrict: 'EA',
- scope: {
- 'artifactName': '=',
- 'item': '='
- },
- controller: controller,
- templateUrl: 'directives/timeline-details.html'
- };
-}
-
-directivesModule.directive('timelineDetails', timelineDetails);
diff --git a/app/js/directives/timeline-dstat.js b/app/js/directives/timeline-dstat.js
deleted file mode 100644
index 5dd3c88..0000000
--- a/app/js/directives/timeline-dstat.js
+++ /dev/null
@@ -1,450 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-var d3Ease = require('d3-ease');
-var d3Interpolate = require('d3-interpolate');
-var d3Scale = require('d3-scale');
-
-var arrayUtil = require('../util/array-util');
-var parseDstat = require('../util/dstat-parse');
-
-var getDstatLanes = function(data, mins, maxes) {
- if (!data || !data.length) {
- return [];
- }
-
- var row = data[0];
- var lanes = [];
-
- if ('total_cpu_usage_usr' in row && 'total_cpu_usage_sys' in row) {
- lanes.push([{
- scale: d3Scale.scaleLinear().domain([0, 100]),
- value: function(d) {
- return d.total_cpu_usage_wai;
- },
- color: "rgba(224, 188, 188, 1)",
- text: "CPU wait"
- }, {
- scale: d3Scale.scaleLinear().domain([0, 100]),
- value: function(d) {
- return d.total_cpu_usage_usr + d.total_cpu_usage_sys;
- },
- color: "rgba(102, 140, 178, 0.75)",
- text: "CPU (user+sys)"
- }]);
- }
-
- if ('memory_usage_used' in row) {
- lanes.push([{
- scale: d3Scale.scaleLinear().domain([0, maxes.memory_usage_used]),
- value: function(d) { return d.memory_usage_used; },
- color: "rgba(102, 140, 178, 0.75)",
- text: "Memory"
- }]);
- }
-
- if ('net_total_recv' in row && 'net_total_send' in row) {
- lanes.push([{
- scale: d3Scale.scaleLinear().domain([0, maxes.net_total_recv]),
- value: function(d) { return d.net_total_recv; },
- color: "rgba(224, 188, 188, 1)",
- text: "Net Down"
- }, {
- scale: d3Scale.scaleLinear().domain([0, maxes.net_total_send]),
- value: function(d) { return d.net_total_send; },
- color: "rgba(102, 140, 178, 0.75)",
- text: "Net Up",
- type: "line"
- }]);
- }
-
- if ('dsk_total_read' in row && 'dsk_total_writ' in row) {
- lanes.push([{
- scale: d3Scale.scaleLinear().domain([0, maxes.dsk_total_read]),
- value: function(d) { return d.dsk_total_read; },
- color: "rgba(224, 188, 188, 1)",
- text: "Disk Read",
- type: "line"
- }, {
- scale: d3Scale.scaleLinear().domain([0, maxes.dsk_total_writ]),
- value: function(d) { return d.dsk_total_writ; },
- color: "rgba(102, 140, 178, 0.75)",
- text: "Disk Write",
- type: "line"
- }]);
- }
-
- return lanes;
-};
-
-function timelineDstat($document, $window) {
- var link = function(scope, el, attrs, timelineController) {
- // local display variables
- var margin = timelineController.margin;
- var height = 140;
- var laneDefs = [];
- var laneHeight = 30;
- var loaded = false;
-
- // axes and dstat-global variables
- var absolute = timelineController.axes.absolute;
- var xSelected = timelineController.axes.selection;
- var y = d3Scale.scaleLinear();
-
- // animation variables
- var currentViewExtents = null;
- var viewInterpolator = null;
- var easeOutCubic = d3Ease.easeCubicOut;
- var easeStartTimestamp = null;
- var easeDuration = 500;
-
- // canvases and layers
- var regions = [];
- var lanes = timelineController.createCanvas(null, height);
- var main = timelineController.createCanvas(null, height, false);
- el.append(main.canvas);
-
- /**
- * Generate the list of active regions or "chunks". These regions span a
- * fixed area of the timeline's full "virtual" width, and contain only a
- * small subset of data points that fall within the area. This function only
- * initializes a list of regions, but does not actually attempt to draw
- * anything. Drawing can be handled lazily and will only occur when a
- * region's 'dirty' property is set. If a list of regions already exists,
- * it will be thrown away and replaced with a new list; this should occur
- * any time the full "virtual" timeline width changes (such as a extent
- * resize), or if the view extents no longer fall within the generated list
- * of regions.
- *
- * This function will limit the number of generated regions. If this is not
- * sufficient to cover the entire area spanned by the timeline's virtual
- * width, regions will be generated around the user's current viewport.
- *
- * Note that individual data points will exist within multiple regions if
- * they span region borders. In this case, each containing region will have
- * a unique rect instance pointing to the same data point.
- */
- function createRegions() {
- regions = [];
-
- var fullWidth = absolute(timelineController.timeExtents[1]);
- var chunkWidth = 500;
- var chunks = Math.ceil(fullWidth / chunkWidth);
- var offset = 0;
-
- // avoid creating lots of chunks - cap and only generate around the
- // current view
- // if we scroll out of bounds of the chunks we *do* have, we can throw
- // away our regions + purge regions in memory
- if (chunks > 30) {
- var startX = absolute(timelineController.viewExtents[0]);
- var endX = absolute(timelineController.viewExtents[1]);
- var midX = startX + (endX - startX) / 2;
-
- chunks = 50;
- offset = Math.max(0, midX - (chunkWidth * 15));
- }
-
- for (var i = 0; i < chunks; i++) {
- // for each desired chunk, find the bounds and managed data points
- // then, calculate positions for each data point
- var w = Math.min(fullWidth - offset, chunkWidth);
- var min = absolute.invert(offset);
- var max = absolute.invert(offset + w);
- var data = timelineController.dstatInBounds(min, max);
-
- regions.push({
- x: offset, width: w, min: min, max: max,
- data: data,
- c: null,
- dirty: true,
- index: regions.length
- });
-
- offset += w;
- }
- }
-
- /**
- * Finds all regions falling within the given minimum and maximum absolute
- * x coordinates.
- * @param {number} minX the minimum x coordinate (exclusive)
- * @param {number} maxX the maximum x coording (exclusive)
- * @return {object[]} a list of matching regions
- */
- function getContainedRegions(minX, maxX) {
- return regions.filter(function(region) {
- return (region.x + region.width) > minX && region.x < maxX;
- });
- }
-
- /**
- * Draw lane labels into the offscreen lanes canvas.
- */
- function drawLanes() {
- // make sure the canvas is the correct size and clear it
- lanes.resize(timelineController.width + margin.left + margin.right);
- lanes.ctx.clearRect(0, 0, lanes.canvas.width, lanes.canvas.height);
-
- lanes.ctx.strokeStyle = 'lightgray';
- lanes.ctx.textAlign = 'end';
- lanes.ctx.textBaseline = 'middle';
- lanes.ctx.font = '10px sans-serif';
-
- // draw lanes for each worker
- var laneHeight = 0.8 * y(1);
- for (var i = 0; i < laneDefs.length; i++) {
- var laneDef = laneDefs[i];
- var yPos = y(i + 0.5);
- var dy = 0;
-
- for (var pathIndex = 0; pathIndex < laneDef.length; pathIndex++) {
- var pathDef = laneDef[pathIndex];
- pathDef.scale.range([laneHeight, 0]);
-
- // draw labels right-aligned to the left of each lane
- if ('text' in pathDef) {
- lanes.ctx.fillStyle = pathDef.color;
- lanes.ctx.fillText(
- pathDef.text,
- margin.left - margin.right, yPos + dy,
- margin.left - 10);
-
- dy += 10;
- }
- }
- }
- }
-
- /**
- * Draw the given region into its own canvas. The region will only be drawn
- * if it is marked as dirty. If its canvas has not yet been created, it will
- * be initialized automatically. Note that this does not actually draw
- * anything to the screen (i.e. main canvas), as this result only populates
- * each region's local offscreen image with content. drawAll() will actually
- * draw to the screen (and implicitly calls this function as well).
- * @param {object} region the region to draw
- */
- function drawRegion(region) {
- if (!region.dirty) {
- // only redraw if dirty
- return;
- }
-
- if (!region.c) {
- // create the actual image buffer lazily - don't waste memory if it will
- // never be seen
- region.c = timelineController.createCanvas(region.width, height);
- }
-
- var ctx = region.c.ctx;
- ctx.clearRect(0, 0, region.width, height);
- ctx.strokeStyle = 'rgb(175, 175, 175)';
- ctx.lineWidth = 1;
-
- for (var laneIndex = 0; laneIndex < laneDefs.length; laneIndex++) {
- var laneDef = laneDefs[laneIndex];
- var bottom = y(laneIndex) + laneHeight;
-
- for (var pathIndex = 0; pathIndex < laneDef.length; pathIndex++) {
- if (!region.data.length) {
- continue;
- }
-
- var pathDef = laneDef[pathIndex];
- var line = pathDef.type === 'line';
-
- ctx.strokeStyle = pathDef.color;
- ctx.fillStyle = pathDef.color;
-
- var first = region.data[0];
- ctx.beginPath();
- ctx.moveTo(
- absolute(+first.system_time) - region.x,
- y(laneIndex) + pathDef.scale(pathDef.value(first)));
-
- for (var i = 1; i < region.data.length; i++) {
- var d = region.data[i];
-
- ctx.lineTo(
- absolute(+d.system_time) - region.x,
- y(laneIndex) + pathDef.scale(pathDef.value(d)));
- }
-
- if (line) {
- ctx.stroke();
- } else {
- var last = region.data[region.data.length - 1];
- ctx.lineTo(absolute(+last.system_time) - region.x, bottom);
- ctx.lineTo(absolute(+first.system_time) - region.x, bottom);
- ctx.fill();
- }
- }
- }
-
- region.dirty = false;
- }
-
- /**
- * Draw all layers and visible regions on the screen.
- */
- function drawAll() {
- if (!currentViewExtents) {
- currentViewExtents = timelineController.viewExtents;
- }
-
- // update size of main canvas
- var w = timelineController.width + margin.left + margin.right;
- var e = angular.element(main.canvas);
- main.resize(w);
-
- var s = function(v) {
- return v * main.ratio;
- };
-
- main.ctx.clearRect(0, 0, main.canvas.width, main.canvas.height);
- main.ctx.drawImage(lanes.canvas, 0, 0);
-
- // draw all visible regions
- var startX = absolute(currentViewExtents[0]);
- var endX = absolute(currentViewExtents[1]);
- var viewRegions = getContainedRegions(startX, endX);
-
- var effectiveWidth = 0;
- viewRegions.forEach(function(region) {
- effectiveWidth += region.width;
- });
-
- if (effectiveWidth < timelineController.width) {
- // we had to cap the region generation previously, but moved outside of
- // the generated area, so regenerate regions around the current view
- createRegions();
- viewRegions = getContainedRegions(startX, endX);
- }
-
- viewRegions.forEach(function(region) {
- drawRegion(region);
-
- // calculate the cropping area and offsets needed to place the region
- // in the main canvas
- var sx1 = Math.max(0, startX - region.x);
- var sx2 = Math.min(region.width, endX - region.x);
- var sw = sx2 - sx1;
- var dx = Math.max(0, startX - region.x);
- if (Math.floor(sw) === 0) {
- return;
- }
-
- main.ctx.drawImage(
- region.c.canvas,
- s(sx1), 0, Math.floor(s(sw)), s(height),
- s(margin.left + region.x - startX + sx1), 0, s(sw), s(height));
- });
- }
-
- timelineController.animateCallbacks.push(function(timestamp) {
- if (!loaded) {
- return false;
- }
-
- if (viewInterpolator) {
- // start the animation
- var currentSize = currentViewExtents[1] - currentViewExtents[0];
- var newSize = timelineController.viewExtents[1] - timelineController.viewExtents[0];
- var diffSize = currentSize - newSize;
- var diffTime = timestamp - easeStartTimestamp;
- var pct = diffTime / easeDuration;
-
- // interpolate the current view bounds according to the easing method
- currentViewExtents = viewInterpolator(easeOutCubic(pct));
-
- if (Math.abs(diffSize) > 1) {
- // size has changed, recalculate regions
- createRegions();
- }
-
- drawAll();
-
- if (pct >= 1) {
- // finished, clear the state vars
- easeStartTimestamp = null;
- viewInterpolator = null;
- return false;
- } else {
- // request more frames until finished
- return true;
- }
- } else {
- // if there is no view interpolator function, just do a plain redraw
- drawAll();
- return false;
- }
- });
-
- scope.$on('dstatLoaded', function(event, dstat) {
- laneDefs = getDstatLanes(dstat.entries, dstat.minimums, dstat.maximums);
- laneHeight = height / (laneDefs.length + 1);
- y.domain([0, laneDefs.length]).range([0, height]);
- drawLanes();
- createRegions();
-
- loaded = true;
- });
-
- scope.$on('update', function() {
- if (!loaded) {
- return;
- }
-
- drawLanes();
- createRegions();
- timelineController.animate();
- });
-
- scope.$on('updateViewSize', function() {
- if (!loaded) {
- return;
- }
-
- if (currentViewExtents) {
- // if we know where the view is already, try to animate the transition
- viewInterpolator = d3Interpolate.interpolateArray(
- currentViewExtents,
- timelineController.viewExtents);
- easeStartTimestamp = performance.now();
- } else {
- // otherwise, move directly to the new location/size (we will need to
- // rebuild regions)
- createRegions();
- }
-
- timelineController.animate();
- });
-
- scope.$on('updateViewPosition', function() {
- if (!loaded) {
- return;
- }
-
- if (currentViewExtents) {
- // if we know where the view is already, try to animate the transition
- viewInterpolator = d3Interpolate.interpolateArray(
- currentViewExtents,
- timelineController.viewExtents);
- easeStartTimestamp = performance.now();
- }
-
- timelineController.animate();
- });
- };
-
- return {
- restrict: 'E',
- require: '^timeline',
- scope: true,
- link: link
- };
-}
-
-directivesModule.directive('timelineDstat', timelineDstat);
diff --git a/app/js/directives/timeline-overview.js b/app/js/directives/timeline-overview.js
deleted file mode 100644
index 8d14671..0000000
--- a/app/js/directives/timeline-overview.js
+++ /dev/null
@@ -1,495 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-var d3Scale = require('d3-scale');
-
-function timelineOverview($document, $window) {
- var link = function(scope, el, attrs, timelineController) {
- // local display variables
- var margin = timelineController.margin;
- var height = 80;
- var laneHeight = 10;
- var loaded = false;
-
- // scales and extents
- var x = timelineController.axes.x;
- var y = d3Scale.scaleLinear();
- var brushExtent = [0, 0];
- var handleSize = 3;
-
- // input variables
- var dragOffsetStart = null;
- var dragType = null; // left, right, position, null
-
- var rects = [];
- var lanes = timelineController.createCanvas(timelineController.width, height);
- var main = timelineController.createCanvas(null, height, false);
- main.canvas.unselectable = 'on';
- main.canvas.onselectstart = function() { return false; };
- main.canvas.style.userSelect = 'none';
- el.append(main.canvas);
-
- /**
- * Centers the viewport on a given date. If the date is not within the
- * bounds of the data, no changes are made and false is returned.
- * @param {Date} date the date to center on
- * @return {boolean} true if the view was centered, false if not
- */
- function centerViewport(date) {
- // explicitly center the viewport on a date
- var timeExtents = timelineController.timeExtents;
- var start = timeExtents[0];
- var end = timeExtents[1];
-
- if (date < start || date > end) {
- return false;
- }
-
- var viewExtents = timelineController.viewExtents;
- var size = viewExtents[1] - viewExtents[0];
-
- var targetStart = math.max(start.getTime(), date - (size / 2));
- targetStart = Math.min(targetStart, end.getTime() - size);
- var targetEnd = begin + extentSize;
-
- brushExtent = [targetStart, targetEnd];
- timelineController.setViewExtents(brushExtent);
- timelineController.animate();
-
- return true;
- }
-
- /**
- * Shift the viewport left or right to fit a data rect. If the item already
- * fits inside the current view bounds, no changes are made and false is
- * returned. If not, the view will shift as much as is needed to fit the
- * item fully into view.
- * @param {object} item the item to fit into the viewport
- * @return {boolean} true if the view was moved, false if not
- */
- function shiftViewport(item) {
- var timeExtents = timelineController.timeExtents;
- var start = timeExtents[0];
- var end = timeExtents[1];
-
- var viewExtents = timelineController.viewExtents;
- var viewStart = viewExtents[0];
- var viewEnd = viewExtents[1];
- if (item.startDate >= viewStart && item.endDate <= viewEnd) {
- return false;
- }
-
- var size = viewEnd - viewStart;
- var currentMid = (+viewStart) + (size / 2);
- var targetMid = item.startDate.getTime() + (item.endDate - item.startDate) / 2;
-
- var targetStart, targetEnd;
- if (targetMid > currentMid) {
- // move right - anchor item end to view right
- targetEnd = item.endDate.getTime();
- targetStart = Math.max(start.getTime(), targetEnd - size);
- } else if (targetMid < currentMid) {
- // move left - anchor item start to view left
- targetStart = item.startDate.getTime();
- targetEnd = Math.min(end.getTime(), targetStart + size);
- } else {
- return false;
- }
-
- brushExtent = [targetStart, targetEnd];
- timelineController.setViewExtents(brushExtent);
- timelineController.animate();
-
- return true;
- }
-
- /**
- * Creates rects from a list of data points, placing them along the primary
- * x axis and within their appropriate lanes.
- * @param {object[]]} data a list of data points.
- * @return {object[]]} a list of rects
- */
- function createRects(data) {
- var rects = [];
-
- for (var i = 0; i < data.length; i++) {
- var d = data[i];
- rects.push({
- x: x(d.startDate),
- y: y(d.worker + 0.5) - 5,
- width: x(d.endDate) - x(d.startDate),
- height: laneHeight,
- entry: d
- });
- }
-
- return rects;
- }
-
- /**
- * Draws a single rect to the off-screen lanes canvas. By default, this does
- * not clear any part of the canvas; however, if `clear` is set to `true`,
- * the area for the rect will be cleared before drawing.
- * @param {object} rect the rect to draw
- * @param {boolean} clear if true, clear the area first
- */
- function drawSingleRect(rect, clear) {
- var ctx = lanes.ctx;
- ctx.fillStyle = timelineController.statusColorMap[rect.entry.status];
- ctx.strokeStyle = 'rgb(200, 200, 200)';
-
- if (clear) {
- ctx.clearRect(rect.x, rect.y, rect.width, rect.height);
- }
-
- ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
- ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
- }
-
- /**
- * Draws all rects into the off-screen lanes canvas. This will fully clear
- * the canvas before drawing any rects. To redraw only a single rect,
- * `drawSingleRect()` can be used instead.
- */
- function drawRects() {
- lanes.resize(timelineController.width);
- lanes.ctx.clearRect(0, 0, lanes.canvas.width, lanes.canvas.height);
-
- for (var i = 0; i < rects.length; i++) {
- drawSingleRect(rects[i]);
- }
- }
-
- /**
- * Draws the brush onto the main (on-screen) canvas. The relevant canvas
- * area should already be cleared and should only contain a rendered lanes
- * image.
- */
- function drawBrush() {
- var r = main.ratio;
- var ctx = main.ctx;
- ctx.fillStyle = 'dodgerblue';
- ctx.globalAlpha = 0.365;
-
- var brushX = (r * margin.left) + (r * x(brushExtent[0]));
- var brushWidth = r * (x(brushExtent[1]) - x(brushExtent[0]));
-
- ctx.fillRect(brushX, 0, brushWidth, main.canvas.height);
- ctx.globalAlpha = 1.0;
- }
-
- /**
- * Draws the pre-rendered lanes image and brush onto the main canvas. This
- * is suitable for calling on every frame for a normal update, but it will
- * not update any rects in the lanes image. If this is needed, `drawRects()`
- * should be called first.
- */
- function drawAll() {
- var r = main.ratio;
- var w = timelineController.width + margin.left + margin.right;
- main.resize(w);
-
- main.ctx.clearRect(0, 0, main.canvas.width, main.canvas.height);
- main.ctx.drawImage(lanes.canvas, r * margin.left, 0);
-
- drawBrush();
- }
-
- timelineController.animateCallbacks.push(function() {
- drawAll();
- });
-
- /**
- * Gets the canvas-local mouse point for the given mouse event, accounting
- * for all relevant offsets and margins. The returned object will include an
- * additional `inBounds` property indicating whether or not the point falls
- * within the bounds of the overview canvas.
- * @param {MouseEvent} evt the mouse event
- * @return {object} a point object
- */
- function getMousePoint(evt) {
- var r = main.canvas.getBoundingClientRect();
- var ret = {
- xRaw: evt.clientX - r.left,
- x: evt.clientX - r.left - margin.left,
- y: evt.clientY - r.top,
- radius: evt.radiusX || (3 * main.ratio)
- };
-
- ret.inBounds = ret.x > 0 &&
- ret.x < timelineController.width &&
- ret.y > 0 && ret.y < height;
-
- return ret;
- }
-
- /**
- * Returns true if the given `point` falls within `size` pixels of the given
- * x coordinate. The pixel size is automatically computed from the touch
- * radius (if available) or a reasonable value, scaled based on the current
- * device pixel ratio.
- * @param {object} point the point to check against
- * @param {number} x the x coordinate
- * @return {boolean} true if the point is in bounds, false otherwise
- */
- function withinPx(point, x) {
- return point.inBounds && Math.abs(x - point.x) <= point.radius;
- }
-
- /**
- * Flips the given drag type string: "left" becomes "right" and "right"
- * becomes "left". The input string is returned if it isn't either "left"
- * or "right".
- * @param {string} side the drag type string to flip
- * @return {string} the opposite value, or the input if invalid
- */
- function flip(side) {
- if (side === 'left') {
- return 'right';
- } else if (side === 'right') {
- return 'left';
- } else {
- return side;
- }
- }
-
- /**
- * Returns the closest matching extent satisfying the desired left and right
- * pixel values within the timeline's overall time extents. If the given
- * type is "position", this will attempt to preserve the size of the extent
- * by adjusting the opposite value to fit when an edge is reached;
- * otherwise, the extent may have one value capped at the timeline's minium
- * or maximum edges. The returned object will contain the resulting drag
- * type (as it may have been flipped) as well as the computed valid extents
- * in an array.
- *
- * Note that order of the left and right parameters does not technically
- * matter, as they will be flipped automatically if necessary.
- * @param {number} desiredLeft the preferred left end of the extent
- * @param {number} desiredRight the preferred right end of the extent
- * @param {string} type the drag type, e.g. "left" or "position"
- * @return {object} an object with the new drag type and the
- * computed extents array
- */
- function smartExtent(desiredLeft, desiredRight, type) {
- desiredLeft = x.invert(desiredLeft);
- desiredRight = x.invert(desiredRight);
- if (desiredLeft > desiredRight) {
- type = flip(type);
- }
- var l = Math.min(desiredLeft, desiredRight);
- var r = Math.max(desiredLeft, desiredRight);
-
- if (type === 'position') {
- // plain translation, don't allow size to change if possible
- var size = r - l;
- if (l < timelineController.timeExtents[0]) {
- l = +timelineController.timeExtents[0];
- r = Math.min(+timelineController.timeExtents[1], l + size);
- } else if (r > timelineController.timeExtents[1]) {
- r = +timelineController.timeExtents[1];
- l = Math.max(+timelineController.timeExtents[0], r - size);
- }
- } else {
- // cap at left and right time extents
- if (l < timelineController.timeExtents[0]) {
- l = timelineController.timeExtents[0];
- }
-
- if (r > timelineController.timeExtents[1]) {
- r = timelineController.timeExtents[1];
- }
- }
-
- return { extent: [l, r], type: type };
- }
-
- /**
- * Handles a mouse press on the canvas at the given point. If the point is
- * within range of a handle (either left or right), it will begin a drag
- * operation for that side. If the click is otherwise within the existing
- * selection, a position drag will be started. Otherwise, the click will
- * start a new selection at the current position with a left drag.
- *
- * Note that this function should only be called for element-level events,
- * and not window-level events.
- * @param {object} p the mouse point
- */
- function handleMouseDown(p) {
- var brushLeft = x(brushExtent[0]);
- var brushRight = x(brushExtent[1]);
-
- if (withinPx(p, brushLeft)) {
- dragType = 'left';
- } else if (withinPx(p, brushRight)) {
- dragType = 'right';
- } else if (p.x > brushLeft && p.x < brushRight) {
- dragType = 'position';
- dragOffsetStart = p.x - brushLeft;
- } else {
- // start a new selection
- brushExtent = [x.invert(p.x), x.invert(p.x)];
- dragType = 'left';
- timelineController.animate();
- }
- }
-
- /**
- * Handles a mouse move at the given point. If a drag is in progress, this
- * will perform the necessary resizing of the brush and start an animate
- * task. If no drag is in process, the mouse cursor will be updated as
- * necessary.
- *
- * Note that this function should be used to handle all mouse events at the
- * window level so that dragging doesn't need to occur strictly in bounds
- * of the canvas element. In most browsers, window-level events will even
- * allow the drag to continue when the mouse leaves the browser window
- * entirely.
- * @param {object} p the mouse point
- * @return {boolean} true if the triggering event's preventDefault() should
- * be called, false otherwise
- */
- function handleMouseMove(p) {
- var brushLeft = x(brushExtent[0]);
- var brushRight = x(brushExtent[1]);
- var e;
-
- if (dragType !== null) {
- // handle the drag
- if (dragType === 'left') {
- e = smartExtent(p.x, brushRight, dragType);
- dragType = e.type;
- brushExtent = e.extent;
- } else if (dragType === 'right') {
- e = smartExtent(brushLeft, p.x, dragType);
- dragType = e.type;
- brushExtent = e.extent;
- } else {
- var size = brushRight - brushLeft;
- var left = p.x - dragOffsetStart;
-
- brushExtent = smartExtent(left, left + size, dragType).extent;
- }
-
- timelineController.setViewExtents(brushExtent);
- timelineController.animate();
- return false;
- } else {
- // just update the cursor as needed - show drag arrows over left & right
- // brush edges
- if (withinPx(p, brushLeft)) {
- main.canvas.style.cursor = 'ew-resize';
- } else if (withinPx(p, brushRight)) {
- main.canvas.style.cursor = 'ew-resize';
- } else if (p.inBounds && p.x > brushLeft && p.x < brushRight) {
- main.canvas.style.cursor = 'move';
- } else {
- main.canvas.style.cursor = 'default';
- }
- }
-
- return true;
- }
-
- /**
- * Handles a mouse up event.
- *
- * This should handle all events at the window level so that drags that
- * don't complete strictly within the window are ended properly. Most
- * browsers will allow window-level mouseup events to trigger for drags even
- * if the cursor is outside of the window entirely.
- */
- function handleMouseUp() {
- dragType = null;
- dragOffsetStart = null;
- main.canvas.style.cursor = 'default';
- }
-
- main.canvas.addEventListener('mousedown', function(evt) {
- // listen on the actual element so we only get element events
- evt.preventDefault();
- handleMouseDown(getMousePoint(evt));
- });
-
- main.canvas.addEventListener('touchstart', function(evt) {
- evt.preventDefault();
- for (var i = 0; i < evt.changedTouches.length; i++) {
- var touch = evt.changedTouches[i];
- handleMouseDown(getMousePoint(touch));
- }
- });
-
- $window.addEventListener('mousemove', function(evt) {
- // listen on the window - this lets us get drag events for the whole page
- // and (depending on browser) even outside of the window
- if (!handleMouseMove(getMousePoint(evt))) {
- evt.preventDefault();
- }
- });
-
- $window.addEventListener('touchmove', function(evt) {
- for (var i = 0; i < evt.changedTouches.length; i++) {
- var touch = evt.changedTouches[i];
- if (!handleMouseMove(getMousePoint(touch))) {
- evt.preventDefault();
- return;
- }
- }
- });
-
- $window.addEventListener('mouseup', handleMouseUp);
- $window.addEventListener('touchend', handleMouseUp);
- $window.addEventListener('touchcancel', handleMouseUp);
-
- scope.$on('dataLoaded', function(event, data) {
- laneHeight = height / (data.length + 1);
- y.domain([0, data.length]).range([0, height]);
- rects = createRects(timelineController.dataRaw);
-
- var timeExtents = timelineController.timeExtents;
- var start = timeExtents[0];
- var end = timeExtents[1];
- var reducedEnd = new Date(start.getTime() + (end - start) / 8);
-
- brushExtent = [start, reducedEnd];
- timelineController.setViewExtents(brushExtent);
-
- loaded = true;
-
- drawRects();
- });
-
- scope.$on('update', function() {
- rects = createRects(timelineController.dataRaw);
- drawRects();
- timelineController.animate();
- });
-
- scope.$on('updateView', function() {
- brushExtent = timelineController.viewExtents;
- timelineController.animate();
- });
-
- scope.$on('select', function(event, selection) {
- if (selection) {
- shiftViewport(selection.item);
- }
- });
-
- scope.$on('filter', function() {
- if (loaded) {
- drawRects();
- }
- });
- };
-
- return {
- restrict: 'E',
- require: '^timeline',
- scope: true,
- link: link
- };
-}
-
-directivesModule.directive('timelineOverview', timelineOverview);
diff --git a/app/js/directives/timeline-search.js b/app/js/directives/timeline-search.js
deleted file mode 100644
index e60d748..0000000
--- a/app/js/directives/timeline-search.js
+++ /dev/null
@@ -1,98 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-/**
- * @ngInject
- */
-function timelineSearch() {
-
- /**
- * @ngInject
- */
- var controller = function($scope, $element) {
- var self = this;
-
- this.open = false;
- this.query = '';
- this.showSuccess = true;
- this.showSkip = true;
- this.showFail = true;
-
- this.results = [];
-
- var doFilter = function(item) {
- if ((item.status === 'success' && !self.showSuccess) ||
- (item.status === 'skip' && !self.showSkip) ||
- (item.status === 'fail' && !self.showFail)) {
- return false;
- }
-
- if (item.name.toLowerCase().indexOf(self.query.toLowerCase()) < 0) {
- return false;
- }
-
- return true;
- };
-
- this.updateResults = function() {
- var timeline = $element.controller('timeline');
- timeline.setFilterFunction(function(item) {
- return doFilter(item);
- });
-
- var ret = [];
- for (var i = 0; i < timeline.dataRaw.length; i++) {
- var item = timeline.dataRaw[i];
-
- if (!doFilter(item)) {
- continue;
- }
-
- ret.push(timeline.dataRaw[i]);
- if (ret.length > 25) {
- break;
- }
- }
-
- this.results = ret;
- };
-
- this.select = function(item) {
- var timeline = $element.controller('timeline');
- timeline.selectItem(item);
- timeline.setFilterFunction(null);
-
- self.query = '';
- self.open = false;
- };
-
- var update = function(a, b) {
- if (a === b) {
- return;
- }
-
- self.updateResults();
- };
-
- $scope.$watch(function() { return self.query; }, update);
- $scope.$watch(function() { return self.showSuccess; }, update);
- $scope.$watch(function() { return self.showSkip; }, update);
- $scope.$watch(function() { return self.showFail; }, update);
-
- $scope.$on('dataLoaded', function() {
- self.updateResults();
- });
- };
-
- return {
- restrict: 'EA',
- require: ['^timelineSearch', '^timeline'],
- scope: true,
- controller: controller,
- controllerAs: 'search',
- templateUrl: 'directives/timeline-search.html'
- };
-}
-
-directivesModule.directive('timelineSearch', timelineSearch);
diff --git a/app/js/directives/timeline-viewport.js b/app/js/directives/timeline-viewport.js
deleted file mode 100644
index 7e91e9c..0000000
--- a/app/js/directives/timeline-viewport.js
+++ /dev/null
@@ -1,686 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-var arrayUtil = require('../util/array-util');
-
-var d3Ease = require('d3-ease');
-var d3Interpolate = require('d3-interpolate');
-var d3Scale = require('d3-scale');
-var d3TimeFormat = require('d3-time-format');
-
-/**
- * @ngInject
- */
-function timelineViewport($document, $window) {
- var link = function(scope, el, attrs, timelineController) {
- // local display variables
- var margin = timelineController.margin;
- var statusColorMap = timelineController.statusColorMap;
- var height = 200;
- var loaded = false;
-
- // axes and timeline-global variables
- var y = d3Scale.scaleLinear();
- var absolute = timelineController.axes.absolute;
- var xSelected = timelineController.axes.selection;
- var cursorTimeFormat = d3TimeFormat.timeFormat('%X');
- var tickFormat = timelineController.axes.x.tickFormat();
-
- // animation variables
- var currentViewExtents = null;
- var viewInterpolator = null;
- var easeOutCubic = d3Ease.easeCubicOut;
- var easeStartTimestamp = null;
- var easeDuration = 500;
-
- // selection and hover variables
- var mousePoint = null;
- var selection = null;
- var hover = null;
-
- // canvases and layers
- var lanes = timelineController.createCanvas();
- var regions = [];
- var cursor = timelineController.createCanvas();
- var main = timelineController.createCanvas(null, null, false);
- el.append(main.canvas);
-
- /**
- * Initializes rects from a list of parsed subunit log entries, setting
- * initial sizes and positions based on the current view extents.
- * @param {Array} data A list of parsed subunit log entries
- */
- function createRects(data) {
- var rects = [];
-
- var h = 0.8 * y(1);
- for (var i = 0; i < data.length; i++) {
- var entry = data[i];
- var start = absolute(+entry.startDate);
- rects.push({
- x: start,
- y: y(entry.worker),
- width: absolute(+entry.endDate) - start,
- height: h,
- entry: entry
- });
- }
-
- return rects;
- }
-
- /**
- * Generate the list of active regions or "chunks". These regions span a
- * fixed area of the timeline's full "virtual" width, and contain only a
- * small subset of data points that fall within the area. This function only
- * initializes a list of regions, but does not actually attempt to draw
- * anything. Drawing can be handled lazily and will only occur when a
- * region's 'dirty' property is set. If a list of regions already exists,
- * it will be thrown away and replaced with a new list; this should occur
- * any time the full "virtual" timeline width changes (such as a extent
- * resize), or if the view extents no longer fall within the generated list
- * of regions.
- *
- * This function will limit the number of generated regions. If this is not
- * sufficient to cover the entire area spanned by the timeline's virtual
- * width, regions will be generated around the user's current viewport.
- *
- * Note that individual data points will exist within multiple regions if
- * they span region borders. In this case, each containing region will have
- * a unique rect instance pointing to the same data point.
- */
- function createRegions() {
- regions = [];
-
- var fullWidth = absolute(timelineController.timeExtents[1]);
- var chunkWidth = 500;
- var chunks = Math.ceil(fullWidth / chunkWidth);
- var offset = 0;
-
- // avoid creating lots of chunks - cap and only generate around the
- // current view
- // if we scroll out of bounds of the chunks we *do* have, we can throw
- // away our regions + purge regions in memory
- if (chunks > 30) {
- var startX = absolute(timelineController.viewExtents[0]);
- var endX = absolute(timelineController.viewExtents[1]);
- var midX = startX + (endX - startX) / 2;
-
- chunks = 50;
- offset = Math.max(0, midX - (chunkWidth * 15));
- }
-
- for (var i = 0; i < chunks; i++) {
- // for each desired chunk, find the bounds and managed data points
- // then, calculate positions for each data point
- var w = Math.min(fullWidth - offset, chunkWidth);
- var min = absolute.invert(offset);
- var max = absolute.invert(offset + w);
- var data = timelineController.dataInBounds(min, max);
- var rects = createRects(data);
-
- regions.push({
- x: offset, width: w, min: min, max: max,
- data: data, rects: rects,
- c: null,
- dirty: true,
- index: regions.length
- });
-
- offset += w;
- }
- }
-
- /**
- * Marks all regions as dirty so they can be redrawn for the next frame.
- */
- function markAllDirty() {
- regions.forEach(function(region) {
- region.dirty = true;
- });
- }
-
- /**
- * Finds all regions falling within the given minimum and maximum absolute
- * x coordinates.
- * @param {number} minX the minimum x coordinate (exclusive)
- * @param {number} maxX the maximum x coording (exclusive)
- * @return {object[]} a list of matching regions
- */
- function getContainedRegions(minX, maxX) {
- return regions.filter(function(region) {
- return (region.x + region.width) > minX && region.x < maxX;
- });
- }
-
- /**
- * Get all regions containing the given data point.
- * @param {object} entry the datapoint
- * @return {object[]} a list of regions containing this entry
- */
- function getRegionsForEntry(entry) {
- var min = absolute(entry.startDate);
- var max = absolute(entry.endDate);
- return getContainedRegions(min, max);
- }
-
- /**
- * Get the rect corresponding to the given entry within a particular region.
- * @param {object} region the region to search in
- * @param {object} entry the entry to search for
- * @return {object|null} the matching rect, if any
- */
- function getRectForEntry(region, entry) {
- return region.rects.find(function(r) {
- return r.entry === entry;
- });
- }
-
- /**
- * Find the region managing the given canvas-local X coordinate. If the x
- * value is outside of the actual canvas area where regions are rendered,
- * this may return null. Rarely, null may also be returned if, while
- * animating, the view moves to a position outside of capped view bounds
- * (i.e. when the view extents are small); if this happens, it can be
- * ignored and createRegions() will generate the necessary area when the
- * animation finishes.
- * @param {number} screenX the canvas-local x coordinate
- * @return {object|null} the matching region or null
- */
- function getRegionAt(screenX) {
- if (screenX < margin.left || screenX > main.canvas.width - margin.right) {
- return null;
- }
- var absX = absolute(currentViewExtents[0]) + (screenX - margin.left);
- return regions.find(function(r) {
- return absX >= r.x && absX <= (r.x + r.width);
- });
- }
-
- /**
- * Get the rect at the given canvas-local coordinates, if any exists. This
- * function has the same limitations as getRegionAt() and will return null
- * when the coordinates are out of bounds or rarely when animating, but also
- * returns null when no rect exists at the given coords.
- *
- * Note that this will only search within the region containing the
- * coordinates so it should be fairly performant, though it will only return
- * one of possibly many matching rects.
- * @param {number} screenX the canvas-local x coordinate
- * @param {number} screenY the canvas-local y coordinate
- * @return {object|null} the matching rect in the region or null
- */
- function getRectAt(screenX, screenY) {
- if (screenY < margin.top || screenY > main.canvas.height - margin.bottom) {
- return null;
- }
-
- var region = getRegionAt(screenX);
- if (!region) {
- return null;
- }
-
- // find the absolute coords in rect-space
- var absX = absolute(currentViewExtents[0]) + (screenX - margin.left);
- var absY = screenY - margin.top;
-
- for (var i = 0; i < region.rects.length; i++) {
- var rect = region.rects[i];
-
- if (absX >= rect.x && absX <= (rect.x + rect.width) &&
- absY >= rect.y && absY <= (rect.y + rect.height)) {
- // make sure the point is contained inside the rect
- return rect;
- }
- }
-
- return null;
- }
-
- /**
- * Draw lane lines and their labels into the offscreen lanes canvas.
- */
- function drawLanes() {
- // make sure the canvas is the correct size and clear it
- lanes.resize(timelineController.width + margin.left + margin.right);
- lanes.ctx.clearRect(0, 0, lanes.canvas.width, lanes.canvas.height);
-
- lanes.ctx.strokeStyle = 'lightgray';
- lanes.ctx.textBaseline = 'middle';
- lanes.ctx.font = '14px Arial';
-
- // draw lanes for each worker
- var laneHeight = y(1);
- for (var worker = 0; worker < timelineController.data.length; worker++) {
- var yPos = margin.top + y(worker - 0.1);
-
- // draw horizontal lines between lanes
- lanes.ctx.beginPath();
- lanes.ctx.moveTo(margin.left, yPos);
- lanes.ctx.lineTo(margin.left + timelineController.width, yPos);
- lanes.ctx.stroke();
-
- // draw labels middle-aligned to the left of each lane
- lanes.ctx.fillText(
- 'Worker #' + worker,
- 5, yPos + (laneHeight / 2),
- margin.left - 10);
- }
- }
-
- /**
- * Draw a single rect within a region. This may be called independently of
- * drawRegion() to update only a single rect, if needed.
- * @param {object} region the region to draw within
- * @param {object} rect the rect to draw
- * @param {boolean} [clear] if true, clear the rect first (default: false)
- */
- function drawSingleRect(region, rect, clear) {
- var ctx = region.c.ctx;
-
- if (rect.entry === selection) {
- ctx.fillStyle = statusColorMap.selected;
- } else if (rect.entry === hover) {
- ctx.fillStyle = statusColorMap.hover;
- } else {
- ctx.fillStyle = statusColorMap[rect.entry.status];
- }
-
- if (clear) {
- ctx.clearRect(rect.x - region.x, rect.y, rect.width, rect.height);
- }
-
- var filter = timelineController.filterFunction;
- if (!filter || filter(rect.entry)) {
- ctx.globalAlpha = 1.0;
- } else {
- ctx.globalAlpha = 0.15;
- }
-
- ctx.fillRect(rect.x - region.x, rect.y, rect.width, rect.height);
- ctx.strokeRect(rect.x - region.x, rect.y, rect.width, rect.height);
- }
-
- /**
- * Redraw all matching rects among all regions that contain this entry.
- * @param {object} entry the entry to redraw
- */
- function drawAllForEntry(entry) {
- getRegionsForEntry(entry).forEach(function(region) {
- if (!region.c) {
- return;
- }
-
- var r = getRectForEntry(region, entry);
- if (r) {
- drawSingleRect(region, r, true);
- }
- });
- }
-
- /**
- * Draw the given region into its own canvas. The region will only be drawn
- * if it is marked as dirty. If its canvas has not yet been created, it will
- * be initialized automatically. Note that this does not actually draw
- * anything to the screen (i.e. main canvas), as this result only populates
- * each region's local offscreen image with content. drawAll() will actually
- * draw to the screen (and implicitly call this function as well.
- * @param {object} region the region to draw
- */
- function drawRegion(region) {
- if (!region.dirty) {
- // only redraw if dirty
- return;
- }
-
- if (!region.c) {
- // create the actual image buffer lazily - don't waste memory if it will
- // never be seen
- region.c = timelineController.createCanvas(
- region.width, height + margin.bottom);
- }
-
- var ctx = region.c.ctx;
- ctx.clearRect(0, 0, region.width, height);
- ctx.strokeStyle = 'rgb(175, 175, 175)';
- ctx.lineWidth = 1;
-
- for (var i = 0; i < region.rects.length; i++) {
- var rect = region.rects[i];
- drawSingleRect(region, rect);
- }
-
- // draw axis ticks + labels
- // main axis line -- offset y by 0.5 to draw crisp lines
- ctx.strokeStyle = 'lightgray';
- ctx.fillStyle = '#888';
- ctx.font = '9px sans-serif';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- ctx.beginPath();
- ctx.moveTo(0, height + 0.5);
- ctx.lineTo(region.width, height + 0.5);
- ctx.stroke();
-
- // make a scale for the position of this region, but shrink it slightly so
- // no labels overlap region boundaries and get cut off
- var tickScale = d3Scale.scaleTime().domain([
- absolute.invert(region.x + 10),
- absolute.invert(region.x + region.width - 10)
- ]);
-
- // 1 tick per 125px
- var ticks = tickScale.ticks(Math.floor(region.width / 125));
-
- for (var tickIndex = 0; tickIndex < ticks.length; tickIndex++) {
- var tick = ticks[tickIndex];
- var tickX = Math.floor(absolute(tick) - region.x) + 0.5;
-
- ctx.beginPath();
- ctx.moveTo(tickX, height);
- ctx.lineTo(tickX, height + 6);
- ctx.stroke();
-
- ctx.fillText(tickFormat(tick), tickX, height + 7);
- }
-
- ctx.strokeStyle = 'rgb(175, 175, 175)';
- region.dirty = false;
- }
-
- function drawCursor() {
- if (!mousePoint || !mousePoint.inBounds) {
- return;
- }
-
- var r = main.ratio;
- var ctx = main.ctx;
- ctx.scale(main.ratio, main.ratio);
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- ctx.fillStyle = 'dimgrey';
- ctx.strokeStyle = 'blue';
-
- // draw the cursor line
- ctx.beginPath();
- ctx.moveTo(mousePoint.x, margin.top);
- ctx.lineTo(mousePoint.x, margin.top + height);
- ctx.stroke();
-
- // draw the time label
- ctx.font = '9px sans-serif';
- var date = new Date(xSelected.invert(mousePoint.x - margin.left));
- ctx.fillText(cursorTimeFormat(date), mousePoint.x, 16);
-
- // draw the hovered item info
- if (hover) {
- var leftEdge = margin.left;
- var rightEdge = leftEdge + timelineController.width;
-
- ctx.font = 'bold 12px sans-serif';
- var name = hover.name.split('.').pop();
- var tw = ctx.measureText(name).width;
-
- var cx = mousePoint.x;
- if (mousePoint.x + (tw / 2) > rightEdge) {
- cx -= mousePoint.x - (rightEdge - tw / 2);
- } else if (mousePoint.x - (tw / 2) < leftEdge) {
- cx += (leftEdge + tw / 2) - mousePoint.x;
- }
- ctx.fillText(name, cx, 1);
- }
-
- // reset scale
- ctx.setTransform(1, 0, 0, 1, 0, 0);
- }
-
- /**
- * Draw all layers and visible regions on the screen.
- */
- function drawAll() {
- // update size of main canvas
- var w = timelineController.width + margin.left + margin.right;
- var e = angular.element(main.canvas);
- main.resize(w);
-
- var s = function(v) {
- return v * main.ratio;
- };
-
- main.ctx.clearRect(0, 0, main.canvas.width, main.canvas.height);
- main.ctx.drawImage(lanes.canvas, 0, 0);
-
- // draw all visible regions
- var startX = absolute(currentViewExtents[0]);
- var endX = absolute(currentViewExtents[1]);
- var viewRegions = getContainedRegions(startX, endX);
-
- var effectiveWidth = 0;
- viewRegions.forEach(function(region) {
- effectiveWidth += region.width;
- });
-
- if (effectiveWidth < timelineController.width) {
- // we had to cap the region generation previously, but moved outside of
- // the generated area, so regenerate regions around the current view
- createRegions();
- viewRegions = getContainedRegions(startX, endX);
- }
-
- viewRegions.forEach(function(region) {
- drawRegion(region);
-
- // calculate the cropping area and offsets needed to place the region
- // in the main canvas
- var sx1 = Math.max(0, startX - region.x);
- var sx2 = Math.min(region.width, endX - region.x);
- var sw = sx2 - sx1;
- var dx = Math.max(0, startX - region.x);
- if (Math.floor(sw) === 0) {
- return;
- }
-
- main.ctx.drawImage(
- region.c.canvas,
- s(sx1), 0,
- Math.floor(s(sw)), s(height + margin.bottom),
- s(margin.left + region.x - startX + sx1), s(margin.top),
- s(sw), s(height + margin.bottom));
- });
-
- drawCursor();
- }
-
- timelineController.animateCallbacks.push(function(timestamp) {
- if (!loaded) {
- return false;
- }
-
- if (viewInterpolator) {
- // start the animation
- var currentSize = currentViewExtents[1] - currentViewExtents[0];
- var newSize = timelineController.viewExtents[1] - timelineController.viewExtents[0];
- var diffSize = currentSize - newSize;
- var diffTime = timestamp - easeStartTimestamp;
- var pct = diffTime / easeDuration;
-
- // interpolate the current view bounds according to the easing method
- currentViewExtents = viewInterpolator(easeOutCubic(pct));
-
- if (Math.abs(diffSize) > 1) {
- // size has changed, recalculate regions
- createRegions();
- }
-
- drawAll();
-
- if (pct >= 1) {
- // finished, clear the state vars
- easeStartTimestamp = null;
- viewInterpolator = null;
- return false;
- } else {
- // request more frames until finished
- return true;
- }
- } else {
- // if there is no view interpolator function, just do a plain redraw
- drawAll();
- return false;
- }
- });
-
- /**
- * Gets the canvas-local mouse point for the given mouse event, accounting
- * for all relevant offsets and margins. The returned object will include an
- * additional `inBounds` property indicating whether or not the point falls
- * within the bounds of the main canvas.
- * @param {MouseEvent} evt the mouse event
- * @return {object} a point object
- */
- function getMousePoint(evt) {
- var r = main.canvas.getBoundingClientRect();
- var ret = {
- xRaw: evt.clientX - r.left,
- x: evt.clientX - r.left,
- y: evt.clientY - r.top
- };
-
- ret.inBounds = ret.x > margin.left &&
- ret.x < (margin.left + timelineController.width) &&
- ret.y > margin.top && ret.y < (margin.top + height);
-
- return ret;
- }
-
- main.canvas.addEventListener('mousedown', function(evt) {
- evt.preventDefault();
-
- mousePoint = getMousePoint(evt);
- var rect = getRectAt(mousePoint.x, mousePoint.y);
- if (rect) {
- timelineController.selectItem(rect.entry);
- scope.$apply();
- }
- });
-
- main.canvas.addEventListener('mousemove', function(evt) {
- mousePoint = getMousePoint(evt);
- var rect = getRectAt(mousePoint.x, mousePoint.y);
- var oldHover = hover;
- if (rect && rect.entry !== hover) {
- main.canvas.style.cursor = 'pointer';
- hover = rect.entry;
-
- drawAllForEntry(rect.entry);
- if (oldHover) {
- drawAllForEntry(oldHover);
- }
- } else if (!rect && hover) {
- main.canvas.style.cursor = 'default';
- hover = null;
- drawAllForEntry(oldHover);
- }
-
- timelineController.animate();
- });
-
- main.canvas.addEventListener('mouseout', function(evt) {
- mousePoint = null;
- main.canvas.style.cursor = 'default';
- timelineController.animate();
- });
-
- scope.$on('dataLoaded', function(event, data) {
- y.domain([0, data.length]).range([0, height]);
- createRegions();
- drawLanes();
-
- loaded = true;
- });
-
- scope.$on('update', function() {
- if (!loaded) {
- return;
- }
-
- createRegions();
- drawLanes();
- timelineController.animate();
- });
-
- scope.$on('updateViewSize', function() {
- if (!loaded) {
- return;
- }
-
- if (currentViewExtents) {
- // if we know where the view is already, try to animate the transition
- viewInterpolator = d3Interpolate.interpolateArray(
- currentViewExtents,
- timelineController.viewExtents);
- easeStartTimestamp = performance.now();
- } else {
- // otherwise, move directly to the new location/size
- currentViewExtents = timelineController.viewExtents;
- createRegions();
- }
-
- timelineController.animate();
- });
-
- scope.$on('updateViewPosition', function() {
- if (!loaded) {
- return;
- }
-
- if (currentViewExtents) {
- // if we know where the view is already, try to animate the transition
- viewInterpolator = d3Interpolate.interpolateArray(
- currentViewExtents,
- timelineController.viewExtents);
- easeStartTimestamp = performance.now();
- } else {
- // otherwise, move directly to the new location
- currentViewExtents = timelineController.viewExtents;
- }
-
- timelineController.animate();
- });
-
- scope.$on('postSelect', function(event, newSelection) {
- var old = selection;
- if (newSelection) {
- selection = newSelection.item;
- } else {
- selection = null;
- }
-
- if (old) {
- drawAllForEntry(old);
- }
-
- if (selection) {
- drawAllForEntry(selection);
- }
-
- timelineController.animate();
- });
-
- scope.$on('filter', function() {
- if (loaded) {
- markAllDirty();
- timelineController.animate();
- }
- });
- };
-
- return {
- restrict: 'E',
- require: '^timeline',
- scope: true,
- link: link
- };
-}
-
-directivesModule.directive('timelineViewport', timelineViewport);
diff --git a/app/js/directives/timeline.js b/app/js/directives/timeline.js
deleted file mode 100644
index ba666e9..0000000
--- a/app/js/directives/timeline.js
+++ /dev/null
@@ -1,480 +0,0 @@
-'use strict';
-
-var directivesModule = require('./_index.js');
-
-var arrayUtil = require('../util/array-util');
-var parseDstat = require('../util/dstat-parse');
-
-var d3Array = require('d3-array');
-var d3Collection = require('d3-collection');
-var d3Scale = require('d3-scale');
-
-var statusColorMap = {
- 'success': 'LightGreen',
- 'fail': 'Crimson',
- 'skip': 'DodgerBlue',
- 'selected': 'GoldenRod',
- 'hover': 'DarkTurquoise'
-};
-
-var parseWorker = function(tags) {
- for (var i = 0; i < tags.length; i++) {
- if (!tags[i].startsWith('worker')) {
- continue;
- }
-
- return parseInt(tags[i].split('-')[1], 10);
- }
-
- return null;
-};
-
-/**
- * @ngInject
- */
-function timeline($window, $log, datasetService, progressService) {
-
- /**
- * @ngInject
- */
- var controller = function($scope) {
- var self = this;
- self.statusColorMap = statusColorMap;
-
- self.data = [];
- self.dataRaw = [];
- self.dstat = [];
-
- self.margin = { top: 40, right: 10, bottom: 20, left: 80 };
- self.width = 0;
- self.height = 550 - this.margin.top - this.margin.bottom;
-
- /**
- * The date extents of all chart entries.
- */
- self.timeExtents = [0, 0];
-
- /**
- * The date extents of the current viewport.
- */
- self.viewExtents = [0, 0];
- self.axes = {
- /**
- * The primary axis mapping date to on-screen x. The lower time bound maps
- * to x=0, while the upper time bound maps to x=width.
- */
- x: d3Scale.scaleTime(),
-
- /**
- * The selection axis, mapping date to on-screen x, depending on the size
- * and position of the user selection. `selection(viewExtents[0]) = 0`,
- * while `selection(viewExtents[1]) = width`
- */
- selection: d3Scale.scaleLinear(),
-
- /**
- * The absolute x axis mapping date to virtual x, depending only on the
- * size (but not position) of the user selection.
- * `absolute(timeExtents[0]) = 0`, while `absolute(timeExtents[1])` will
- * be the total width at the current scale, spanning as many
- * viewport-widths as necessary.
- */
- absolute: d3Scale.scaleLinear()
- };
-
- self.selectionName = null;
- self.selection = null;
- self.hover = null;
- self.filterFunction = null;
-
- self.animateId = null;
- self.animateCallbacks = [];
-
- self.setViewExtents = function(extents) {
- if (extents[0] instanceof Date) {
- extents[0] = +extents[0];
- }
-
- if (extents[1] instanceof Date) {
- extents[1] = +extents[1];
- }
-
- var oldSize = self.viewExtents[1] - self.viewExtents[0];
- var newSize = extents[1] - extents[0];
-
- self.viewExtents = extents;
- self.axes.selection.domain(extents);
-
- // slight hack: d3 extrapolates by default, and these scales are identical
- // when the lower bound is zero, so just keep absolute's domain at
- // [0, selectionWidth]
- self.axes.absolute.domain([
- +self.timeExtents[0],
- +self.timeExtents[0] + newSize
- ]);
-
- if (Math.abs(oldSize - newSize) > 1) {
- $scope.$broadcast('updateViewSize');
- } else {
- $scope.$broadcast('updateViewPosition');
- }
-
- $scope.$broadcast('updateView');
- };
-
- self.setHover = function(item) {
- self.hover = item;
- $scope.hoveredItem = item;
- };
-
- self.clearHover = function() {
- self.hover = null;
- $scope.hoveredItem = null;
- };
-
- self.setSelection = function(index, item) {
- if (self.selection && self.selection.item.name === item.name) {
- self.selectionName = null;
- self.selection = null;
- $scope.selectedItem = null;
- } else {
- self.selectionName = item.name;
- self.selection = {
- item: item,
- index: index
- };
- $scope.selectedItem = item;
- }
-
- // selection in the viewport depends on the overview setting the view
- // extents & makings sure there is a visible rect to select
- // the postSelect event makes sure that this is handled in the correct
- // sequence
- $scope.$broadcast('select', self.selection);
- $scope.$broadcast('postSelect', self.selection);
- };
-
- self.setFilterFunction = function(fn) {
- self.filterFunction = fn;
-
- $scope.$broadcast('filter', fn);
- };
-
- self.selectItem = function(item) {
- var workerItems = self.data[item.worker].values;
- var index = -1;
-
- workerItems.forEach(function(d, i) {
- if (d.name === item.name) {
- index = i;
- }
- });
-
- if (index === -1) {
- return false;
- }
-
- self.setSelection(index, item);
- return true;
- };
-
- self.selectIndex = function(worker, index) {
- var item = self.data[worker].values[index];
-
- self.setSelection(index, item);
- return true;
- };
-
- self.clearSelection = function() {
- self.selection = null;
- $scope.$broadcast('select', null);
- };
-
- self.selectNextItem = function() {
- if (self.selection) {
- var worker = self.selection.item.worker;
- if (self.selection.index < self.data[worker].values.length - 1) {
- self.selectIndex(worker, (self.selection.index) + 1);
- return true;
- }
- }
- return false;
- };
-
- self.selectPreviousItem = function() {
- if (self.selection) {
- var worker = self.selection.item.worker;
- if (self.selection.index > 0) {
- self.selectIndex(worker, (self.selection.index) - 1);
- return true;
- }
- }
- return false;
- };
-
- self.hidden = function(item) {
- var width = self.axes.selection(item.endDate) -
- self.axes.selection(item.startDate);
- var hidden = width < 2;
- item.hidden = hidden;
-
- return hidden;
- };
-
- /**
- * Get all raw data that at least partially fall within the given bounds;
- * that is, data points with an end date greater than the minimum bound, and
- * an end date less than the maximum bound. Note that returned data will
- * be a flat array, i.e. not grouped by worker.
- * @param {Date} min the lower date bound
- * @param {Date} max the upper date bound
- * @return {Array} all matching data points
- */
- self.dataInBounds = function(min, max) {
- return self.dataRaw.filter(function(d) {
- return (+d.endDate) > (+min) && (+d.startDate) < (+max);
- });
- };
-
- /**
- * Gets all dstat entries within the given bounds.
- * @param {Date} min the lower time bound
- * @param {Date} max the upper time bound
- * @return {Array} a list of dstat entries within the given bounds
- */
- self.dstatInBounds = function(min, max) {
- var entries = self.dstat.entries;
- var timeFunc = function(d) { return d.system_time; };
- return entries.slice(
- arrayUtil.binaryMinIndex(min, entries, timeFunc),
- arrayUtil.binaryMaxIndex(max, entries, timeFunc) + 1
- );
- };
-
- /**
- * Creates an empty canvas with the specified width and height, returning
- * the element and its 2d context. The element will not be appended to the
- * document and may be used for offscreen rendering.
- * @param {number} [w] the canvas width in px, or null
- * @param {number} [h] the canvas height in px, or null
- * @return {object} an object containing the canvas and its 2d context
- */
- self.createCanvas = function(w, h, scale) {
- w = w || self.width + self.margin.left + self.margin.right;
- h = h || 200 + self.margin.top + self.margin.bottom;
- if (typeof scale === 'undefined') {
- scale = true;
- }
-
- /** @type {HTMLCanvasElement} */
- var canvas = angular.element('