blob: 6a70f48ee3c81b3e081f5d2a86beb1741c27e0ad [file] [log] [blame]
/*jshint loopfunc: true, unused:false */
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
angular.module('zeppelinWebApp')
.controller('ParagraphCtrl', function($scope,$rootScope, $route, $window, $element, $routeParams, $location,
$timeout, $compile, websocketMsgSrv) {
$scope.paragraph = null;
$scope.originalText = '';
$scope.editor = null;
var editorModes = {
'ace/mode/scala': /^%spark/,
'ace/mode/sql': /^%(\w*\.)?\wql/,
'ace/mode/markdown': /^%md/,
'ace/mode/sh': /^%sh/
};
// Controller init
$scope.init = function(newParagraph) {
$scope.paragraph = newParagraph;
$scope.originalText = angular.copy(newParagraph.text);
$scope.chart = {};
$scope.colWidthOption = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
$scope.showTitleEditor = false;
$scope.paragraphFocused = false;
if (newParagraph.focus) {
$scope.paragraphFocused = true;
}
if (!$scope.paragraph.config) {
$scope.paragraph.config = {};
}
initializeDefault();
if ($scope.getResultType() === 'TABLE') {
$scope.loadTableData($scope.paragraph.result);
$scope.setGraphMode($scope.getGraphMode(), false, false);
} else if ($scope.getResultType() === 'HTML') {
$scope.renderHtml();
} else if ($scope.getResultType() === 'ANGULAR') {
$scope.renderAngular();
}
};
$scope.renderHtml = function() {
var retryRenderer = function() {
if (angular.element('#p' + $scope.paragraph.id + '_html').length) {
try {
angular.element('#p' + $scope.paragraph.id + '_html').html($scope.paragraph.result.msg);
angular.element('#p' + $scope.paragraph.id + '_html').find('pre code').each(function(i, e) {
hljs.highlightBlock(e);
});
} catch (err) {
console.log('HTML rendering error %o', err);
}
} else {
$timeout(retryRenderer, 10);
}
};
$timeout(retryRenderer);
};
$scope.renderAngular = function() {
var retryRenderer = function() {
if (angular.element('#p'+$scope.paragraph.id+'_angular').length) {
try {
angular.element('#p'+$scope.paragraph.id+'_angular').html($scope.paragraph.result.msg);
$compile(angular.element('#p'+$scope.paragraph.id+'_angular').contents())($rootScope.compiledScope);
} catch(err) {
console.log('ANGULAR rendering error %o', err);
}
} else {
$timeout(retryRenderer, 10);
}
};
$timeout(retryRenderer);
};
var initializeDefault = function() {
var config = $scope.paragraph.config;
if (!config.colWidth) {
config.colWidth = 12;
}
if (!config.graph) {
config.graph = {};
}
if (!config.graph.mode) {
config.graph.mode = 'table';
}
if (!config.graph.height) {
config.graph.height = 300;
}
if (!config.graph.optionOpen) {
config.graph.optionOpen = false;
}
if (!config.graph.keys) {
config.graph.keys = [];
}
if (!config.graph.values) {
config.graph.values = [];
}
if (!config.graph.groups) {
config.graph.groups = [];
}
if (!config.graph.scatter) {
config.graph.scatter = {};
}
if (config.enabled === undefined) {
config.enabled = true;
}
};
$scope.getIframeDimensions = function () {
if ($scope.asIframe) {
var paragraphid = '#' + $routeParams.paragraphId + '_container';
var height = angular.element(paragraphid).height();
return height;
}
return 0;
};
$scope.$watch($scope.getIframeDimensions, function (newValue, oldValue) {
if ($scope.asIframe && newValue) {
var message = {};
message.height = newValue;
message.url = $location.$$absUrl;
$window.parent.postMessage(angular.toJson(message), '*');
}
});
// TODO: this may have impact on performance when there are many paragraphs in a note.
$scope.$on('updateParagraph', function(event, data) {
if (data.paragraph.id === $scope.paragraph.id &&
(data.paragraph.dateCreated !== $scope.paragraph.dateCreated ||
data.paragraph.dateFinished !== $scope.paragraph.dateFinished ||
data.paragraph.dateStarted !== $scope.paragraph.dateStarted ||
data.paragraph.dateUpdated !== $scope.paragraph.dateUpdated ||
data.paragraph.status !== $scope.paragraph.status ||
data.paragraph.jobName !== $scope.paragraph.jobName ||
data.paragraph.title !== $scope.paragraph.title ||
data.paragraph.errorMessage !== $scope.paragraph.errorMessage ||
!angular.equals(data.paragraph.settings, $scope.paragraph.settings) ||
!angular.equals(data.paragraph.config, $scope.paragraph.config))
) {
var oldType = $scope.getResultType();
var newType = $scope.getResultType(data.paragraph);
var oldGraphMode = $scope.getGraphMode();
var newGraphMode = $scope.getGraphMode(data.paragraph);
var resultRefreshed = (data.paragraph.dateFinished !== $scope.paragraph.dateFinished);
var statusChanged = (data.paragraph.status !== $scope.paragraph.status);
//console.log("updateParagraph oldData %o, newData %o. type %o -> %o, mode %o -> %o", $scope.paragraph, data, oldType, newType, oldGraphMode, newGraphMode);
if ($scope.paragraph.text !== data.paragraph.text) {
if ($scope.dirtyText) { // check if editor has local update
if ($scope.dirtyText === data.paragraph.text ) { // when local update is the same from remote, clear local update
$scope.paragraph.text = data.paragraph.text;
$scope.dirtyText = undefined;
$scope.originalText = angular.copy(data.paragraph.text);
} else { // if there're local update, keep it.
$scope.paragraph.text = $scope.dirtyText;
}
} else {
$scope.paragraph.text = data.paragraph.text;
$scope.originalText = angular.copy(data.paragraph.text);
}
}
/** push the rest */
$scope.paragraph.aborted = data.paragraph.aborted;
$scope.paragraph.dateUpdated = data.paragraph.dateUpdated;
$scope.paragraph.dateCreated = data.paragraph.dateCreated;
$scope.paragraph.dateFinished = data.paragraph.dateFinished;
$scope.paragraph.dateStarted = data.paragraph.dateStarted;
$scope.paragraph.errorMessage = data.paragraph.errorMessage;
$scope.paragraph.jobName = data.paragraph.jobName;
$scope.paragraph.title = data.paragraph.title;
$scope.paragraph.lineNumbers = data.paragraph.lineNumbers;
$scope.paragraph.status = data.paragraph.status;
$scope.paragraph.result = data.paragraph.result;
$scope.paragraph.settings = data.paragraph.settings;
if (!$scope.asIframe) {
$scope.paragraph.config = data.paragraph.config;
initializeDefault();
} else {
data.paragraph.config.editorHide = true;
data.paragraph.config.tableHide = false;
$scope.paragraph.config = data.paragraph.config;
}
if (newType === 'TABLE') {
$scope.loadTableData($scope.paragraph.result);
if (oldType !== 'TABLE' || resultRefreshed) {
clearUnknownColsFromGraphOption();
selectDefaultColsForGraphOption();
}
/** User changed the chart type? */
if (oldGraphMode !== newGraphMode) {
$scope.setGraphMode(newGraphMode, false, false);
} else {
$scope.setGraphMode(newGraphMode, false, true);
}
} else if (newType === 'HTML' && resultRefreshed) {
$scope.renderHtml();
} else if (newType === 'ANGULAR' && resultRefreshed) {
$scope.renderAngular();
}
if (statusChanged || resultRefreshed) {
// when last paragraph runs, zeppelin automatically appends new paragraph.
// this broadcast will focus to the newly inserted paragraph
var paragraphs = angular.element('div[id$="_paragraphColumn_main"');
if (paragraphs.length >= 2 && paragraphs[paragraphs.length-2].id.startsWith($scope.paragraph.id)) {
// rendering output can took some time. So delay scrolling event firing for sometime.
setTimeout(function() {
$rootScope.$broadcast('scrollToCursor');
}, 500);
}
}
}
});
$scope.isRunning = function() {
if ($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING') {
return true;
} else {
return false;
}
};
$scope.cancelParagraph = function() {
console.log('Cancel %o', $scope.paragraph.id);
websocketMsgSrv.cancelParagraphRun($scope.paragraph.id);
};
$scope.runParagraph = function(data) {
websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title,
data, $scope.paragraph.config, $scope.paragraph.settings.params);
$scope.originalText = angular.copy(data);
$scope.dirtyText = undefined;
};
$scope.saveParagraph = function(){
if($scope.dirtyText === undefined || $scope.dirtyText === $scope.originalText){
return;
}
commitParagraph($scope.paragraph.title, $scope.dirtyText, $scope.paragraph.config, $scope.paragraph.settings.params);
$scope.originalText = angular.copy($scope.dirtyText);
$scope.dirtyText = undefined;
};
$scope.toggleEnableDisable = function () {
$scope.paragraph.config.enabled = $scope.paragraph.config.enabled ? false : true;
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.run = function() {
var editorValue = $scope.editor.getValue();
if (editorValue) {
if (!($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING')) {
$scope.runParagraph(editorValue);
}
}
};
$scope.moveUp = function() {
$scope.$emit('moveParagraphUp', $scope.paragraph.id);
};
$scope.moveDown = function() {
$scope.$emit('moveParagraphDown', $scope.paragraph.id);
};
$scope.insertNew = function(position) {
$scope.$emit('insertParagraph', $scope.paragraph.id, position || 'below');
};
$scope.removeParagraph = function() {
BootstrapDialog.confirm({
closable: true,
title: '',
message: 'Do you want to delete this paragraph?',
callback: function(result) {
if (result) {
console.log('Remove paragraph');
websocketMsgSrv.removeParagraph($scope.paragraph.id);
}
}
});
};
$scope.clearParagraphOutput = function() {
websocketMsgSrv.clearParagraphOutput($scope.paragraph.id);
};
$scope.toggleEditor = function() {
if ($scope.paragraph.config.editorHide) {
$scope.openEditor();
} else {
$scope.closeEditor();
}
};
$scope.closeEditor = function() {
console.log('close the note');
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.editorHide = true;
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.openEditor = function() {
console.log('open the note');
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.editorHide = false;
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.closeTable = function() {
console.log('close the output');
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.tableHide = true;
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.openTable = function() {
console.log('open the output');
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.tableHide = false;
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.showTitle = function() {
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.title = true;
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.hideTitle = function() {
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.title = false;
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.setTitle = function() {
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.showLineNumbers = function () {
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.lineNumbers = true;
$scope.editor.renderer.setShowGutter(true);
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.hideLineNumbers = function () {
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.lineNumbers = false;
$scope.editor.renderer.setShowGutter(false);
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.columnWidthClass = function(n) {
if ($scope.asIframe) {
return 'col-md-12';
} else {
return 'col-md-' + n;
}
};
$scope.changeColWidth = function() {
angular.element('.navbar-right.open').removeClass('open');
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.toggleGraphOption = function() {
var newConfig = angular.copy($scope.paragraph.config);
if (newConfig.graph.optionOpen) {
newConfig.graph.optionOpen = false;
} else {
newConfig.graph.optionOpen = true;
}
var newParams = angular.copy($scope.paragraph.settings.params);
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.toggleOutput = function() {
var newConfig = angular.copy($scope.paragraph.config);
newConfig.tableHide = !newConfig.tableHide;
var newParams = angular.copy($scope.paragraph.settings.params);
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
$scope.toggleLineWithFocus = function () {
var mode = $scope.getGraphMode();
if (mode === 'lineWithFocusChart') {
$scope.setGraphMode('lineChart', true);
return true;
}
if (mode === 'lineChart') {
$scope.setGraphMode('lineWithFocusChart', true);
return true;
}
return false;
};
$scope.loadForm = function(formulaire, params) {
var value = formulaire.defaultValue;
if (params[formulaire.name]) {
value = params[formulaire.name];
}
if (value === '') {
value = formulaire.options[0].value;
}
$scope.paragraph.settings.params[formulaire.name] = value;
};
$scope.aceChanged = function() {
$scope.dirtyText = $scope.editor.getSession().getValue();
$scope.startSaveTimer();
$timeout(function() {
$scope.setParagraphMode($scope.editor.getSession(), $scope.dirtyText, $scope.editor.getCursorPosition());
});
};
$scope.aceLoaded = function(_editor) {
var langTools = ace.require('ace/ext/language_tools');
var Range = ace.require('ace/range').Range;
_editor.$blockScrolling = Infinity;
$scope.editor = _editor;
if (_editor.container.id !== '{{paragraph.id}}_editor') {
$scope.editor.renderer.setShowGutter($scope.paragraph.config.lineNumbers);
$scope.editor.setShowFoldWidgets(false);
$scope.editor.setHighlightActiveLine(false);
$scope.editor.setHighlightGutterLine(false);
$scope.editor.getSession().setUseWrapMode(true);
$scope.editor.setTheme('ace/theme/chrome');
if ($scope.paragraphFocused) {
$scope.editor.focus();
}
autoAdjustEditorHeight(_editor.container.id);
angular.element(window).resize(function() {
autoAdjustEditorHeight(_editor.container.id);
});
if (navigator.appVersion.indexOf('Mac') !== -1 ) {
$scope.editor.setKeyboardHandler('ace/keyboard/emacs');
} else if (navigator.appVersion.indexOf('Win') !== -1 ||
navigator.appVersion.indexOf('X11') !== -1 ||
navigator.appVersion.indexOf('Linux') !== -1) {
// not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows.
}
$scope.setParagraphMode = function(session, paragraphText, pos) {
// Evaluate the mode only if the first 30 characters of the paragraph have been modified or the the position is undefined.
if ( (typeof pos === 'undefined') || (pos.row === 0 && pos.column < 30)) {
// If paragraph loading, use config value if exists
if ((typeof pos === 'undefined') && $scope.paragraph.config.editorMode) {
session.setMode($scope.paragraph.config.editorMode);
} else {
// Defaults to spark mode
var newMode = 'ace/mode/scala';
// Test first against current mode
var oldMode = session.getMode().$id;
if (!editorModes[oldMode] || !editorModes[oldMode].test(paragraphText)) {
for (var key in editorModes) {
if (key !== oldMode) {
if (editorModes[key].test(paragraphText)){
$scope.paragraph.config.editorMode = key;
session.setMode(key);
return true;
}
}
}
$scope.paragraph.config.editorMode = newMode;
session.setMode(newMode);
}
}
}
};
var remoteCompleter = {
getCompletions : function(editor, session, pos, prefix, callback) {
if (!$scope.editor.isFocused() ){ return;}
pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length;
var buf = session.getValue();
websocketMsgSrv.completion($scope.paragraph.id, buf, pos);
$scope.$on('completionList', function(event, data) {
if (data.completions) {
var completions = [];
for (var c in data.completions) {
var v = data.completions[c];
completions.push({
name:v,
value:v,
score:300
});
}
callback(null, completions);
}
});
}
};
langTools.setCompleters([remoteCompleter, langTools.keyWordCompleter, langTools.snippetCompleter, langTools.textCompleter]);
$scope.editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: false,
enableLiveAutocompletion:false
});
$scope.handleFocus = function(value) {
$scope.paragraphFocused = value;
// Protect against error in case digest is already running
$timeout(function() {
// Apply changes since they come from 3rd party library
$scope.$digest();
});
};
$scope.editor.on('focus', function() {
$scope.handleFocus(true);
});
$scope.editor.on('blur', function() {
$scope.handleFocus(false);
});
$scope.editor.getSession().on('change', function(e, editSession) {
autoAdjustEditorHeight(_editor.container.id);
});
$scope.setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue());
// autocomplete on '.'
/*
$scope.editor.commands.on("afterExec", function(e, t) {
if (e.command.name == "insertstring" && e.args == "." ) {
var all = e.editor.completers;
//e.editor.completers = [remoteCompleter];
e.editor.execCommand("startAutocomplete");
//e.editor.completers = all;
}
});
*/
// remove binding
$scope.editor.commands.bindKey('ctrl-alt-n.', null);
// autocomplete on 'ctrl+.'
$scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete');
$scope.editor.commands.bindKey('ctrl-space', null);
// handle cursor moves
$scope.editor.keyBinding.origOnCommandKey = $scope.editor.keyBinding.onCommandKey;
$scope.editor.keyBinding.onCommandKey = function(e, hashId, keyCode) {
if ($scope.editor.completer && $scope.editor.completer.activated) { // if autocompleter is active
} else {
// fix ace editor focus issue in chrome (textarea element goes to top: -1000px after focused by cursor move)
if (parseInt(angular.element('#' + $scope.paragraph.id + '_editor > textarea').css('top').replace('px', '')) < 0) {
var position = $scope.editor.getCursorPosition();
var cursorPos = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true);
angular.element('#' + $scope.paragraph.id + '_editor > textarea').css('top', cursorPos.top);
}
var numRows;
var currentRow;
if (keyCode === 38 || (keyCode === 80 && e.ctrlKey && !e.altKey)) { // UP
numRows = $scope.editor.getSession().getLength();
currentRow = $scope.editor.getCursorPosition().row;
if (currentRow === 0) {
// move focus to previous paragraph
$scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id);
} else {
$scope.scrollToCursor($scope.paragraph.id, -1);
}
} else if (keyCode === 40 || (keyCode === 78 && e.ctrlKey && !e.altKey)) { // DOWN
numRows = $scope.editor.getSession().getLength();
currentRow = $scope.editor.getCursorPosition().row;
if (currentRow === numRows-1) {
// move focus to next paragraph
$scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id);
} else {
$scope.scrollToCursor($scope.paragraph.id, 1);
}
}
}
this.origOnCommandKey(e, hashId, keyCode);
};
}
};
var autoAdjustEditorHeight = function(id) {
var editor = $scope.editor;
var height = editor.getSession().getScreenLength() * editor.renderer.lineHeight + editor.renderer.scrollBar.getWidth();
angular.element('#' + id).height(height.toString() + 'px');
editor.resize();
};
$rootScope.$on('scrollToCursor', function(event) {
// scroll on 'scrollToCursor' event only when cursor is in the last paragraph
var paragraphs = angular.element('div[id$="_paragraphColumn_main"');
if (paragraphs[paragraphs.length-1].id.startsWith($scope.paragraph.id)) {
$scope.scrollToCursor($scope.paragraph.id, 0);
}
});
/** scrollToCursor if it is necessary
* when cursor touches scrollTriggerEdgeMargin from the top (or bottom) of the screen, it autoscroll to place cursor around 1/3 of screen height from the top (or bottom)
* paragraphId : paragraph that has active cursor
* lastCursorMove : 1(down), 0, -1(up) last cursor move event
**/
$scope.scrollToCursor = function(paragraphId, lastCursorMove) {
if (!$scope.editor.isFocused()) {
// only make sense when editor is focused
return;
}
var lineHeight = $scope.editor.renderer.lineHeight;
var headerHeight = 103; // menubar, notebook titlebar
var scrollTriggerEdgeMargin = 50;
var documentHeight = angular.element(document).height();
var windowHeight = angular.element(window).height(); // actual viewport height
var scrollPosition = angular.element(document).scrollTop();
var editorPosition = angular.element('#'+paragraphId+'_editor').offset();
var position = $scope.editor.getCursorPosition();
var lastCursorPosition = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true);
var calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight*lastCursorMove;
var scrollTargetPos;
if (calculatedCursorPosition < scrollPosition + headerHeight + scrollTriggerEdgeMargin) {
scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight-headerHeight)/3);
if (scrollTargetPos < 0) {
scrollTargetPos = 0;
}
} else if(calculatedCursorPosition > scrollPosition + scrollTriggerEdgeMargin + windowHeight - headerHeight) {
scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight-headerHeight)*2/3);
if (scrollTargetPos > documentHeight) {
scrollTargetPos = documentHeight;
}
}
// cancel previous scroll animation
var bodyEl = angular.element('body');
bodyEl.stop();
bodyEl.finish();
// scroll to scrollTargetPos
bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration:100});
};
var setEditorHeight = function(id, height) {
angular.element('#' + id).height(height.toString() + 'px');
};
$scope.getEditorValue = function() {
return $scope.editor.getValue();
};
$scope.getProgress = function() {
return ($scope.currentProgress) ? $scope.currentProgress : 0;
};
$scope.getExecutionTime = function() {
var pdata = $scope.paragraph;
var timeMs = Date.parse(pdata.dateFinished) - Date.parse(pdata.dateStarted);
if (isNaN(timeMs) || timeMs < 0) {
if ($scope.isResultOutdated()){
return 'outdated';
}
return '';
}
var desc = 'Took ' + (timeMs/1000) + ' seconds';
if ($scope.isResultOutdated()){
desc += ' (outdated)';
}
return desc;
};
$scope.isResultOutdated = function() {
var pdata = $scope.paragraph;
if (pdata.dateUpdated !==undefined && Date.parse(pdata.dateUpdated) > Date.parse(pdata.dateStarted)){
return true;
}
return false;
};
$scope.$on('updateProgress', function(event, data) {
if (data.id === $scope.paragraph.id) {
$scope.currentProgress = data.progress;
}
});
$scope.$on('keyEvent', function(event, keyEvent) {
if ($scope.paragraphFocused) {
var paragraphId = $scope.paragraph.id;
var keyCode = keyEvent.keyCode;
var noShortcutDefined = false;
var editorHide = $scope.paragraph.config.editorHide;
if (editorHide && (keyCode === 38 || (keyCode === 80 && keyEvent.ctrlKey && !keyEvent.altKey))) { // up
// move focus to previous paragraph
$scope.$emit('moveFocusToPreviousParagraph', paragraphId);
} else if (editorHide && (keyCode === 40 || (keyCode === 78 && keyEvent.ctrlKey && !keyEvent.altKey))) { // down
// move focus to next paragraph
$scope.$emit('moveFocusToNextParagraph', paragraphId);
} else if (keyEvent.shiftKey && keyCode === 13) { // Shift + Enter
$scope.run();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 67) { // Ctrl + Alt + c
$scope.cancelParagraph();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 68) { // Ctrl + Alt + d
$scope.removeParagraph();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 75) { // Ctrl + Alt + k
$scope.moveUp();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 74) { // Ctrl + Alt + j
$scope.moveDown();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 66) { // Ctrl + Alt + b
$scope.insertNew();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 79) { // Ctrl + Alt + o
$scope.toggleOutput();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 69) { // Ctrl + Alt + e
$scope.toggleEditor();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 77) { // Ctrl + Alt + m
if ($scope.paragraph.config.lineNumbers) {
$scope.hideLineNumbers();
} else {
$scope.showLineNumbers();
}
} else if (keyEvent.ctrlKey && keyEvent.altKey && ((keyCode >= 48 && keyCode <=57) || keyCode === 189 || keyCode === 187)) { // Ctrl + Alt + [1~9,0,-,=]
var colWidth = 12;
if (keyCode === 48) {
colWidth = 10;
} else if (keyCode === 189) {
colWidth = 11;
} else if (keyCode === 187) {
colWidth = 12;
} else {
colWidth = keyCode - 48;
}
$scope.paragraph.config.colWidth = colWidth;
$scope.changeColWidth();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 84) { // Ctrl + Alt + t
if ($scope.paragraph.config.title) {
$scope.hideTitle();
} else {
$scope.showTitle();
}
} else {
noShortcutDefined = true;
}
if (!noShortcutDefined) {
keyEvent.preventDefault();
}
}
});
$scope.$on('focusParagraph', function(event, paragraphId, cursorPos, mouseEvent) {
if ($scope.paragraph.id === paragraphId) {
// focus editor
if (!$scope.paragraph.config.editorHide) {
if (!mouseEvent) {
$scope.editor.focus();
// move cursor to the first row (or the last row)
var row;
if (cursorPos >= 0) {
row = cursorPos;
$scope.editor.gotoLine(row, 0);
} else {
row = $scope.editor.session.getLength();
$scope.editor.gotoLine(row, 0);
}
$scope.scrollToCursor($scope.paragraph.id, 0);
}
}
$scope.handleFocus(true);
} else {
$scope.editor.blur();
$scope.handleFocus(false);
}
});
$scope.$on('runParagraph', function(event) {
$scope.runParagraph($scope.editor.getValue());
});
$scope.$on('openEditor', function(event) {
$scope.openEditor();
});
$scope.$on('closeEditor', function(event) {
$scope.closeEditor();
});
$scope.$on('openTable', function(event) {
$scope.openTable();
});
$scope.$on('closeTable', function(event) {
$scope.closeTable();
});
$scope.getResultType = function(paragraph) {
var pdata = (paragraph) ? paragraph : $scope.paragraph;
if (pdata.result && pdata.result.type) {
return pdata.result.type;
} else {
return 'TEXT';
}
};
$scope.getBase64ImageSrc = function(base64Data) {
return 'data:image/png;base64,'+base64Data;
};
$scope.getGraphMode = function(paragraph) {
var pdata = (paragraph) ? paragraph : $scope.paragraph;
if (pdata.config.graph && pdata.config.graph.mode) {
return pdata.config.graph.mode;
} else {
return 'table';
}
};
$scope.loadTableData = function(result) {
if (!result) {
return;
}
if (result.type === 'TABLE') {
var columnNames = [];
var rows = [];
var array = [];
var textRows = result.msg.split('\n');
result.comment = '';
var comment = false;
for (var i = 0; i < textRows.length; i++) {
var textRow = textRows[i];
if (comment) {
result.comment += textRow;
continue;
}
if (textRow === '') {
if (rows.length>0) {
comment = true;
}
continue;
}
var textCols = textRow.split('\t');
var cols = [];
var cols2 = [];
for (var j = 0; j < textCols.length; j++) {
var col = textCols[j];
if (i === 0) {
columnNames.push({name:col, index:j, aggr:'sum'});
} else {
cols.push(col);
cols2.push({key: (columnNames[i]) ? columnNames[i].name: undefined, value: col});
}
}
if (i !== 0) {
rows.push(cols);
array.push(cols2);
}
}
result.msgTable = array;
result.columnNames = columnNames;
result.rows = rows;
}
};
$scope.setGraphMode = function(type, emit, refresh) {
if (emit) {
setNewMode(type);
} else {
clearUnknownColsFromGraphOption();
// set graph height
var height = $scope.paragraph.config.graph.height;
angular.element('#p' + $scope.paragraph.id + '_graph').height(height);
if (!type || type === 'table') {
setTable($scope.paragraph.result, refresh);
}
else {
setD3Chart(type, $scope.paragraph.result, refresh);
}
}
};
var setNewMode = function(newMode) {
var newConfig = angular.copy($scope.paragraph.config);
var newParams = angular.copy($scope.paragraph.settings.params);
// graph options
newConfig.graph.mode = newMode;
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
var commitParagraph = function(title, text, config, params) {
websocketMsgSrv.commitParagraph($scope.paragraph.id, title, text, config, params);
};
var setTable = function(type, data, refresh) {
var getTableContentFormat = function(d) {
if (isNaN(d)) {
if (d.length>'%html'.length && '%html ' === d.substring(0, '%html '.length)) {
return 'html';
} else {
return '';
}
} else {
return '';
}
};
var formatTableContent = function(d) {
if (isNaN(d)) {
var f = getTableContentFormat(d);
if (f !== '') {
return d.substring(f.length+2);
} else {
return d;
}
} else {
var dStr = d.toString();
var splitted = dStr.split('.');
var formatted = splitted[0].replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
if (splitted.length>1) {
formatted+= '.'+splitted[1];
}
return formatted;
}
};
var renderTable = function() {
var html = '';
html += '<table class="table table-hover table-condensed">';
html += ' <thead>';
html += ' <tr style="background-color: #F6F6F6; font-weight: bold;">';
for (var titleIndex in $scope.paragraph.result.columnNames) {
html += '<th>'+$scope.paragraph.result.columnNames[titleIndex].name+'</th>';
}
html += ' </tr>';
html += ' </thead>';
html += ' <tbody>';
for (var r in $scope.paragraph.result.msgTable) {
var row = $scope.paragraph.result.msgTable[r];
html += ' <tr>';
for (var index in row) {
var v = row[index].value;
if (getTableContentFormat(v) !== 'html') {
v = v.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
return '&#'+i.charCodeAt(0)+';';
});
}
html += ' <td>'+formatTableContent(v)+'</td>';
}
html += ' </tr>';
}
html += ' </tbody>';
html += '</table>';
angular.element('#p' + $scope.paragraph.id + '_table').html(html);
if ($scope.paragraph.result.msgTable.length > 10000) {
angular.element('#p' + $scope.paragraph.id + '_table').css('overflow', 'scroll');
// set table height
var height = $scope.paragraph.config.graph.height;
angular.element('#p' + $scope.paragraph.id + '_table').css('height', height);
} else {
var dataTable = angular.element('#p' + $scope.paragraph.id + '_table .table');
dataTable.floatThead({
scrollContainer: function (dataTable) {
return angular.element('#p' + $scope.paragraph.id + '_table');
}
});
angular.element('#p' + $scope.paragraph.id + '_table .table').on('remove', function () {
angular.element('#p' + $scope.paragraph.id + '_table .table').floatThead('destroy');
});
angular.element('#p' + $scope.paragraph.id + '_table').css('position', 'relative');
angular.element('#p' + $scope.paragraph.id + '_table').css('height', '100%');
angular.element('#p' + $scope.paragraph.id + '_table').perfectScrollbar('destroy');
angular.element('#p' + $scope.paragraph.id + '_table').perfectScrollbar();
angular.element('.ps-scrollbar-y-rail').css('z-index', '1002');
// set table height
var psHeight = $scope.paragraph.config.graph.height;
angular.element('#p' + $scope.paragraph.id + '_table').css('height', psHeight);
angular.element('#p' + $scope.paragraph.id + '_table').perfectScrollbar('update');
}
};
var retryRenderer = function() {
if (angular.element('#p' + $scope.paragraph.id + '_table').length) {
try {
renderTable();
} catch(err) {
console.log('Chart drawing error %o', err);
}
} else {
$timeout(retryRenderer,10);
}
};
$timeout(retryRenderer);
};
var integerFormatter = d3.format(',.1d');
var customAbbrevFormatter = function(x) {
var s = d3.format('.3s')(x);
switch (s[s.length - 1]) {
case 'G': return s.slice(0, -1) + 'B';
}
return s;
};
var xAxisTickFormat = function(d, xLabels) {
if (xLabels[d] && (isNaN(parseFloat(xLabels[d])) || !isFinite(xLabels[d]))) { // to handle string type xlabel
return xLabels[d];
} else {
return d;
}
};
var yAxisTickFormat = function(d) {
if(d >= Math.pow(10,6)){
return customAbbrevFormatter(d);
}
return integerFormatter(d);
};
var setD3Chart = function(type, data, refresh) {
if (!$scope.chart[type]) {
var chart = nv.models[type]();
$scope.chart[type] = chart;
}
var d3g = [];
var xLabels;
var yLabels;
if (type === 'scatterChart') {
var scatterData = setScatterChart(data, refresh);
xLabels = scatterData.xLabels;
yLabels = scatterData.yLabels;
d3g = scatterData.d3g;
$scope.chart[type].xAxis.tickFormat(function(d) {return xAxisTickFormat(d, xLabels);});
$scope.chart[type].yAxis.tickFormat(function(d) {return xAxisTickFormat(d, yLabels);});
// configure how the tooltip looks.
$scope.chart[type].tooltipContent(function(key, x, y, graph, data) {
var tooltipContent = '<h3>' + key + '</h3>';
if ($scope.paragraph.config.graph.scatter.size &&
$scope.isValidSizeOption($scope.paragraph.config.graph.scatter, $scope.paragraph.result.rows)) {
tooltipContent += '<p>' + data.point.size + '</p>';
}
return tooltipContent;
});
$scope.chart[type].showDistX(true)
.showDistY(true);
//handle the problem of tooltip not showing when muliple points have same value.
} else {
var p = pivot(data);
if (type === 'pieChart') {
var d = pivotDataToD3ChartFormat(p, true).d3g;
$scope.chart[type].x(function(d) { return d.label;})
.y(function(d) { return d.value;});
if ( d.length > 0 ) {
for ( var i=0; i<d[0].values.length ; i++) {
var e = d[0].values[i];
d3g.push({
label : e.x,
value : e.y
});
}
}
} else if (type === 'multiBarChart') {
d3g = pivotDataToD3ChartFormat(p, true, false, type).d3g;
$scope.chart[type].yAxis.axisLabelDistance(50);
$scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d);});
} else if (type === 'lineChart' || type === 'stackedAreaChart' || type === 'lineWithFocusChart') {
var pivotdata = pivotDataToD3ChartFormat(p, false, true);
xLabels = pivotdata.xLabels;
d3g = pivotdata.d3g;
$scope.chart[type].xAxis.tickFormat(function(d) {return xAxisTickFormat(d, xLabels);});
$scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d);});
$scope.chart[type].yAxis.axisLabelDistance(50);
if ($scope.chart[type].useInteractiveGuideline) { // lineWithFocusChart hasn't got useInteractiveGuideline
$scope.chart[type].useInteractiveGuideline(true); // for better UX and performance issue. (https://github.com/novus/nvd3/issues/691)
}
if($scope.paragraph.config.graph.forceY) {
$scope.chart[type].forceY([0]); // force y-axis minimum to 0 for line chart.
} else {
$scope.chart[type].forceY([]);
}
}
}
var renderChart = function() {
if (!refresh) {
// TODO force destroy previous chart
}
var height = $scope.paragraph.config.graph.height;
var animationDuration = 300;
var numberOfDataThreshold = 150;
// turn off animation when dataset is too large. (for performance issue)
// still, since dataset is large, the chart content sequentially appears like animated.
try {
if (d3g[0].values.length > numberOfDataThreshold) {
animationDuration = 0;
}
} catch(ignoreErr) {
}
var chartEl = d3.select('#p'+$scope.paragraph.id+'_'+type+' svg')
.attr('height', $scope.paragraph.config.graph.height)
.datum(d3g)
.transition()
.duration(animationDuration)
.call($scope.chart[type]);
d3.select('#p'+$scope.paragraph.id+'_'+type+' svg').style.height = height+'px';
nv.utils.windowResize($scope.chart[type].update);
};
var retryRenderer = function() {
if (angular.element('#p' + $scope.paragraph.id + '_' + type + ' svg').length !== 0) {
try {
renderChart();
} catch(err) {
console.log('Chart drawing error %o', err);
}
} else {
$timeout(retryRenderer,10);
}
};
$timeout(retryRenderer);
};
$scope.isGraphMode = function(graphName) {
if ($scope.getResultType() === 'TABLE' && $scope.getGraphMode()===graphName) {
return true;
} else {
return false;
}
};
$scope.onGraphOptionChange = function() {
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeGraphOptionKeys = function(idx) {
$scope.paragraph.config.graph.keys.splice(idx, 1);
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeGraphOptionValues = function(idx) {
$scope.paragraph.config.graph.values.splice(idx, 1);
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeGraphOptionGroups = function(idx) {
$scope.paragraph.config.graph.groups.splice(idx, 1);
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.setGraphOptionValueAggr = function(idx, aggr) {
$scope.paragraph.config.graph.values[idx].aggr = aggr;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeScatterOptionXaxis = function(idx) {
$scope.paragraph.config.graph.scatter.xAxis = null;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeScatterOptionYaxis = function(idx) {
$scope.paragraph.config.graph.scatter.yAxis = null;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeScatterOptionGroup = function(idx) {
$scope.paragraph.config.graph.scatter.group = null;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeScatterOptionSize = function(idx) {
$scope.paragraph.config.graph.scatter.size = null;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
/* Clear unknown columns from graph option */
var clearUnknownColsFromGraphOption = function() {
var unique = function(list) {
for (var i = 0; i<list.length; i++) {
for (var j=i+1; j<list.length; j++) {
if (angular.equals(list[i], list[j])) {
list.splice(j, 1);
}
}
}
};
var removeUnknown = function(list) {
for (var i = 0; i<list.length; i++) {
// remove non existing column
var found = false;
for (var j=0; j<$scope.paragraph.result.columnNames.length; j++) {
var a = list[i];
var b = $scope.paragraph.result.columnNames[j];
if (a.index === b.index && a.name === b.name) {
found = true;
break;
}
}
if (!found) {
list.splice(i, 1);
}
}
};
var removeUnknownFromScatterSetting = function(fields) {
for (var f in fields) {
if (fields[f]) {
var found = false;
for (var i = 0; i < $scope.paragraph.result.columnNames.length; i++) {
var a = fields[f];
var b = $scope.paragraph.result.columnNames[i];
if (a.index === b.index && a.name === b.name) {
found = true;
break;
}
}
if (!found) {
fields[f] = null;
}
}
}
};
unique($scope.paragraph.config.graph.keys);
removeUnknown($scope.paragraph.config.graph.keys);
removeUnknown($scope.paragraph.config.graph.values);
unique($scope.paragraph.config.graph.groups);
removeUnknown($scope.paragraph.config.graph.groups);
removeUnknownFromScatterSetting($scope.paragraph.config.graph.scatter);
};
/* select default key and value if there're none selected */
var selectDefaultColsForGraphOption = function() {
if ($scope.paragraph.config.graph.keys.length === 0 && $scope.paragraph.result.columnNames.length > 0) {
$scope.paragraph.config.graph.keys.push($scope.paragraph.result.columnNames[0]);
}
if ($scope.paragraph.config.graph.values.length === 0 && $scope.paragraph.result.columnNames.length > 1) {
$scope.paragraph.config.graph.values.push($scope.paragraph.result.columnNames[1]);
}
if (!$scope.paragraph.config.graph.scatter.xAxis && !$scope.paragraph.config.graph.scatter.yAxis) {
if ($scope.paragraph.result.columnNames.length > 1) {
$scope.paragraph.config.graph.scatter.xAxis = $scope.paragraph.result.columnNames[0];
$scope.paragraph.config.graph.scatter.yAxis = $scope.paragraph.result.columnNames[1];
} else if ($scope.paragraph.result.columnNames.length === 1) {
$scope.paragraph.config.graph.scatter.xAxis = $scope.paragraph.result.columnNames[0];
}
}
};
var pivot = function(data) {
var keys = $scope.paragraph.config.graph.keys;
var groups = $scope.paragraph.config.graph.groups;
var values = $scope.paragraph.config.graph.values;
var aggrFunc = {
sum : function(a,b) {
var varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0;
var varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0;
return varA+varB;
},
count : function(a,b) {
var varA = (a !== undefined) ? parseInt(a) : 0;
var varB = (b !== undefined) ? 1 : 0;
return varA+varB;
},
min : function(a,b) {
var varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0;
var varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0;
return Math.min(varA,varB);
},
max : function(a,b) {
var varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0;
var varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0;
return Math.max(varA,varB);
},
avg : function(a,b,c) {
var varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0;
var varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0;
return varA+varB;
}
};
var aggrFuncDiv = {
sum : false,
count : false,
min : false,
max : false,
avg : true
};
var schema = {};
var rows = {};
for (var i=0; i < data.rows.length; i++) {
var row = data.rows[i];
var newRow = {};
var s = schema;
var p = rows;
for (var k=0; k < keys.length; k++) {
var key = keys[k];
// add key to schema
if (!s[key.name]) {
s[key.name] = {
order : k,
index : key.index,
type : 'key',
children : {}
};
}
s = s[key.name].children;
// add key to row
var keyKey = row[key.index];
if (!p[keyKey]) {
p[keyKey] = {};
}
p = p[keyKey];
}
for (var g=0; g < groups.length; g++) {
var group = groups[g];
var groupKey = row[group.index];
// add group to schema
if (!s[groupKey]) {
s[groupKey] = {
order : g,
index : group.index,
type : 'group',
children : {}
};
}
s = s[groupKey].children;
// add key to row
if (!p[groupKey]) {
p[groupKey] = {};
}
p = p[groupKey];
}
for (var v=0; v < values.length; v++) {
var value = values[v];
var valueKey = value.name+'('+value.aggr+')';
// add value to schema
if (!s[valueKey]) {
s[valueKey] = {
type : 'value',
order : v,
index : value.index
};
}
// add value to row
if (!p[valueKey]) {
p[valueKey] = {
value : (value.aggr !== 'count') ? row[value.index] : 1,
count: 1
};
} else {
p[valueKey] = {
value : aggrFunc[value.aggr](p[valueKey].value, row[value.index], p[valueKey].count+1),
count : (aggrFuncDiv[value.aggr]) ? p[valueKey].count+1 : p[valueKey].count
};
}
}
}
//console.log("schema=%o, rows=%o", schema, rows);
return {
schema : schema,
rows : rows
};
};
var pivotDataToD3ChartFormat = function(data, allowTextXAxis, fillMissingValues, chartType) {
// construct d3 data
var d3g = [];
var schema = data.schema;
var rows = data.rows;
var values = $scope.paragraph.config.graph.values;
var concat = function(o, n) {
if (!o) {
return n;
} else {
return o+'.'+n;
}
};
var getSchemaUnderKey = function(key, s) {
for (var c in key.children) {
s[c] = {};
getSchemaUnderKey(key.children[c], s[c]);
}
};
var traverse = function(sKey, s, rKey, r, func, rowName, rowValue, colName) {
//console.log("TRAVERSE sKey=%o, s=%o, rKey=%o, r=%o, rowName=%o, rowValue=%o, colName=%o", sKey, s, rKey, r, rowName, rowValue, colName);
if (s.type==='key') {
rowName = concat(rowName, sKey);
rowValue = concat(rowValue, rKey);
} else if (s.type==='group') {
colName = concat(colName, rKey);
} else if (s.type==='value' && sKey===rKey || valueOnly) {
colName = concat(colName, rKey);
func(rowName, rowValue, colName, r);
}
for (var c in s.children) {
if (fillMissingValues && s.children[c].type === 'group' && r[c] === undefined) {
var cs = {};
getSchemaUnderKey(s.children[c], cs);
traverse(c, s.children[c], c, cs, func, rowName, rowValue, colName);
continue;
}
for (var j in r) {
if (s.children[c].type === 'key' || c === j) {
traverse(c, s.children[c], j, r[j], func, rowName, rowValue, colName);
}
}
}
};
var keys = $scope.paragraph.config.graph.keys;
var groups = $scope.paragraph.config.graph.groups;
values = $scope.paragraph.config.graph.values;
var valueOnly = (keys.length === 0 && groups.length === 0 && values.length > 0);
var noKey = (keys.length === 0);
var isMultiBarChart = (chartType === 'multiBarChart');
var sKey = Object.keys(schema)[0];
var rowNameIndex = {};
var rowIdx = 0;
var colNameIndex = {};
var colIdx = 0;
var rowIndexValue = {};
for (var k in rows) {
traverse(sKey, schema[sKey], k, rows[k], function(rowName, rowValue, colName, value) {
//console.log("RowName=%o, row=%o, col=%o, value=%o", rowName, rowValue, colName, value);
if (rowNameIndex[rowValue] === undefined) {
rowIndexValue[rowIdx] = rowValue;
rowNameIndex[rowValue] = rowIdx++;
}
if (colNameIndex[colName] === undefined) {
colNameIndex[colName] = colIdx++;
}
var i = colNameIndex[colName];
if (noKey && isMultiBarChart) {
i = 0;
}
if (!d3g[i]) {
d3g[i] = {
values : [],
key : (noKey && isMultiBarChart) ? 'values' : colName
};
}
var xVar = isNaN(rowValue) ? ((allowTextXAxis) ? rowValue : rowNameIndex[rowValue]) : parseFloat(rowValue);
var yVar = 0;
if (xVar === undefined) { xVar = colName; }
if (value !== undefined) {
yVar = isNaN(value.value) ? 0 : parseFloat(value.value) / parseFloat(value.count);
}
d3g[i].values.push({
x : xVar,
y : yVar
});
});
}
// clear aggregation name, if possible
var namesWithoutAggr = {};
var colName;
var withoutAggr;
// TODO - This part could use som refactoring - Weird if/else with similar actions and variable names
for (colName in colNameIndex) {
withoutAggr = colName.substring(0, colName.lastIndexOf('('));
if (!namesWithoutAggr[withoutAggr]) {
namesWithoutAggr[withoutAggr] = 1;
} else {
namesWithoutAggr[withoutAggr]++;
}
}
if (valueOnly) {
for (var valueIndex = 0; valueIndex < d3g[0].values.length; valueIndex++) {
colName = d3g[0].values[valueIndex].x;
if (!colName) {
continue;
}
withoutAggr = colName.substring(0, colName.lastIndexOf('('));
if (namesWithoutAggr[withoutAggr] <= 1 ) {
d3g[0].values[valueIndex].x = withoutAggr;
}
}
} else {
for (var d3gIndex = 0; d3gIndex < d3g.length; d3gIndex++) {
colName = d3g[d3gIndex].key;
withoutAggr = colName.substring(0, colName.lastIndexOf('('));
if (namesWithoutAggr[withoutAggr] <= 1 ) {
d3g[d3gIndex].key = withoutAggr;
}
}
// use group name instead of group.value as a column name, if there're only one group and one value selected.
if (groups.length === 1 && values.length === 1) {
for (d3gIndex = 0; d3gIndex < d3g.length; d3gIndex++) {
colName = d3g[d3gIndex].key;
colName = colName.split('.')[0];
d3g[d3gIndex].key = colName;
}
}
}
return {
xLabels : rowIndexValue,
d3g : d3g
};
};
var setDiscreteScatterData = function(data) {
var xAxis = $scope.paragraph.config.graph.scatter.xAxis;
var yAxis = $scope.paragraph.config.graph.scatter.yAxis;
var group = $scope.paragraph.config.graph.scatter.group;
var xValue;
var yValue;
var grp;
var rows = {};
for (var i = 0; i < data.rows.length; i++) {
var row = data.rows[i];
if (xAxis) {
xValue = row[xAxis.index];
}
if (yAxis) {
yValue = row[yAxis.index];
}
if (group) {
grp = row[group.index];
}
var key = xValue + ',' + yValue + ',' + grp;
if(!rows[key]) {
rows[key] = {
x : xValue,
y : yValue,
group : grp,
size : 1
};
} else {
rows[key].size++;
}
}
// change object into array
var newRows = [];
for(var r in rows){
var newRow = [];
if (xAxis) { newRow[xAxis.index] = rows[r].x; }
if (yAxis) { newRow[yAxis.index] = rows[r].y; }
if (group) { newRow[group.index] = rows[r].group; }
newRow[data.rows[0].length] = rows[r].size;
newRows.push(newRow);
}
return newRows;
};
var setScatterChart = function(data, refresh) {
var xAxis = $scope.paragraph.config.graph.scatter.xAxis;
var yAxis = $scope.paragraph.config.graph.scatter.yAxis;
var group = $scope.paragraph.config.graph.scatter.group;
var size = $scope.paragraph.config.graph.scatter.size;
var xValues = [];
var yValues = [];
var rows = {};
var d3g = [];
var rowNameIndex = {};
var colNameIndex = {};
var grpNameIndex = {};
var rowIndexValue = {};
var colIndexValue = {};
var grpIndexValue = {};
var rowIdx = 0;
var colIdx = 0;
var grpIdx = 0;
var grpName = '';
var xValue;
var yValue;
var row;
if (!xAxis && !yAxis) {
return {
d3g : []
};
}
for (var i = 0; i < data.rows.length; i++) {
row = data.rows[i];
if (xAxis) {
xValue = row[xAxis.index];
xValues[i] = xValue;
}
if (yAxis) {
yValue = row[yAxis.index];
yValues[i] = yValue;
}
}
var isAllDiscrete = ((xAxis && yAxis && isDiscrete(xValues) && isDiscrete(yValues)) ||
(!xAxis && isDiscrete(yValues)) ||
(!yAxis && isDiscrete(xValues)));
if (isAllDiscrete) {
rows = setDiscreteScatterData(data);
} else {
rows = data.rows;
}
if (!group && isAllDiscrete) {
grpName = 'count';
} else if (!group && !size) {
if (xAxis && yAxis) {
grpName = '(' + xAxis.name + ', ' + yAxis.name + ')';
} else if (xAxis && !yAxis) {
grpName = xAxis.name;
} else if (!xAxis && yAxis) {
grpName = yAxis.name;
}
} else if (!group && size) {
grpName = size.name;
}
for (i = 0; i < rows.length; i++) {
row = rows[i];
if (xAxis) {
xValue = row[xAxis.index];
}
if (yAxis) {
yValue = row[yAxis.index];
}
if (group) {
grpName = row[group.index];
}
var sz = (isAllDiscrete) ? row[row.length-1] : ((size) ? row[size.index] : 1);
if (grpNameIndex[grpName] === undefined) {
grpIndexValue[grpIdx] = grpName;
grpNameIndex[grpName] = grpIdx++;
}
if (xAxis && rowNameIndex[xValue] === undefined) {
rowIndexValue[rowIdx] = xValue;
rowNameIndex[xValue] = rowIdx++;
}
if (yAxis && colNameIndex[yValue] === undefined) {
colIndexValue[colIdx] = yValue;
colNameIndex[yValue] = colIdx++;
}
if (!d3g[grpNameIndex[grpName]]) {
d3g[grpNameIndex[grpName]] = {
key : grpName,
values : []
};
}
d3g[grpNameIndex[grpName]].values.push({
x : xAxis ? (isNaN(xValue) ? rowNameIndex[xValue] : parseFloat(xValue)) : 0,
y : yAxis ? (isNaN(yValue) ? colNameIndex[yValue] : parseFloat(yValue)) : 0,
size : isNaN(parseFloat(sz))? 1 : parseFloat(sz)
});
}
return {
xLabels : rowIndexValue,
yLabels : colIndexValue,
d3g : d3g
};
};
var isDiscrete = function(field) {
var getUnique = function(f) {
var uniqObj = {};
var uniqArr = [];
var j = 0;
for (var i = 0; i < f.length; i++) {
var item = f[i];
if(uniqObj[item] !== 1) {
uniqObj[item] = 1;
uniqArr[j++] = item;
}
}
return uniqArr;
};
for (var i = 0; i < field.length; i++) {
if(isNaN(parseFloat(field[i])) &&
(typeof field[i] === 'string' || field[i] instanceof String)) {
return true;
}
}
var threshold = 0.05;
var unique = getUnique(field);
if (unique.length/field.length < threshold) {
return true;
} else {
return false;
}
};
$scope.isValidSizeOption = function (options, rows) {
var xValues = [];
var yValues = [];
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var size = row[options.size.index];
//check if the field is numeric
if (isNaN(parseFloat(size)) || !isFinite(size)) {
return false;
}
if (options.xAxis) {
var x = row[options.xAxis.index];
xValues[i] = x;
}
if (options.yAxis) {
var y = row[options.yAxis.index];
yValues[i] = y;
}
}
//check if all existing fields are discrete
var isAllDiscrete = ((options.xAxis && options.yAxis && isDiscrete(xValues) && isDiscrete(yValues)) ||
(!options.xAxis && isDiscrete(yValues)) ||
(!options.yAxis && isDiscrete(xValues)));
if (isAllDiscrete) {
return false;
}
return true;
};
$scope.resizeParagraph = function(width, height) {
if ($scope.paragraph.config.colWidth !== width) {
$scope.paragraph.config.colWidth = width;
$scope.changeColWidth();
$timeout(function() {
autoAdjustEditorHeight($scope.paragraph.id + '_editor');
$scope.changeHeight(height);
}, 200);
} else {
$scope.changeHeight(height);
}
};
$scope.changeHeight = function(height) {
var newParams = angular.copy($scope.paragraph.settings.params);
var newConfig = angular.copy($scope.paragraph.config);
newConfig.graph.height = height;
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
};
/** Utility function */
if (typeof String.prototype.startsWith !== 'function') {
String.prototype.startsWith = function(str) {
return this.slice(0, str.length) === str;
};
}
$scope.goToSingleParagraph = function () {
var noteId = $route.current.pathParams.noteId;
var redirectToUrl = location.protocol + '//' + location.host + location.pathname + '#/notebook/' + noteId + '/paragraph/' + $scope.paragraph.id+'?asIframe';
$window.open(redirectToUrl);
};
});