You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by co...@apache.org on 2016/10/03 08:58:34 UTC

[2/3] zeppelin git commit: [Zeppelin-1496] Apply Zeppelin-Web Good Practice Guide #1 to the code

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/a9e7bc38/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
index 971f515..6eaba82 100644
--- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
+++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
@@ -12,2740 +12,2762 @@
  * limitations under the License.
  */
 'use strict';
+(function() {
+
+  angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl);
+
+  ParagraphCtrl.$inject = [
+    '$scope',
+    '$rootScope',
+    '$route',
+    '$window',
+    '$routeParams',
+    '$location',
+    '$timeout',
+    '$compile',
+    '$http',
+    '$q',
+    'websocketMsgSrv',
+    'baseUrlSrv',
+    'ngToast',
+    'saveAsService',
+    'esriLoader'
+  ];
+
+  function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $location,
+                         $timeout, $compile, $http, $q, websocketMsgSrv,
+                         baseUrlSrv, ngToast, saveAsService, esriLoader) {
+    var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_';
+    $scope.parentNote = null;
+    $scope.paragraph = null;
+    $scope.originalText = '';
+    $scope.editor = null;
+    $scope.magic = null;
+
+    var paragraphScope = $rootScope.$new(true, $rootScope);
+
+    // to keep backward compatibility
+    $scope.compiledScope = paragraphScope;
+
+    paragraphScope.z = {
+      // z.runParagraph('20150213-231621_168813393')
+      runParagraph: function(paragraphId) {
+        if (paragraphId) {
+          var filtered = $scope.parentNote.paragraphs.filter(function(x) {
+            return x.id === paragraphId;});
+          if (filtered.length === 1) {
+            var paragraph = filtered[0];
+            websocketMsgSrv.runParagraph(paragraph.id, paragraph.title, paragraph.text,
+                paragraph.config, paragraph.settings.params);
+          } else {
+            ngToast.danger({content: 'Cannot find a paragraph with id \'' + paragraphId + '\'',
+              verticalPosition: 'top', dismissOnTimeout: false});
+          }
+        } else {
+          ngToast.danger({
+            content: 'Please provide a \'paragraphId\' when calling z.runParagraph(paragraphId)',
+            verticalPosition: 'top', dismissOnTimeout: false});
+        }
+      },
 
-angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $rootScope, $route, $window,
-                                                                      $routeParams, $location, $timeout, $compile,
-                                                                      $http, $q, websocketMsgSrv, baseUrlSrv, ngToast,
-                                                                      saveAsService, esriLoader) {
-  var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_';
-  $scope.parentNote = null;
-  $scope.paragraph = null;
-  $scope.originalText = '';
-  $scope.editor = null;
-  $scope.magic = null;
-
-  var paragraphScope = $rootScope.$new(true, $rootScope);
-
-  // to keep backward compatibility
-  $scope.compiledScope = paragraphScope;
-
-  paragraphScope.z = {
-    // z.runParagraph('20150213-231621_168813393')
-    runParagraph: function(paragraphId) {
-      if (paragraphId) {
-        var filtered = $scope.parentNote.paragraphs.filter(function(x) {
-          return x.id === paragraphId;});
-        if (filtered.length === 1) {
-          var paragraph = filtered[0];
-          websocketMsgSrv.runParagraph(paragraph.id, paragraph.title, paragraph.text,
-              paragraph.config, paragraph.settings.params);
+      // Example: z.angularBind('my_var', 'Test Value', '20150213-231621_168813393')
+      angularBind: function(varName, value, paragraphId) {
+        // Only push to server if there paragraphId is defined
+        if (paragraphId) {
+          websocketMsgSrv.clientBindAngularObject($routeParams.noteId, varName, value, paragraphId);
         } else {
-          ngToast.danger({content: 'Cannot find a paragraph with id \'' + paragraphId + '\'',
+          ngToast.danger({
+            content: 'Please provide a \'paragraphId\' when calling ' +
+            'z.angularBind(varName, value, \'PUT_HERE_PARAGRAPH_ID\')',
+            verticalPosition: 'top', dismissOnTimeout: false});
+        }
+      },
+
+      // Example: z.angularUnBind('my_var', '20150213-231621_168813393')
+      angularUnbind: function(varName, paragraphId) {
+        // Only push to server if paragraphId is defined
+        if (paragraphId) {
+          websocketMsgSrv.clientUnbindAngularObject($routeParams.noteId, varName, paragraphId);
+        } else {
+          ngToast.danger({
+            content: 'Please provide a \'paragraphId\' when calling ' +
+            'z.angularUnbind(varName, \'PUT_HERE_PARAGRAPH_ID\')',
             verticalPosition: 'top', dismissOnTimeout: false});
         }
-      } else {
-        ngToast.danger({
-          content: 'Please provide a \'paragraphId\' when calling z.runParagraph(paragraphId)',
-          verticalPosition: 'top', dismissOnTimeout: false});
       }
-    },
+    };
 
-    // Example: z.angularBind('my_var', 'Test Value', '20150213-231621_168813393')
-    angularBind: function(varName, value, paragraphId) {
-      // Only push to server if there paragraphId is defined
-      if (paragraphId) {
-        websocketMsgSrv.clientBindAngularObject($routeParams.noteId, varName, value, paragraphId);
-      } else {
-        ngToast.danger({
-          content: 'Please provide a \'paragraphId\' when calling ' +
-          'z.angularBind(varName, value, \'PUT_HERE_PARAGRAPH_ID\')',
-          verticalPosition: 'top', dismissOnTimeout: false});
-      }
-    },
-
-    // Example: z.angularUnBind('my_var', '20150213-231621_168813393')
-    angularUnbind: function(varName, paragraphId) {
-      // Only push to server if paragraphId is defined
-      if (paragraphId) {
-        websocketMsgSrv.clientUnbindAngularObject($routeParams.noteId, varName, paragraphId);
-      } else {
-        ngToast.danger({
-          content: 'Please provide a \'paragraphId\' when calling ' +
-          'z.angularUnbind(varName, \'PUT_HERE_PARAGRAPH_ID\')',
-          verticalPosition: 'top', dismissOnTimeout: false});
+    var angularObjectRegistry = {};
+
+    // Controller init
+    $scope.init = function(newParagraph, note) {
+      $scope.paragraph = newParagraph;
+      $scope.parentNote = note;
+      $scope.originalText = angular.copy(newParagraph.text);
+      $scope.chart = {};
+      $scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain'];
+      $scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
+      $scope.paragraphFocused = false;
+      if (newParagraph.focus) {
+        $scope.paragraphFocused = true;
+      }
+      if (!$scope.paragraph.config) {
+        $scope.paragraph.config = {};
       }
-    }
-  };
-
-  var angularObjectRegistry = {};
-
-  // Controller init
-  $scope.init = function(newParagraph, note) {
-    $scope.paragraph = newParagraph;
-    $scope.parentNote = note;
-    $scope.originalText = angular.copy(newParagraph.text);
-    $scope.chart = {};
-    $scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain'];
-    $scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
-    $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();
-    } else if ($scope.getResultType() === 'TEXT') {
-      $scope.renderText();
-    }
+      initializeDefault();
 
-    getApplicationStates();
-    getSuggestions();
+      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();
+      } else if ($scope.getResultType() === 'TEXT') {
+        $scope.renderText();
+      }
 
-    var activeApp =  _.get($scope.paragraph.config, 'helium.activeApp');
-    if (activeApp) {
-      var app = _.find($scope.apps, {id: activeApp});
-      renderApp(app);
-    }
-  };
+      getApplicationStates();
+      getSuggestions();
 
-  $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);
+      var activeApp =  _.get($scope.paragraph.config, 'helium.activeApp');
+      if (activeApp) {
+        var app = _.find($scope.apps, {id: activeApp});
+        renderApp(app);
+      }
+    };
+
+    $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);
+            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())(paragraphScope);
+          } catch (err) {
+            console.log('ANGULAR rendering error %o', err);
+          }
+        } else {
+          $timeout(retryRenderer, 10);
+        }
+      };
+      $timeout(retryRenderer);
+    };
+
+    $scope.renderText = function() {
+      var retryRenderer = function() {
+
+        var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
+        if (textEl.length) {
+          // clear all lines before render
+          $scope.clearTextOutput();
+
+          if ($scope.paragraph.result && $scope.paragraph.result.msg) {
+            $scope.appendTextOutput($scope.paragraph.result.msg);
+          }
+
+          angular.element('#p' + $scope.paragraph.id + '_text').bind('mousewheel', function(e) {
+            $scope.keepScrollDown = false;
           });
-        } catch (err) {
-          console.log('HTML rendering error %o', err);
+          $scope.flushStreamingOutput = true;
+        } else {
+          $timeout(retryRenderer, 10);
         }
-      } else {
-        $timeout(retryRenderer, 10);
-      }
+      };
+      $timeout(retryRenderer);
     };
-    $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);
+    $scope.clearTextOutput = function() {
+      var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
+      if (textEl.length) {
+        textEl.children().remove();
+      }
+    };
 
-          $compile(angular.element('#p' + $scope.paragraph.id + '_angular').contents())(paragraphScope);
-        } catch (err) {
-          console.log('ANGULAR rendering error %o', err);
+    $scope.appendTextOutput = function(msg) {
+      var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
+      if (textEl.length) {
+        var lines = msg.split('\n');
+        for (var i = 0; i < lines.length; i++) {
+          textEl.append(angular.element('<div></div>').text(lines[i]));
         }
+      }
+      if ($scope.keepScrollDown) {
+        var doc = angular.element('#p' + $scope.paragraph.id + '_text');
+        doc[0].scrollTop = doc[0].scrollHeight;
+      }
+    };
+
+    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.graph.map) {
+        config.graph.map = {};
+      }
+
+      if (!config.graph.map.baseMapType) {
+        config.graph.map.baseMapType = $scope.baseMapOption[0];
+      }
+
+      if (!config.graph.map.isOnline) {
+        config.graph.map.isOnline = true;
+      }
+
+      if (!config.graph.map.pinCols) {
+        config.graph.map.pinCols = [];
+      }
+
+      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), '*');
+      }
+    });
+
+    var isEmpty = function(object) {
+      return !object;
+    };
+
+    $scope.isRunning = function() {
+      if ($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING') {
+        return true;
       } else {
-        $timeout(retryRenderer, 10);
+        return false;
       }
     };
-    $timeout(retryRenderer);
-  };
 
-  $scope.renderText = function() {
-    var retryRenderer = function() {
+    $scope.cancelParagraph = function() {
+      console.log('Cancel %o', $scope.paragraph.id);
+      websocketMsgSrv.cancelParagraphRun($scope.paragraph.id);
+    };
 
-      var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
-      if (textEl.length) {
-        // clear all lines before render
-        $scope.clearTextOutput();
+    $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;
+    };
 
-        if ($scope.paragraph.result && $scope.paragraph.result.msg) {
-          $scope.appendTextOutput($scope.paragraph.result.msg);
+    $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);
+    };
 
-        angular.element('#p' + $scope.paragraph.id + '_text').bind('mousewheel', function(e) {
-          $scope.keepScrollDown = false;
+    $scope.moveDown = function() {
+      $scope.$emit('moveParagraphDown', $scope.paragraph.id);
+    };
+
+    $scope.insertNew = function(position) {
+      $scope.$emit('insertParagraph', $scope.paragraph.id, position || 'below');
+    };
+
+    $scope.removeParagraph = function() {
+      var paragraphs = angular.element('div[id$="_paragraphColumn_main"]');
+      if (paragraphs[paragraphs.length - 1].id.startsWith($scope.paragraph.id)) {
+        BootstrapDialog.alert({
+          closable: true,
+          message: 'The last paragraph can\'t be deleted.'
         });
-        $scope.flushStreamingOutput = true;
       } else {
-        $timeout(retryRenderer, 10);
+        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);
+            }
+          }
+        });
       }
     };
-    $timeout(retryRenderer);
-  };
 
-  $scope.clearTextOutput = function() {
-    var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
-    if (textEl.length) {
-      textEl.children().remove();
-    }
-  };
+    $scope.clearParagraphOutput = function() {
+      websocketMsgSrv.clearParagraphOutput($scope.paragraph.id);
+    };
 
-  $scope.appendTextOutput = function(msg) {
-    var textEl = angular.element('#p' + $scope.paragraph.id + '_text');
-    if (textEl.length) {
-      var lines = msg.split('\n');
-      for (var i = 0; i < lines.length; i++) {
-        textEl.append(angular.element('<div></div>').text(lines[i]));
+    $scope.toggleEditor = function() {
+      if ($scope.paragraph.config.editorHide) {
+        $scope.openEditor();
+      } else {
+        $scope.closeEditor();
       }
-    }
-    if ($scope.keepScrollDown) {
-      var doc = angular.element('#p' + $scope.paragraph.id + '_text');
-      doc[0].scrollTop = doc[0].scrollHeight;
-    }
-  };
+    };
 
-  var initializeDefault = function() {
-    var config = $scope.paragraph.config;
+    $scope.closeEditor = function() {
+      console.log('close the note');
 
-    if (!config.colWidth) {
-      config.colWidth = 12;
-    }
+      var newParams = angular.copy($scope.paragraph.settings.params);
+      var newConfig = angular.copy($scope.paragraph.config);
+      newConfig.editorHide = true;
 
-    if (!config.graph) {
-      config.graph = {};
-    }
+      commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+    };
 
-    if (!config.graph.mode) {
-      config.graph.mode = 'table';
-    }
+    $scope.openEditor = function() {
+      console.log('open the note');
 
-    if (!config.graph.height) {
-      config.graph.height = 300;
-    }
+      var newParams = angular.copy($scope.paragraph.settings.params);
+      var newConfig = angular.copy($scope.paragraph.config);
+      newConfig.editorHide = false;
 
-    if (!config.graph.optionOpen) {
-      config.graph.optionOpen = false;
-    }
+      commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+    };
 
-    if (!config.graph.keys) {
-      config.graph.keys = [];
-    }
+    $scope.closeTable = function() {
+      console.log('close the output');
 
-    if (!config.graph.values) {
-      config.graph.values = [];
-    }
+      var newParams = angular.copy($scope.paragraph.settings.params);
+      var newConfig = angular.copy($scope.paragraph.config);
+      newConfig.tableHide = true;
 
-    if (!config.graph.groups) {
-      config.graph.groups = [];
-    }
+      commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+    };
 
-    if (!config.graph.scatter) {
-      config.graph.scatter = {};
-    }
+    $scope.openTable = function() {
+      console.log('open the output');
 
-    if (!config.graph.map) {
-      config.graph.map = {};
-    }
+      var newParams = angular.copy($scope.paragraph.settings.params);
+      var newConfig = angular.copy($scope.paragraph.config);
+      newConfig.tableHide = false;
 
-    if (!config.graph.map.baseMapType) {
-      config.graph.map.baseMapType = $scope.baseMapOption[0];
-    }
+      commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+    };
 
-    if (!config.graph.map.isOnline) {
-      config.graph.map.isOnline = true;
-    }
+    $scope.showTitle = function() {
+      var newParams = angular.copy($scope.paragraph.settings.params);
+      var newConfig = angular.copy($scope.paragraph.config);
+      newConfig.title = true;
 
-    if (!config.graph.map.pinCols) {
-      config.graph.map.pinCols = [];
-    }
+      commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+    };
 
-    if (config.enabled === undefined) {
-      config.enabled = true;
-    }
-  };
+    $scope.hideTitle = function() {
+      var newParams = angular.copy($scope.paragraph.settings.params);
+      var newConfig = angular.copy($scope.paragraph.config);
+      newConfig.title = false;
 
-  $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), '*');
-    }
-  });
+      commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+    };
 
-  var isEmpty = function(object) {
-    return !object;
-  };
+    $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(width) {
+      angular.element('.navbar-right.open').removeClass('open');
+      if (!width || width !== $scope.paragraph.config.colWidth) {
+        if (width) {
+          $scope.paragraph.config.colWidth = width;
+        }
+        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;
+      }
 
-  $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.loadForm = function(formulaire, params) {
+      var value = formulaire.defaultValue;
+      if (params[formulaire.name]) {
+        value = params[formulaire.name];
       }
-    }
-  };
-
-  $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() {
-    var paragraphs = angular.element('div[id$="_paragraphColumn_main"]');
-    if (paragraphs[paragraphs.length - 1].id.startsWith($scope.paragraph.id)) {
-      BootstrapDialog.alert({
-        closable: true,
-        message: 'The last paragraph can\'t be deleted.'
-      });
-    } else {
-      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.paragraph.settings.params[formulaire.name] = value;
+    };
+
+    $scope.toggleCheckbox = function(formulaire, option) {
+      var idx = $scope.paragraph.settings.params[formulaire.name].indexOf(option.value);
+      if (idx > -1) {
+        $scope.paragraph.settings.params[formulaire.name].splice(idx, 1);
+      } else {
+        $scope.paragraph.settings.params[formulaire.name].push(option.value);
+      }
+    };
+
+    $scope.aceChanged = function() {
+      $scope.dirtyText = $scope.editor.getSession().getValue();
+      $scope.startSaveTimer();
+      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;
+      $scope.editor.on('input', $scope.aceChanged);
+      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');
+        $scope.editor.setReadOnly($scope.isRunning());
+        if ($scope.paragraphFocused) {
+          $scope.editor.focus();
+          $scope.goToEnd();
         }
-      });
-    }
-  };
 
-  $scope.clearParagraphOutput = function() {
-    websocketMsgSrv.clearParagraphOutput($scope.paragraph.id);
-  };
+        autoAdjustEditorHeight(_editor.container.id);
+        angular.element(window).resize(function() {
+          autoAdjustEditorHeight(_editor.container.id);
+        });
 
-  $scope.toggleEditor = function() {
-    if ($scope.paragraph.config.editorHide) {
-      $scope.openEditor();
-    } else {
-      $scope.closeEditor();
-    }
-  };
+        if (navigator.appVersion.indexOf('Mac') !== -1) {
+          $scope.editor.setKeyboardHandler('ace/keyboard/emacs');
+          $rootScope.isMac = true;
+        } else if (navigator.appVersion.indexOf('Win') !== -1 ||
+                   navigator.appVersion.indexOf('X11') !== -1 ||
+                   navigator.appVersion.indexOf('Linux') !== -1) {
+          $rootScope.isMac = false;
+          // not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows.
+        }
 
-  $scope.closeEditor = function() {
-    console.log('close the note');
+        var remoteCompleter = {
+          getCompletions: function(editor, session, pos, prefix, callback) {
+            if (!$scope.editor.isFocused()) {
+              return;
+            }
 
-    var newParams = angular.copy($scope.paragraph.settings.params);
-    var newConfig = angular.copy($scope.paragraph.config);
-    newConfig.editorHide = true;
+            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.name,
+                    value: v.value,
+                    score: 300
+                  });
+                }
+                callback(null, completions);
+              }
+            });
+          }
+        };
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+        langTools.setCompleters([remoteCompleter, langTools.keyWordCompleter, langTools.snippetCompleter,
+          langTools.textCompleter]);
 
-  $scope.openEditor = function() {
-    console.log('open the note');
+        $scope.editor.setOptions({
+          enableBasicAutocompletion: true,
+          enableSnippets: false,
+          enableLiveAutocompletion: false
+        });
 
-    var newParams = angular.copy($scope.paragraph.settings.params);
-    var newConfig = angular.copy($scope.paragraph.config);
-    newConfig.editorHide = false;
+        $scope.handleFocus = function(value, isDigestPass) {
+          $scope.paragraphFocused = value;
+          if (isDigestPass === false || isDigestPass === undefined) {
+            // Protect against error in case digest is already running
+            $timeout(function() {
+              // Apply changes since they come from 3rd party library
+              $scope.$digest();
+            });
+          }
+        };
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+        $scope.editor.on('focus', function() {
+          $scope.handleFocus(true);
+        });
 
-  $scope.closeTable = function() {
-    console.log('close the output');
+        $scope.editor.on('blur', function() {
+          $scope.handleFocus(false);
+        });
 
-    var newParams = angular.copy($scope.paragraph.settings.params);
-    var newConfig = angular.copy($scope.paragraph.config);
-    newConfig.tableHide = true;
+        $scope.editor.getSession().on('change', function(e, editSession) {
+          autoAdjustEditorHeight(_editor.container.id);
+        });
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+        setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue());
 
-  $scope.openTable = function() {
-    console.log('open the output');
+        // 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);
+        $scope.editor.commands.removeCommand('showSettingsMenu');
+
+        // autocomplete on 'ctrl+.'
+        $scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete');
+        $scope.editor.commands.bindKey('ctrl-space', null);
+
+        var keyBindingEditorFocusAction = function(scrollValue) {
+          var numRows = $scope.editor.getSession().getLength();
+          var currentRow = $scope.editor.getCursorPosition().row;
+          if (currentRow === 0 && scrollValue <= 0) {
+            // move focus to previous paragraph
+            $scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id);
+          } else if (currentRow === numRows - 1 && scrollValue >= 0) {
+            $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id);
+          } else {
+            $scope.scrollToCursor($scope.paragraph.id, scrollValue);
+          }
+        };
 
-    var newParams = angular.copy($scope.paragraph.settings.params);
-    var newConfig = angular.copy($scope.paragraph.config);
-    newConfig.tableHide = false;
+        // 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);
+            }
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+            var ROW_UP = -1;
+            var ROW_DOWN = 1;
 
-  $scope.showTitle = function() {
-    var newParams = angular.copy($scope.paragraph.settings.params);
-    var newConfig = angular.copy($scope.paragraph.config);
-    newConfig.title = true;
+            switch (keyCode) {
+              case 38:
+                keyBindingEditorFocusAction(ROW_UP);
+                break;
+              case 80:
+                if (e.ctrlKey && !e.altKey) {
+                  keyBindingEditorFocusAction(ROW_UP);
+                }
+                break;
+              case 40:
+                keyBindingEditorFocusAction(ROW_DOWN);
+                break;
+              case 78:
+                if (e.ctrlKey && !e.altKey) {
+                  keyBindingEditorFocusAction(ROW_DOWN);
+                }
+                break;
+            }
+          }
+          this.origOnCommandKey(e, hashId, keyCode);
+        };
+      }
+    };
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+    var getAndSetEditorSetting = function(session, interpreterName) {
+      var deferred = $q.defer();
+      websocketMsgSrv.getEditorSetting($scope.paragraph.id, interpreterName);
+      $timeout(
+        $scope.$on('editorSetting', function(event, data) {
+          if ($scope.paragraph.id === data.paragraphId) {
+            deferred.resolve(data);
+          }
+        }
+      ), 1000);
+      deferred.promise.then(function(editorSetting) {
+        if (!_.isEmpty(editorSetting.editor)) {
+          var mode = 'ace/mode/' + editorSetting.editor.language;
+          $scope.paragraph.config.editorMode = mode;
+          session.setMode(mode);
+        }
+      });
+    };
+
+    var setParagraphMode = function(session, paragraphText, pos) {
+      // Evaluate the mode only if the the position is undefined
+      // or the first 30 characters of the paragraph have been modified
+      // or cursor position is at beginning of second line.(in case user hit enter after typing %magic)
+      if ((typeof pos === 'undefined') || (pos.row === 0 && pos.column < 30) || (pos.row === 1 && pos.column === 0)) {
+        // If paragraph loading, use config value if exists
+        if ((typeof pos === 'undefined') && $scope.paragraph.config.editorMode) {
+          session.setMode($scope.paragraph.config.editorMode);
+        } else {
+          var magic;
+          // set editor mode to default interpreter syntax if paragraph text doesn't start with '%'
+          // TODO(mina): dig into the cause what makes interpreterBindings has no element
+          if (!paragraphText.startsWith('%') && ((typeof pos !== 'undefined') && pos.row === 0 && pos.column === 1) ||
+              (typeof pos === 'undefined') && $scope.$parent.interpreterBindings.length !== 0) {
+            magic = $scope.$parent.interpreterBindings[0].name;
+            getAndSetEditorSetting(session, magic);
+          } else {
+            var replNameRegexp = /%(.+?)\s/g;
+            var match = replNameRegexp.exec(paragraphText);
+            if (match && $scope.magic !== match[1]) {
+              magic = match[1].trim();
+              $scope.magic = magic;
+              getAndSetEditorSetting(session, magic);
+            }
+          }
+        }
+      }
+    };
 
-  $scope.hideTitle = function() {
-    var newParams = angular.copy($scope.paragraph.settings.params);
-    var newConfig = angular.copy($scope.paragraph.config);
-    newConfig.title = false;
+    var autoAdjustEditorHeight = function(id) {
+      var editor = $scope.editor;
+      var height = editor.getSession().getScreenLength() * editor.renderer.lineHeight +
+        editor.renderer.scrollBar.getWidth();
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+      angular.element('#' + id).height(height.toString() + 'px');
+      editor.resize();
+    };
 
-  $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);
-  };
+    $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);
+      }
+    });
 
-  $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);
+    /** 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;
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+      var documentHeight = angular.element(document).height();
+      var windowHeight = angular.element(window).height();  // actual viewport height
 
-  $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);
+      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);
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+      var calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight * lastCursorMove;
 
-  $scope.columnWidthClass = function(n) {
-    if ($scope.asIframe) {
-      return 'col-md-12';
-    } else {
-      return 'col-md-' + n;
-    }
-  };
+      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);
 
-  $scope.changeColWidth = function(width) {
-    angular.element('.navbar-right.open').removeClass('open');
-    if (!width || width !== $scope.paragraph.config.colWidth) {
-      if (width) {
-        $scope.paragraph.config.colWidth = width;
+        if (scrollTargetPos > documentHeight) {
+          scrollTargetPos = documentHeight;
+        }
       }
-      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);
+      // cancel previous scroll animation
+      var bodyEl = angular.element('body');
+      bodyEl.stop();
+      bodyEl.finish();
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+      // scroll to scrollTargetPos
+      bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration: 100});
+    };
 
-  $scope.toggleOutput = function() {
-    var newConfig = angular.copy($scope.paragraph.config);
-    newConfig.tableHide = !newConfig.tableHide;
-    var newParams = angular.copy($scope.paragraph.settings.params);
+    $scope.getEditorValue = function() {
+      return $scope.editor.getValue();
+    };
 
-    commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
-  };
+    $scope.getProgress = function() {
+      return ($scope.currentProgress) ? $scope.currentProgress : 0;
+    };
 
-  $scope.toggleLineWithFocus = function() {
-    var mode = $scope.getGraphMode();
+    $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 user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user;
+      var desc = 'Took ' + moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]') +
+        '. Last updated by ' + user + ' at ' + moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A') + '.';
+      if ($scope.isResultOutdated()) {
+        desc += ' (outdated)';
+      }
+      return desc;
+    };
 
-    if (mode === 'lineWithFocusChart') {
-      $scope.setGraphMode('lineChart', true);
-      return true;
-    }
+    $scope.getElapsedTime = function() {
+      return 'Started ' + moment($scope.paragraph.dateStarted).fromNow() + '.';
+    };
 
-    if (mode === 'lineChart') {
-      $scope.setGraphMode('lineWithFocusChart', true);
-      return true;
-    }
+    $scope.isResultOutdated = function() {
+      var pdata = $scope.paragraph;
+      if (pdata.dateUpdated !== undefined && Date.parse(pdata.dateUpdated) > Date.parse(pdata.dateStarted)) {
+        return true;
+      }
+      return false;
+    };
 
-    return false;
-  };
+    $scope.goToEnd = function() {
+      $scope.editor.navigateFileEnd();
+    };
 
-  $scope.loadForm = function(formulaire, params) {
-    var value = formulaire.defaultValue;
-    if (params[formulaire.name]) {
-      value = params[formulaire.name];
-    }
+    $scope.getResultType = function(paragraph) {
+      var pdata = (paragraph) ? paragraph : $scope.paragraph;
+      if (pdata.result && pdata.result.type) {
+        return pdata.result.type;
+      } else {
+        return 'TEXT';
+      }
+    };
 
-    $scope.paragraph.settings.params[formulaire.name] = value;
-  };
+    $scope.getBase64ImageSrc = function(base64Data) {
+      return 'data:image/png;base64,' + base64Data;
+    };
 
-  $scope.toggleCheckbox = function(formulaire, option) {
-    var idx = $scope.paragraph.settings.params[formulaire.name].indexOf(option.value);
-    if (idx > -1) {
-      $scope.paragraph.settings.params[formulaire.name].splice(idx, 1);
-    } else {
-      $scope.paragraph.settings.params[formulaire.name].push(option.value);
-    }
-  };
-
-  $scope.aceChanged = function() {
-    $scope.dirtyText = $scope.editor.getSession().getValue();
-    $scope.startSaveTimer();
-    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;
-    $scope.editor.on('input', $scope.aceChanged);
-    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');
-      $scope.editor.setReadOnly($scope.isRunning());
-      if ($scope.paragraphFocused) {
-        $scope.editor.focus();
-        $scope.goToEnd();
+    $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';
       }
+    };
 
-      autoAdjustEditorHeight(_editor.container.id);
-      angular.element(window).resize(function() {
-        autoAdjustEditorHeight(_editor.container.id);
-      });
+    $scope.parseTableCell = function(cell) {
+      if (!isNaN(cell)) {
+        if (cell.length === 0 || Number(cell) > Number.MAX_SAFE_INTEGER || Number(cell) < Number.MIN_SAFE_INTEGER) {
+          return cell;
+        } else {
+          return Number(cell);
+        }
+      }
+      var d = moment(cell);
+      if (d.isValid()) {
+        return d;
+      }
+      return cell;
+    };
 
-      if (navigator.appVersion.indexOf('Mac') !== -1) {
-        $scope.editor.setKeyboardHandler('ace/keyboard/emacs');
-        $rootScope.isMac = true;
-      } else if (navigator.appVersion.indexOf('Win') !== -1 ||
-                 navigator.appVersion.indexOf('X11') !== -1 ||
-                 navigator.appVersion.indexOf('Linux') !== -1) {
-        $rootScope.isMac = false;
-        // not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows.
+    $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;
+          }
 
-      var remoteCompleter = {
-        getCompletions: function(editor, session, pos, prefix, callback) {
-          if (!$scope.editor.isFocused()) {
-            return;
+          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 {
+              var parsedCol = $scope.parseTableCell(col);
+              cols.push(parsedCol);
+              cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, value: parsedCol});
+            }
+          }
+          if (i !== 0) {
+            rows.push(cols);
+            array.push(cols2);
           }
+        }
+        result.msgTable = array;
+        result.columnNames = columnNames;
+        result.rows = rows;
+      }
+    };
 
-          pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length;
-          var buf = session.getValue();
+    $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 if (type === 'map') {
+          setMap($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);
 
-          websocketMsgSrv.completion($scope.paragraph.id, buf, pos);
+      // graph options
+      newConfig.graph.mode = newMode;
+
+      // see switchApp()
+      _.set(newConfig, 'helium.activeApp', undefined);
+
+      commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
+    };
 
-          $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.name,
-                  value: v.value,
-                  score: 300
-                });
+    var commitParagraph = function(title, text, config, params) {
+      websocketMsgSrv.commitParagraph($scope.paragraph.id, title, text, config, params);
+    };
+
+    var setTable = function(data, refresh) {
+      var renderTable = function() {
+        var height = $scope.paragraph.config.graph.height;
+        var container = angular.element('#p' + $scope.paragraph.id + '_table').css('height', height).get(0);
+        var resultRows = data.rows;
+        var columnNames = _.pluck(data.columnNames, 'name');
+
+        if ($scope.hot) {
+          $scope.hot.destroy();
+        }
+
+        $scope.hot = new Handsontable(container, {
+          colHeaders: columnNames,
+          data: resultRows,
+          rowHeaders: false,
+          stretchH: 'all',
+          sortIndicator: true,
+          columnSorting: true,
+          contextMenu: false,
+          manualColumnResize: true,
+          manualRowResize: true,
+          readOnly: true,
+          readOnlyCellClassName: '',  // don't apply any special class so we can retain current styling
+          fillHandle: false,
+          fragmentSelection: true,
+          disableVisualSelection: true,
+          cells: function(row, col, prop) {
+            var cellProperties = {};
+            cellProperties.renderer = function(instance, td, row, col, prop, value, cellProperties) {
+              if (value instanceof moment) {
+                td.innerHTML = value._i;
+              } else if (!isNaN(value)) {
+                cellProperties.format = '0,0.[00000]';
+                td.style.textAlign = 'left';
+                Handsontable.renderers.NumericRenderer.apply(this, arguments);
+              } else if (value.length > '%html'.length && '%html ' === value.substring(0, '%html '.length)) {
+                td.innerHTML = value.substring('%html'.length);
+              } else {
+                Handsontable.renderers.TextRenderer.apply(this, arguments);
               }
-              callback(null, completions);
-            }
-          });
+            };
+            return cellProperties;
+          }
+        });
+      };
+
+      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);
 
-      langTools.setCompleters([remoteCompleter, langTools.keyWordCompleter, langTools.snippetCompleter,
-        langTools.textCompleter]);
+    };
 
-      $scope.editor.setOptions({
-        enableBasicAutocompletion: true,
-        enableSnippets: false,
-        enableLiveAutocompletion: false
-      });
+    var groupedThousandsWith3DigitsFormatter = function(x) {
+      return d3.format(',')(d3.round(x, 3));
+    };
 
-      $scope.handleFocus = function(value, isDigestPass) {
-        $scope.paragraphFocused = value;
-        if (isDigestPass === false || isDigestPass === undefined) {
-          // Protect against error in case digest is already running
-          $timeout(function() {
-            // Apply changes since they come from 3rd party library
-            $scope.$digest();
-          });
-        }
-      };
+    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;
+    };
 
-      $scope.editor.on('focus', function() {
-        $scope.handleFocus(true);
-      });
+    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;
+      }
+    };
 
-      $scope.editor.on('blur', function() {
-        $scope.handleFocus(false);
-      });
+    var yAxisTickFormat = function(d) {
+      if (Math.abs(d) >= Math.pow(10,6)) {
+        return customAbbrevFormatter(d);
+      }
+      return groupedThousandsWith3DigitsFormatter(d);
+    };
 
-      $scope.editor.getSession().on('change', function(e, editSession) {
-        autoAdjustEditorHeight(_editor.container.id);
-      });
+    var setD3Chart = function(type, data, refresh) {
+      if (!$scope.chart[type]) {
+        var chart = nv.models[type]();
+        $scope.chart[type] = chart;
+      }
 
-      setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue());
+      var d3g = [];
+      var xLabels;
+      var yLabels;
 
-      // 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;
-    }
-      });
-      */
+      if (type === 'scatterChart') {
+        var scatterData = setScatterChart(data, refresh);
 
-      // remove binding
-      $scope.editor.commands.bindKey('ctrl-alt-n.', null);
-      $scope.editor.commands.removeCommand('showSettingsMenu');
+        xLabels = scatterData.xLabels;
+        yLabels = scatterData.yLabels;
+        d3g = scatterData.d3g;
 
-      // autocomplete on 'ctrl+.'
-      $scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete');
-      $scope.editor.commands.bindKey('ctrl-space', null);
+        $scope.chart[type].xAxis.tickFormat(function(d) {return xAxisTickFormat(d, xLabels);});
+        $scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(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>';
+          }
 
-      var keyBindingEditorFocusAction = function(scrollValue) {
-        var numRows = $scope.editor.getSession().getLength();
-        var currentRow = $scope.editor.getCursorPosition().row;
-        if (currentRow === 0 && scrollValue <= 0) {
-          // move focus to previous paragraph
-          $scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id);
-        } else if (currentRow === numRows - 1 && scrollValue >= 0) {
-          $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id);
-        } else {
-          $scope.scrollToCursor($scope.paragraph.id, scrollValue);
+          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);});
+          if (type === 'stackedAreaChart') {
+            $scope.chart[type].yAxisTickFormat(function(d) {return yAxisTickFormat(d);});
+          } else {
+            $scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d, xLabels);});
+          }
+          $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) {
+        }
+
+        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);
       };
 
-      // 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 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);
+    };
 
-          var ROW_UP = -1;
-          var ROW_DOWN = 1;
+    var setMap = function(data, refresh) {
+      var createPinMapLayer = function(pins, cb) {
+        esriLoader.require(['esri/layers/FeatureLayer'], function(FeatureLayer) {
+          var pinLayer = new FeatureLayer({
+            id: 'pins',
+            spatialReference: $scope.map.spatialReference,
+            geometryType: 'point',
+            source: pins,
+            fields: [],
+            objectIdField: '_ObjectID',
+            renderer: $scope.map.pinRenderer,
+            popupTemplate: {
+              title: '[{_lng}, {_lat}]',
+              content: [{
+                type: 'fields',
+                fieldInfos: []
+              }]
+            }
+          });
 
-          switch (keyCode) {
-            case 38:
-              keyBindingEditorFocusAction(ROW_UP);
-              break;
-            case 80:
-              if (e.ctrlKey && !e.altKey) {
-                keyBindingEditorFocusAction(ROW_UP);
-              }
-              break;
-            case 40:
-              keyBindingEditorFocusAction(ROW_DOWN);
-              break;
-            case 78:
-              if (e.ctrlKey && !e.altKey) {
-                keyBindingEditorFocusAction(ROW_DOWN);
+          // add user-selected pin info fields to popup
+          var pinInfoCols = $scope.paragraph.config.graph.map.pinCols;
+          for (var i = 0; i < pinInfoCols.length; ++i) {
+            pinLayer.popupTemplate.content[0].fieldInfos.push({
+              fieldName: pinInfoCols[i].name,
+              visible: true
+            });
+          }
+          cb(pinLayer);
+        });
+      };
+
+      var getMapPins = function(cb) {
+        esriLoader.require(['esri/geometry/Point'], function(Point, FeatureLayer) {
+          var latCol = $scope.paragraph.config.graph.map.lat;
+          var lngCol = $scope.paragraph.config.graph.map.lng;
+          var pinInfoCols = $scope.paragraph.config.graph.map.pinCols;
+          var pins = [];
+
+          // construct objects for pins
+          if (latCol && lngCol && data.rows) {
+            for (var i = 0; i < data.rows.length; ++i) {
+              var row = data.rows[i];
+              var lng = row[lngCol.index];
+              var lat = row[latCol.index];
+              var pin = {
+                geometry: new Point({
+                  longitude: lng,
+                  latitude: lat,
+                  spatialReference: $scope.map.spatialReference
+                }),
+                attributes: {
+                  _ObjectID: i,
+                  _lng: lng,
+                  _lat: lat
+                }
+              };
+
+              // add pin info from user-selected columns
+              for (var j = 0; j < pinInfoCols.length; ++j) {
+                var col = pinInfoCols[j];
+                pin.attributes[col.name] = row[col.index];
               }
-              break;
+              pins.push(pin);
+            }
+          }
+          cb(pins);
+        });
+      };
+
+      var updateMapPins = function() {
+        var pinLayer = $scope.map.map.findLayerById('pins');
+        $scope.map.popup.close();
+        if (pinLayer) {
+          $scope.map.map.remove(pinLayer);
+        }
+
+        // add pins to map as layer
+        getMapPins(function(pins) {
+          createPinMapLayer(pins, function(pinLayer) {
+            $scope.map.map.add(pinLayer);
+            if (pinLayer.source.length > 0) {
+              $scope.map.goTo(pinLayer.source);
+            }
+          });
+        });
+      };
+
+      var createMap = function(mapdiv) {
+        // prevent zooming with the scroll wheel
+        var disableZoom = function(e) {
+          var evt = e || window.event;
+          evt.cancelBubble = true;
+          evt.returnValue = false;
+          if (evt.stopPropagation) {
+            evt.stopPropagation();
+          }
+        };
+        var eName = window.WheelEvent ? 'wheel' :  // Modern browsers
+                    window.MouseWheelEvent ? 'mousewheel' :  // WebKit and IE
+                    'DOMMouseScroll';  // Old Firefox
+        mapdiv.addEventListener(eName, disableZoom, true);
+
+        esriLoader.require(['esri/views/MapView',
+                            'esri/Map',
+                            'esri/renderers/SimpleRenderer',
+                            'esri/symbols/SimpleMarkerSymbol'],
+                            function(MapView, Map, SimpleRenderer, SimpleMarkerSymbol) {
+          $scope.map = new MapView({
+            container: mapdiv,
+            map: new Map({
+              basemap: $scope.paragraph.config.graph.map.baseMapType.toLowerCase()
+            }),
+            center: [-106.3468, 56.1304],  // Canada (lng, lat)
+            zoom: 2,
+            pinRenderer: new SimpleRenderer({
+              symbol: new SimpleMarkerSymbol({
+                'color': [255, 0, 0, 0.5],
+                'size': 16.5,
+                'outline': {
+                  'color': [0, 0, 0, 1],
+                  'width': 1.125,
+                },
+                // map pin SVG path
+                'path': 'M16,3.5c-4.142,0-7.5,3.358-7.5,7.5c0,4.143,7.5,18.121,7.5,' +
+                        '18.121S23.5,15.143,23.5,11C23.5,6.858,20.143,3.5,16,3.5z ' +
+                        'M16,14.584c-1.979,0-3.584-1.604-3.584-3.584S14.021,7.416,' +
+                        '16,7.416S19.584,9.021,19.584,11S17.979,14.584,16,14.584z'
+              })
+            })
+          });
+
+          $scope.map.on('click', function() {
+            // ArcGIS JS API 4.0 does not account for scrolling or position
+            // changes by default (this is a bug, to be fixed in the upcoming
+            // version 4.1; see https://geonet.esri.com/thread/177238#comment-609681).
+            // This results in a misaligned popup.
+
+            // Workaround: manually set popup position to match position of selected pin
+            if ($scope.map.popup.selectedFeature) {
+              $scope.map.popup.location = $scope.map.popup.selectedFeature.geometry;
+            }
+          });
+          $scope.map.then(updateMapPins);
+        });
+      };
+
+      var checkMapOnline = function(cb) {
+        // are we able to get a response from the ArcGIS servers?
+        var callback = function(res) {
+          var online = (res.status > 0);
+          $scope.paragraph.config.graph.map.isOnline = online;
+          cb(online);
+        };
+        $http.head('//services.arcgisonline.com/arcgis/', {
+          timeout: 5000,
+          withCredentials: false
+        }).then(callback, callback);
+      };
+
+      var renderMap = function() {
+        var mapdiv = angular.element('#p' + $scope.paragraph.id + '_map')
+                            .css('height', $scope.paragraph.config.graph.height)
+                            .children('div').get(0);
+
+        // on chart type change, destroy map to force reinitialization.
+        if ($scope.map && !refresh) {
+          $scope.map.map.destroy();
+          $scope.map.pinRenderer = null;
+          $scope.map = null;
+        }
+
+        var requireMapCSS = function() {
+          var url = '//js.arcgis.com/4.0/esri/css/main.css';
+          if (!angular.element('link[href="' + url + '"]').length) {
+            var link = document.createElement('link');
+            link.rel = 'stylesheet';
+            link.type = 'text/css';
+            link.href = url;
+            angular.element('head').append(link);
+          }
+        };
+
+        var requireMapJS = function(cb) {
+          if (!esriLoader.isLoaded()) {
+            esriLoader.bootstrap({
+              url: '//js.arcgis.com/4.0'
+            }).then(cb);
+          } else {
+            cb();
+          }
+        };
+
+        checkMapOnline(function(online) {
+          // we need an internet connection to use the map
+          if (online) {
+            // create map if not exists.
+            if (!$scope.map) {
+              requireMapCSS();
+              requireMapJS(function() {
+                createMap(mapdiv);
+              });
+            } else {
+              updateMapPins();
+            }
           }
-        }
-        this.origOnCommandKey(e, hashId, keyCode);
+        });
       };
-    }
-  };
 
-  var getAndSetEditorSetting = function(session, interpreterName) {
-    var deferred = $q.defer();
-    websocketMsgSrv.getEditorSetting($scope.paragraph.id, interpreterName);
-    $timeout(
-      $scope.$on('editorSetting', function(event, data) {
-        if ($scope.paragraph.id === data.paragraphId) {
-          deferred.resolve(data);
-        }
-      }
-    ), 1000);
-    deferred.promise.then(function(editorSetting) {
-      if (!_.isEmpty(editorSetting.editor)) {
-        var mode = 'ace/mode/' + editorSetting.editor.language;
-        $scope.paragraph.config.editorMode = mode;
-        session.setMode(mode);
-      }
-    });
-  };
-
-  var setParagraphMode = function(session, paragraphText, pos) {
-    // Evaluate the mode only if the the position is undefined
-    // or the first 30 characters of the paragraph have been modified
-    // or cursor position is at beginning of second line.(in case user hit enter after typing %magic)
-    if ((typeof pos === 'undefined') || (pos.row === 0 && pos.column < 30) || (pos.row === 1 && pos.column === 0)) {
-      // If paragraph loading, use config value if exists
-      if ((typeof pos === 'undefined') && $scope.paragraph.config.editorMode) {
-        session.setMode($scope.paragraph.config.editorMode);
-      } else {
-        var magic;
-        // set editor mode to default interpreter syntax if paragraph text doesn't start with '%'
-        // TODO(mina): dig into the cause what makes interpreterBindings has no element
-        if (!paragraphText.startsWith('%') && ((typeof pos !== 'undefined') && pos.row === 0 && pos.column === 1) ||
-            (typeof pos === 'undefined') && $scope.$parent.interpreterBindings.length !== 0) {
-          magic = $scope.$parent.interpreterBindings[0].name;
-          getAndSetEditorSetting(session, magic);
-        } else {
-          var replNameRegexp = /%(.+?)\s/g;
-          var match = replNameRegexp.exec(paragraphText);
-          if (match && $scope.magic !== match[1]) {
-            magic = match[1].trim();
-            $scope.magic = magic;
-            getAndSetEditorSetting(session, magic);
+      var retryRenderer = function() {
+        if (angular.element('#p' + $scope.paragraph.id + '_map div').length) {
+          try {
+            renderMap();
+          } catch (err) {
+            console.log('Map drawing error %o', err);
           }
+        } else {
+          $timeout(retryRenderer,10);
         }
+      };
+      $timeout(retryRenderer);
+    };
+
+    $scope.setMapBaseMap = function(bm) {
+      $scope.paragraph.config.graph.map.baseMapType = bm;
+      if ($scope.map) {
+        $scope.map.map.basemap = bm.toLowerCase();
       }
-    }
-  };
-
-  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
+    $scope.isGraphMode = function(graphName) {
+      var activeAppId = _.get($scope.paragraph.config, 'helium.activeApp');
+      if ($scope.getResultType() === 'TABLE' && $scope.getGraphMode() === graphName && !activeAppId) {
+        return true;
+      } else {
+        return false;
+      }
+    };
 
-    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);
+    $scope.onGraphOptionChange = function() {
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-    var calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight * lastCursorMove;
+    $scope.removeGraphOptionKeys = function(idx) {
+      $scope.paragraph.config.graph.keys.splice(idx, 1);
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-    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);
+    $scope.removeGraphOptionValues = function(idx) {
+      $scope.paragraph.config.graph.values.splice(idx, 1);
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-      if (scrollTargetPos > documentHeight) {
-        scrollTargetPos = documentHeight;
-      }
-    }
+    $scope.removeGraphOptionGroups = function(idx) {
+      $scope.paragraph.config.graph.groups.splice(idx, 1);
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-    // cancel previous scroll animation
-    var bodyEl = angular.element('body');
-    bodyEl.stop();
-    bodyEl.finish();
+    $scope.setGraphOptionValueAggr = function(idx, aggr) {
+      $scope.paragraph.config.graph.values[idx].aggr = aggr;
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-    // scroll to scrollTargetPos
-    bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration: 100});
-  };
+    $scope.removeScatterOptionXaxis = function(idx) {
+      $scope.paragraph.config.graph.scatter.xAxis = null;
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-  $scope.getEditorValue = function() {
-    return $scope.editor.getValue();
-  };
+    $scope.removeScatterOptionYaxis = function(idx) {
+      $scope.paragraph.config.graph.scatter.yAxis = null;
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-  $scope.getProgress = function() {
-    return ($scope.currentProgress) ? $scope.currentProgress : 0;
-  };
+    $scope.removeScatterOptionGroup = function(idx) {
+      $scope.paragraph.config.graph.scatter.group = null;
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-  $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 user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user;
-    var desc = 'Took ' + moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]') +
-      '. Last updated by ' + user + ' at ' + moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A') + '.';
-    if ($scope.isResultOutdated()) {
-      desc += ' (outdated)';
-    }
-    return desc;
-  };
+    $scope.removeScatterOptionSize = function(idx) {
+      $scope.paragraph.config.graph.scatter.size = null;
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-  $scope.getElapsedTime = function() {
-    return 'Started ' + moment($scope.paragraph.dateStarted).fromNow() + '.';
-  };
+    $scope.removeMapOptionLat = function(idx) {
+      $scope.paragraph.config.graph.map.lat = null;
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-  $scope.isResultOutdated = function() {
-    var pdata = $scope.paragraph;
-    if (pdata.dateUpdated !== undefined && Date.parse(pdata.dateUpdated) > Date.parse(pdata.dateStarted)) {
-      return true;
-    }
-    return false;
-  };
-
-  $scope.goToEnd = function() {
-    $scope.editor.navigateFileEnd();
-  };
-
-  $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.removeMapOptionLng = function(idx) {
+      $scope.paragraph.config.graph.map.lng = null;
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-  $scope.parseTableCell = function(cell) {
-    if (!isNaN(cell)) {
-      if (cell.length === 0 || Number(cell) > Number.MAX_SAFE_INTEGER || Number(cell) < Number.MIN_SAFE_INTEGER) {
-        return cell;
-      } else {
-        return Number(cell);
-      }
-    }
-    var d = moment(cell);
-    if (d.isValid()) {
-      return d;
-    }
-    return cell;
-  };
+    $scope.removeMapOptionPinInfo = function(idx) {
+      $scope.paragraph.config.graph.map.pinCols.splice(idx, 1);
+      clearUnknownColsFromGraphOption();
+      $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
+    };
 
-  $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 {
-            var parsedCol = $scope.parseTableCell(col);
-            cols.push(parsedCol);
-            cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, value: parsedCol});
+    /* 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);
+            }
           }
         }
-        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 if (type === 'map') {
-        setMap($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;
-
-    // see switchApp()
-    _.set(newConfig, 'helium.activeApp', undefined);
-
-    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(data, refresh) {
-    var renderTable = function() {
-      var height = $scope.paragraph.config.graph.height;
-      var container = angular.element('#p' + $scope.paragraph.id + '_table').css('height', height).get(0);
-      var resultRows = data.rows;
-      var columnNames = _.pluck(data.columnNames, 'name');
-
-      if ($scope.hot) {
-        $scope.hot.destroy();
-      }
-
-      $scope.hot = new Handsontable(container, {
-        colHeaders: columnNames,
-        data: resultRows,
-        rowHeaders: false,
-        stretchH: 'all',
-        sortIndicator: true,
-        columnSorting: true,
-        contextMenu: false,
-        manualColumnResize: true,
-        manualRowResize: true,
-        readOnly: true,
-        readOnlyCellClassName: '',  // don't apply any special class so we can retain current styling
-        fillHandle: false,
-        fragmentSelection: true,
-        disableVisualSelection: true,
-        cells: function(row, col, prop) {
-          var cellProperties = {};
-          cellProperties.renderer = function(instance, td, row, col, prop, value, cellProperties) {
-            if (value instanceof moment) {
-              td.innerHTML = value._i;
-            } else if (!isNaN(value)) {
-              cellProperties.format = '0,0.[00000]';
-              td.style.textAlign = 'left';
-              Handsontable.renderers.NumericRenderer.apply(this, arguments);
-            } else if (value.length > '%html'.length && '%html ' === value.substring(0, '%html '.length

<TRUNCATED>