http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/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
new file mode 100644
index 0000000..21e022a
--- /dev/null
+++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
@@ -0,0 +1,1594 @@
+/* global $:false, jQuery:false, ace:false, confirm:false, d3:false, nv:false*/
+/*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.editor = null;
+  var editorMode = {scala: 'ace/mode/scala', sql: 'ace/mode/sql', markdown: 
'ace/mode/markdown'};
+
+  // Controller init
+  $scope.init = function(newParagraph) {
+    $scope.paragraph = newParagraph;
+    $scope.chart = {};
+    $scope.colWidthOption = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
+    $scope.showTitleEditor = false;
+    $scope.paragraphFocused = false;
+
+    if (!$scope.paragraph.config) {
+      $scope.paragraph.config = {};
+    }
+
+    initializeDefault();
+
+    if (!$scope.lastData) {
+      $scope.lastData = {};
+    }
+
+    if ($scope.getResultType() === 'TABLE') {
+      $scope.lastData.settings = angular.copy($scope.paragraph.settings);
+      $scope.lastData.config = angular.copy($scope.paragraph.config);
+      $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 ($('#p'+$scope.paragraph.id+'_html').length) {
+        try {
+          
$('#p'+$scope.paragraph.id+'_html').html($scope.paragraph.result.msg);
+
+          $('#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 = {};
+    }
+  };
+
+  $scope.getIframeDimensions = function () {
+    if ($scope.asIframe) {
+      var paragraphid = '#' + $routeParams.paragraphId + '_container';
+      var height = $(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.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.lastData.settings) ||
+      !angular.equals(data.paragraph.config, $scope.lastData.config)
+    )
+       ) {
+      // store original data for comparison
+      $scope.lastData.settings = angular.copy(data.paragraph.settings);
+      $scope.lastData.config = angular.copy(data.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);
+
+      //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;
+          } else { // if there're local update, keep it.
+            $scope.paragraph.text = $scope.dirtyText;
+          }
+        } else {
+          $scope.paragraph.text = data.paragraph.text;
+        }
+      }
+
+      /** push the rest */
+      $scope.paragraph.aborted = data.paragraph.aborted;
+      $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.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') {
+        $scope.renderHtml();
+      } else if (newType === 'ANGULAR') {
+        $scope.renderAngular();
+      }
+    }
+  });
+
+  $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.dirtyText = undefined;
+  };
+
+  $scope.moveUp = function() {
+    $scope.$emit('moveParagraphUp', $scope.paragraph.id);
+  };
+
+  $scope.moveDown = function() {
+    $scope.$emit('moveParagraphDown', $scope.paragraph.id);
+  };
+
+  $scope.insertNew = function() {
+    $scope.$emit('insertParagraph', $scope.paragraph.id);
+  };
+
+  $scope.removeParagraph = function() {
+    var result = confirm('Do you want to delete this paragraph?');
+    if (result) {
+      console.log('Remove paragraph');
+      websocketMsgSrv.removeParagraph($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.columnWidthClass = function(n) {
+    if ($scope.asIframe) {
+      return 'col-md-12';
+    } else {
+      return 'col-md-' + n;
+    }
+  };
+
+  $scope.changeColWidth = 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.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.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.aceLoaded = function(_editor) {
+    var langTools = ace.require('ace/ext/language_tools');
+    var Range = ace.require('ace/range').Range;
+
+    $scope.editor = _editor;
+    if (_editor.container.id !== '{{paragraph.id}}_editor') {
+      $scope.editor.renderer.setShowGutter(false);
+      $scope.editor.setHighlightActiveLine(false);
+      $scope.editor.setTheme('ace/theme/github');
+      $scope.editor.focus();
+      var hight = $scope.editor.getSession().getScreenLength() * 
$scope.editor.renderer.lineHeight + $scope.editor.renderer.scrollBar.getWidth();
+      setEditorHeight(_editor.container.id, hight);
+
+      $scope.editor.getSession().setUseWrapMode(true);
+      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.editor.setOptions({
+        enableBasicAutocompletion: true,
+        enableSnippets: false,
+        enableLiveAutocompletion:false
+      });
+      var remoteCompleter = {
+          getCompletions : function(editor, session, pos, prefix, callback) {
+              if (!$scope.editor.isFocused() ){ return;}
+
+              var pos = session.getTextRange(new Range(0, 0, pos.row, 
pos.column)).length;
+              var buf = session.getValue();
+              $rootScope.$emit('sendNewEvent', {
+                  op : 'COMPLETION',
+                  data : {
+                      id : $scope.paragraph.id,
+                      buf : buf,
+                      cursor : 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.addCompleter(remoteCompleter);
+
+
+      $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) {
+        hight = editSession.getScreenLength() * 
$scope.editor.renderer.lineHeight + $scope.editor.renderer.scrollBar.getWidth();
+        setEditorHeight(_editor.container.id, hight);
+        $scope.editor.resize();
+      });
+
+      var code = $scope.editor.getSession().getValue();
+      if ( String(code).startsWith('%sql')) {
+        $scope.editor.getSession().setMode(editorMode.sql);
+      } else if ( String(code).startsWith('%md')) {
+        $scope.editor.getSession().setMode(editorMode.markdown);
+      } else {
+        $scope.editor.getSession().setMode(editorMode.scala);
+      }
+
+      $scope.editor.commands.addCommand({
+        name: 'run',
+        bindKey: {win: 'Shift-Enter', mac: 'Shift-Enter'},
+        exec: function(editor) {
+          var editorValue = editor.getValue();
+          if (editorValue) {
+            $scope.runParagraph(editorValue);
+          }
+        },
+        readOnly: false
+      });
+
+      // 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;
+    }
+      });
+      */
+
+      // 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 {
+          var numRows;
+          var currentRow;
+          if (keyCode === 38 || (keyCode === 80 && e.ctrlKey)) {  // 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 if (keyCode === 40 || (keyCode === 78 && e.ctrlKey)) {  // 
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);
+            }
+          }
+        }
+        this.origOnCommandKey(e, hashId, keyCode);
+      };
+    }
+  };
+
+  var setEditorHeight = function(id, height) {
+    $('#' + 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) {
+      return '&nbsp;';
+    }
+    return 'Took ' + (timeMs/1000) + ' seconds';
+  };
+
+  $scope.$on('updateProgress', function(event, data) {
+    if (data.id === $scope.paragraph.id) {
+      $scope.currentProgress = data.progress;
+    }
+  });
+
+  $scope.$on('focusParagraph', function(event, paragraphId) {
+    if ($scope.paragraph.id === paragraphId) {
+      $scope.editor.focus();
+      $('body').scrollTo('#'+paragraphId+'_editor', 300, {offset:-60});
+    }
+  });
+
+  $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;
+      $('#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 c in $scope.paragraph.result.columnNames) {
+        html += '<th>'+$scope.paragraph.result.columnNames[c].name+'</th>';
+      }
+      html += '    </tr>';
+      html += '  </thead>';
+
+      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 += '</table>';
+
+      $('#p' + $scope.paragraph.id + '_table').html(html);
+      $('#p' + $scope.paragraph.id + '_table').perfectScrollbar();
+
+      // set table height
+      var height = $scope.paragraph.config.graph.height;
+      $('#p'+$scope.paragraph.id+'_table').height(height);
+    };
+
+    var retryRenderer = function() {
+      if ($('#p'+$scope.paragraph.id+'_table').length) {
+        try {
+          renderTable();
+        } catch(err) {
+          console.log('Chart drawing error %o', err);
+        }
+      } else {
+        $timeout(retryRenderer,10);
+      }
+    };
+    $timeout(retryRenderer);
+
+  };
+
+  var setD3Chart = function(type, data, refresh) {
+    if (!$scope.chart[type]) {
+      var chart = nv.models[type]();
+      $scope.chart[type] = chart;
+    }
+
+    var d3g = [];
+
+    if (type === 'scatterChart') {
+      var scatterData = setScatterChart(data, refresh);
+
+      var xLabels = scatterData.xLabels;
+      var yLabels = scatterData.yLabels;
+      d3g = scatterData.d3g;
+
+      $scope.chart[type].xAxis.tickFormat(function(d) {
+        if (xLabels[d] && (isNaN(parseFloat(xLabels[d])) || 
!isFinite(xLabels[d]))) {
+          return xLabels[d];
+        } else {
+          return d;
+        }
+      });
+
+      $scope.chart[type].yAxis.tickFormat(function(d) {
+        if (yLabels[d] && (isNaN(parseFloat(yLabels[d])) || 
!isFinite(yLabels[d]))) {
+          return yLabels[d];
+        } else {
+          return d;
+        }
+      });
+
+      // configure how the tooltip looks.
+      $scope.chart[type].tooltipContent(function(key, x, y, 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.
+        .scatter.useVoronoi(false);
+    } 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);
+      } else if (type === 'lineChart' || type === 'stackedAreaChart') {
+        var pivotdata = pivotDataToD3ChartFormat(p, false, true);
+        var xLabels = pivotdata.xLabels;
+        d3g = pivotdata.d3g;
+        $scope.chart[type].xAxis.tickFormat(function(d) {
+          if (xLabels[d] && (isNaN(parseFloat(xLabels[d])) || 
!isFinite(xLabels[d]))) { // to handle string type xlabel
+            return xLabels[d];
+          } else {
+            return d;
+          }
+        });
+        $scope.chart[type].yAxis.axisLabelDistance(50);
+        $scope.chart[type].useInteractiveGuideline(true); // for better UX and 
performance issue. (https://github.com/novus/nvd3/issues/691)
+        $scope.chart[type].forceY([0]); // force y-axis minimum to 0 for line 
chart.
+      }
+    }
+
+    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 ($('#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;
+    var 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 = {};
+    // TODO - This part could use som refactoring - Weird if/else with similar 
actions and variable names
+    for (var colName in colNameIndex) {
+      var 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++) {
+        var colName = d3g[0].values[valueIndex].x;
+        if (!colName) {
+          continue;
+        }
+
+        var 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++) {
+        var colName = d3g[d3gIndex].key;
+        var 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++) {
+          var 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.setGraphHeight = function() {
+    var height = $('#p'+$scope.paragraph.id+'_graph').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 + 
'/#/notebook/' + noteId + '/paragraph/' + $scope.paragraph.id+'?asIframe';
+    $window.open(redirectToUrl);
+  };
+});

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/notebook/paragraph/paragraph.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.html 
b/zeppelin-web/src/app/notebook/paragraph/paragraph.html
new file mode 100644
index 0000000..bcd2483
--- /dev/null
+++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.html
@@ -0,0 +1,449 @@
+<!--
+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.
+-->
+
+<div id="{{paragraph.id}}_container"
+     ng-class="{'paragraph outlineOnFocus': !asIframe, 'paragraphAsIframe': 
asIframe}">
+
+  <div ng-show="paragraph.config.title"
+       id="{{paragraph.id}}_title"
+       class="title">
+    <input type="text"
+           placeholder="Edit title"
+           ng-model="paragraph.title"
+           ng-show="showTitleEditor"
+           ng-delete="showTitleEditor = false"
+           ng-enter="setTitle(); showTitleEditor = false"/>
+    <div ng-click="showTitleEditor = !asIframe && !viewOnly"
+         ng-show="!showTitleEditor"
+         ng-bind-html="paragraph.title || 'Untitled'">
+    </div>
+  </div>
+
+  <div>
+    <div ng-show="!paragraph.config.editorHide && !viewOnly">
+      <div id="{{paragraph.id}}_editor"
+           style="opacity: 1;"
+           class="editor"
+           ui-ace="{
+                     onLoad : aceLoaded,
+                     onChange: aceChanged,
+                     require : ['ace/ext/language_tools']
+                   }"
+           ng-model="paragraph.text"
+           ng-class="{'disable': paragraph.status == 'RUNNING' || 
paragraph.status == 'PENDING' }">
+      </div>
+    </div>
+
+    <div id="{{paragraph.id}}_runControl" class="runControl">
+      <div ng-if="(getProgress()<=0 || getProgress()>=100) && 
(paragraph.status=='RUNNING' )">
+        <div id="{{paragraph.id}}_progress"
+             class="progress">
+            <div class="progress-bar progress-bar-striped active" 
role="progressbar" style="width:100%;"></div>
+          <span class="sr-only"></span>
+        </div>
+      </div>
+      <div ng-if="getProgress()>0 && getProgress()<100 && 
paragraph.status=='RUNNING'">
+        <div id="{{paragraph.id}}_progress"
+             class="progress">
+          <div class="progress-bar" role="progressbar" 
style="width:{{getProgress()}}%;"></div>
+          <span class="sr-only">{{getProgress()}}%</span>
+        </div>
+      </div>
+    </div>
+
+    <form id="{{paragraph.id}}_form" role="form"
+          ng-show="!paragraph.config.tableHide && !asIframe"
+          class=" paragraphForm form-horizontal row">
+      <div class="form-group col-sm-6 col-md-6 col-lg-4"
+           ng-repeat="formulaire in paragraph.settings.forms"
+           ng-Init="loadForm(formulaire, paragraph.settings.params)">
+        <label class="control-label input-sm" ng-class="{'disable': 
paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' 
}">{{formulaire.name}}</label>
+        <div class="">
+          <input class="form-control input-sm"
+                 ng-if="!paragraph.settings.forms[formulaire.name].options"
+                 ng-enter="runParagraph(getEditorValue())"
+                 ng-model="paragraph.settings.params[formulaire.name]"
+                 ng-class="{'disable': paragraph.status == 'RUNNING' || 
paragraph.status == 'PENDING' }"
+                 name="{{formulaire.name}}">
+          </input>
+
+          <select class="form-control input-sm"
+                 ng-if="paragraph.settings.forms[formulaire.name].options"
+                 ng-change="runParagraph(getEditorValue())"
+                 ng-model="paragraph.settings.params[formulaire.name]"
+                 ng-class="{'disable': paragraph.status == 'RUNNING' || 
paragraph.status == 'PENDING' }"
+                 name="{{formulaire.name}}"
+                 ng-options="option.value as 
(option.displayName||option.value) for option in 
paragraph.settings.forms[formulaire.name].options"
+                 >
+<!--
+            <option
+                ng-repeat="option in 
paragraph.settings.forms[formulaire.name].options"
+                value="{{option.value}}"
+                >{{option.displayName || option.value}}
+            </option>
+-->
+          </select>
+        </div>
+      </div>
+    </form>
+
+    <!-- Rendering -->
+    <div class='tableDisplay' ng-show="!paragraph.config.tableHide">
+      <div id="{{paragraph.id}}_switch"
+           ng-if="paragraph.result.type == 'TABLE' && !asIframe && !viewOnly"
+           class="btn-group"
+           style='margin-bottom: 10px;'>
+        <button type="button" class="btn btn-default btn-sm"
+                ng-class="{'active': isGraphMode('table')}"
+                ng-click="setGraphMode('table', true)" ><i class="fa 
fa-table"></i>
+        </button>
+        <button type="button" class="btn btn-default btn-sm"
+                ng-class="{'active': isGraphMode('multiBarChart')}"
+                ng-click="setGraphMode('multiBarChart', true)"><i class="fa 
fa-bar-chart"></i>
+        </button>
+        <button type="button" class="btn btn-default btn-sm"
+                ng-class="{'active': isGraphMode('pieChart')}"
+                ng-click="setGraphMode('pieChart', true)"><i class="fa 
fa-pie-chart"></i>
+        </button>
+        <button type="button" class="btn btn-default btn-sm"
+                ng-class="{'active': isGraphMode('stackedAreaChart')}"
+                ng-click="setGraphMode('stackedAreaChart', true)"><i class="fa 
fa-area-chart"></i>
+        </button>
+        <button type="button" class="btn btn-default btn-sm"
+                ng-class="{'active': isGraphMode('lineChart')}"
+                ng-click="setGraphMode('lineChart', true)"><i class="fa 
fa-line-chart"></i>
+        </button>
+        <button type="button" class="btn btn-default btn-sm"
+                ng-class="{'active': isGraphMode('scatterChart')}"
+                ng-click="setGraphMode('scatterChart', true)"><i class="cf 
cf-scatter-chart"></i>
+        </button>
+      </div>
+      <span ng-if="getResultType()=='TABLE' && getGraphMode()!='table' && 
!asIframe && !viewOnly"
+            style="margin-left:10px; cursor:pointer; display: inline-block; 
vertical-align:top; position: relative; line-height:30px;">
+        <a class="btnText" ng-if="paragraph.config.graph.optionOpen"
+           ng-click="toggleGraphOption()">
+          settings <span class="fa fa-caret-up"></span>
+        </a>
+        <a class="btnText" ng-if="!paragraph.config.graph.optionOpen"
+           ng-click="toggleGraphOption()" >
+          settings <span class="fa fa-caret-down"></span>
+        </a>
+      </span>
+
+      <div class="option lightBold" style="overflow: visible;"
+         ng-if="getResultType()=='TABLE' && getGraphMode()!='table'
+         && paragraph.config.graph.optionOpen && !asIframe && !viewOnly">
+        All fields:
+        <div class="allFields row">
+          <ul class="noDot">
+            <li class="liVertical" ng-repeat="col in 
paragraph.result.columnNames">
+              <div class="btn btn-default btn-xs"
+                   data-drag="true"
+                   data-jqyoui-options="{revert: 'invalid', helper: 'clone'}"
+                   ng-model="paragraph.result.columnNames"
+                   jqyoui-draggable="{index: {{$index}}, placeholder: 'keep'}">
+                 {{col.name | limitTo: 30}}{{col.name.length > 30 ? '...' : 
''}}
+              </div>
+            </li>
+          </ul>
+        </div>
+
+        <div class="row" ng-if="getGraphMode()!='scatterChart'">
+          <div class="col-md-4">
+            <span class="columns lightBold">
+              Keys
+              <ul data-drop="true"
+                  ng-model="paragraph.config.graph.keys"
+                  jqyoui-droppable="{multiple:true, 
onDrop:'onGraphOptionChange()'}"
+                  class="list-unstyled">
+                <li ng-repeat="item in paragraph.config.graph.keys">
+                  <button class="btn btn-primary btn-xs">
+                    {{item.name}} <span class="fa fa-close" 
ng-click="removeGraphOptionKeys($index)"></span>
+                  </button>
+                </li>
+              </ul>
+            </span>
+          </div>
+          <div class="col-md-4">
+            <span class="columns lightBold">
+              Groups
+              <ul data-drop="true"
+                  ng-model="paragraph.config.graph.groups"
+                  jqyoui-droppable="{multiple:true, 
onDrop:'onGraphOptionChange()'}"
+                  class="list-unstyled">
+                <li ng-repeat="item in paragraph.config.graph.groups">
+                  <button class="btn btn-success btn-xs">
+                    {{item.name}} <span class="fa fa-close" 
ng-click="removeGraphOptionGroups($index)"></span>
+                  </button>
+                </li>
+              </ul>
+            </span>
+          </div>
+          <div class="col-md-4">
+            <span class="columns lightBold">
+              Values
+              <ul data-drop="true"
+                  ng-model="paragraph.config.graph.values"
+                  jqyoui-droppable="{multiple:true, 
onDrop:'onGraphOptionChange()'}"
+                  class="list-unstyled">
+                <li ng-repeat="item in paragraph.config.graph.values">
+                  <div class="btn-group">
+                    <div class="btn btn-info btn-xs dropdown-toggle"
+                         type="button"
+                         data-toggle="dropdown">
+                      {{item.name | limitTo: 30}}{{item.name.length > 30 ? 
'...' : ''}}
+                      <font style="color:#EEEEEE;"><span class="lightBold" 
style="text-transform: uppercase;">{{item.aggr}}</span></font>
+                      <span class="fa fa-close" 
ng-click="removeGraphOptionValues($index)"></span>
+                    </div>
+                    <ul class="dropdown-menu" role="menu">
+                      <li ng-click="setGraphOptionValueAggr($index, 
'sum')"><a>sum</a></li>
+                      <li ng-click="setGraphOptionValueAggr($index, 
'count')"><a>count</a></li>
+                      <li ng-click="setGraphOptionValueAggr($index, 
'avg')"><a>avg</a></li>
+                      <li ng-click="setGraphOptionValueAggr($index, 
'min')"><a>min</a></li>
+                      <li ng-click="setGraphOptionValueAggr($index, 
'max')"><a>max</a></li>
+                    </ul>
+                  </div>
+                </li>
+              </ul>
+            </span>
+          </div>
+        </div>
+
+        <div class="row" ng-if="getGraphMode()=='scatterChart'">
+          <div class="col-md-3">
+            <span class="columns lightBold">
+              xAxis
+              <ul data-drop="true"
+                  ng-model="paragraph.config.graph.scatter.xAxis"
+                  jqyoui-droppable="{onDrop:'onGraphOptionChange()'}"
+                  class="list-unstyled"
+                  style="height:36px">
+                <li ng-if="paragraph.config.graph.scatter.xAxis">
+                  <button class="btn btn-primary btn-xs">
+                    {{paragraph.config.graph.scatter.xAxis.name}} <span 
class="fa fa-close" ng-click="removeScatterOptionXaxis($index)"></span>
+                  </button>
+                </li>
+              </ul>
+            </span>
+          </div>
+          <div class="col-md-3">
+            <span class="columns lightBold">
+              yAxis
+              <ul data-drop="true"
+                  ng-model="paragraph.config.graph.scatter.yAxis"
+                  jqyoui-droppable="{onDrop:'onGraphOptionChange()'}"
+                  class="list-unstyled"
+                  style="height:36px">
+                <li ng-if="paragraph.config.graph.scatter.yAxis">
+                  <button class="btn btn-success btn-xs">
+                    {{paragraph.config.graph.scatter.yAxis.name}} <span 
class="fa fa-close" ng-click="removeScatterOptionYaxis($index)"></span>
+                  </button>
+                </li>
+              </ul>
+            </span>
+          </div>
+          <div class="col-md-3">
+            <span class="columns lightBold">
+              group
+              <ul data-drop="true"
+                  ng-model="paragraph.config.graph.scatter.group"
+                  jqyoui-droppable="{onDrop:'onGraphOptionChange()'}"
+                  class="list-unstyled"
+                  style="height:36px">
+                <li ng-if="paragraph.config.graph.scatter.group">
+                  <button class="btn btn-info btn-xs">
+                    {{paragraph.config.graph.scatter.group.name}} <span 
class="fa fa-close" ng-click="removeScatterOptionGroup($index)"></span>
+                  </button>
+                </li>
+              </ul>
+            </span>
+          </div>
+          <div class="col-md-3">
+            <span class="columns lightBold">
+              size
+              <a tabindex="0" class="fa fa-info-circle" role="button" 
popover-placement="top"
+                 popover-trigger="focus"
+                 popover-html-unsafe="<li>Size option is valid only when you 
drop numeric field here.</li>
+                                      <li>When data in each axis are discrete, 
'number of values in corresponding coordinate' will be used as size.</li>
+                                      <li>Zeppelin consider values as discrete 
when the values contain string value or the number of distinct values are 
bigger than 5% of total number of values.</li>
+                                      <li>Size field button turns to grey when 
the option you chose is not valid.</li>"></a>
+              <ul data-drop="true"
+                  ng-model="paragraph.config.graph.scatter.size"
+                  jqyoui-droppable="{onDrop:'onGraphOptionChange()'}"
+                  class="list-unstyled"
+                  style="height:36px">
+                <li ng-if="paragraph.config.graph.scatter.size">
+                  <button class="btn btn-xs" style="color:white" 
ng-class="{'btn-warning': isValidSizeOption(paragraph.config.graph.scatter, 
paragraph.result.rows)}">
+                    {{paragraph.config.graph.scatter.size.name}} <span 
class="fa fa-close" ng-click="removeScatterOptionSize($index)"></span>
+                  </button>
+                </li>
+              </ul>
+            </span>
+          </div>
+        </div>
+
+      </div>
+
+
+      <div id="p{{paragraph.id}}_graph"
+           class="graphContainer"
+           ng-class="{'noOverflow': getGraphMode()=='table'}"
+           ng-if="getResultType()=='TABLE'"
+           allowresize="{{!asIframe && !viewOnly}}"
+           resizable on-resize="setGraphHeight();">
+
+        <div ng-if="getGraphMode()=='table'"
+             id="p{{paragraph.id}}_table"
+             class="table">
+        </div>
+
+        <div ng-if="getGraphMode()=='multiBarChart'"
+             id="p{{paragraph.id}}_multiBarChart">
+          <svg></svg>
+        </div>
+
+        <div ng-if="getGraphMode()=='pieChart'"
+             id="p{{paragraph.id}}_pieChart">
+          <svg></svg>
+        </div>
+
+        <div ng-if="getGraphMode()=='stackedAreaChart'"
+             id="p{{paragraph.id}}_stackedAreaChart">
+          <svg></svg>
+        </div>
+
+        <div ng-if="getGraphMode()=='lineChart'"
+             id="p{{paragraph.id}}_lineChart">
+          <svg></svg>
+        </div>
+
+        <div ng-if="getGraphMode()=='scatterChart'"
+             id="p{{paragraph.id}}_scatterChart">
+          <svg></svg>
+        </div>
+      </div>
+
+      <div id="{{paragraph.id}}_comment"
+           class="text"
+           ng-if="getResultType()=='TABLE' && paragraph.result.comment"
+           ng-Init="loadResultType(paragraph.result)"
+           ng-bind-html="paragraph.result.comment">
+      </div>
+
+      <div id="{{paragraph.id}}_text"
+           class="text"
+           ng-if="paragraph.result.type == 'TEXT'"
+           ng-Init="loadResultType(paragraph.result)"
+           ng-bind="paragraph.result.msg">
+      </div>
+
+      <div id="p{{paragraph.id}}_html"
+           ng-if="paragraph.result.type == 'HTML'"
+           ng-Init="loadResultType(paragraph.result)">
+      </div>
+
+      <div id="p{{paragraph.id}}_angular"
+           ng-if="paragraph.result.type == 'ANGULAR'"
+           ng-Init="loadResultType(paragraph.result)">
+      </div>
+
+      <img id="{{paragraph.id}}_img"
+           ng-if="paragraph.result.type == 'IMG'"
+           ng-Init="loadResultType(paragraph.result)"
+           ng-src="{{getBase64ImageSrc(paragraph.result.msg)}}">
+      </img>
+
+      <div id="{{paragraph.id}}_error"
+           class="error"
+           ng-if="paragraph.status == 'ERROR'"
+           ng-bind="paragraph.errorMessage">
+      </div>
+
+      <div id="{{paragraph.id}}_executionTime" class="executionTime" 
ng-bind-html="getExecutionTime()">
+      </div>
+    </div>
+  </div>
+  <div id="{{paragraph.id}}_control" class="control" ng-show="!asIframe">
+
+    <span>
+      {{paragraph.status}}
+    </span>
+
+    <span ng-if="paragraph.status=='RUNNING'">
+      {{getProgress()}}%
+    </span>
+
+    <!-- Run / Cancel button -->
+    <span class="icon-control-play" style="cursor:pointer;color:#3071A9" 
tooltip-placement="top" tooltip="Run this paragraph"
+          ng-click="runParagraph(getEditorValue())"
+          ng-show="paragraph.status!='RUNNING' && 
paragraph.status!='PENDING'"></span>
+    <span class="icon-control-pause" style="cursor:pointer;color:#CD5C5C" 
tooltip-placement="top" tooltip="Cancel"
+          ng-click="cancelParagraph()"
+          ng-show="paragraph.status=='RUNNING' || 
paragraph.status=='PENDING'"></span>
+
+    <span class="{{paragraph.config.editorHide ? 'icon-size-fullscreen' : 
'icon-size-actual'}}" style="cursor:pointer;" tooltip-placement="top" 
tooltip="{{(paragraph.config.editorHide ? 'Show' : 'Hide') + ' editor'}}"
+          ng-click="toggleEditor()"></span>
+    <span class="{{paragraph.config.tableHide ? 'icon-notebook' : 
'icon-book-open'}}" style="cursor:pointer;" tooltip-placement="top" 
tooltip="{{(paragraph.config.tableHide ? 'Show' : 'Hide') + ' output'}}"
+          ng-click="toggleOutput()"></span>
+
+    <span class="dropdown navbar-right">
+      <span class="icon-settings" style="cursor:pointer"
+            data-toggle="dropdown"
+            type="button">
+      </span>
+      <ul class="dropdown-menu" role="menu" style="width:200px;">
+        <li>
+          <a class="fa fa-arrows-h dropdown"> Width
+          <form style="display:inline; margin-left:5px;">
+            <select ng-model="paragraph.config.colWidth"
+                    class="selectpicker"
+                    ng-change="changeColWidth()"
+                    ng-options="col for col in colWidthOption"></select>
+          </form>
+          </a>
+        </li>
+        <li>
+          <a class="icon-arrow-up" style="cursor:pointer"
+             ng-click="moveUp()"> Move Up</a>
+        </li>
+        <li>
+          <a class="icon-arrow-down" style="cursor:pointer"
+             ng-click="moveDown()"> Move Down</a>
+        </li>
+        <li>
+          <a class="icon-plus" style="cursor:pointer"
+             ng-click="insertNew()"> Insert New</a>
+        </li>
+        <li>
+          <!-- paragraph handler -->
+          <a class="fa fa-font" style="cursor:pointer"
+             ng-click="hideTitle()"
+             ng-show="paragraph.config.title"> Hide title</a>
+          <a class="fa fa-font" style="cursor:pointer"
+             ng-click="showTitle()"
+             ng-show="!paragraph.config.title"> Show title</a>
+        </li>
+
+        <li><a class="icon-share-alt" style="cursor:pointer"
+               ng-click="goToSingleParagraph()"> Link this paragraph</a>
+        </li>
+        <li>
+          <!-- remove paragraph -->
+          <a class="fa fa-times" style="cursor:pointer"
+             ng-click="removeParagraph()"> Remove</a>
+        </li>
+      </ul>
+    </span>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/images/zepLogo.png
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/assets/images/zepLogo.png 
b/zeppelin-web/src/assets/images/zepLogo.png
new file mode 100644
index 0000000..f11f022
Binary files /dev/null and b/zeppelin-web/src/assets/images/zepLogo.png differ

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/images/zepLogoW.png
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/assets/images/zepLogoW.png 
b/zeppelin-web/src/assets/images/zepLogoW.png
new file mode 100644
index 0000000..0b45fa4
Binary files /dev/null and b/zeppelin-web/src/assets/images/zepLogoW.png differ

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/styles/looknfeel/default.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/assets/styles/looknfeel/default.css 
b/zeppelin-web/src/assets/styles/looknfeel/default.css
new file mode 100644
index 0000000..8aeee95
--- /dev/null
+++ b/zeppelin-web/src/assets/styles/looknfeel/default.css
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+body {
+  background: #ecf0f1;
+}
+
+/**
+ * Box and well
+ */
+.box{
+  border-style: solid;
+  min-height: 20px;
+  padding: 19px;
+  margin-bottom: 20px;
+}
+
+.box, 
+.well {
+  background-color: #ffffff;
+  border-color: #e5e5e5;
+  border-width: 1px 1px 2px;
+  border-radius: 3px;
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+
+.paragraph {
+  min-height: 32px;
+}
+
+.noteAction {
+  background-color: white;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+  color: #2c3e50;
+  border-bottom: 1px solid #E5E5E5;
+}
+
+.control span {
+    margin-left: 4px;
+}
+
+.control {
+    padding: 4px;
+}
+
+.paragraph-space {
+  margin-bottom: 5px;
+  padding: 10px !important;
+}
+
+.editor,
+.executionTime,
+.nv-controlsWrap {
+  display:block;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/styles/looknfeel/report.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/assets/styles/looknfeel/report.css 
b/zeppelin-web/src/assets/styles/looknfeel/report.css
new file mode 100644
index 0000000..065b92c
--- /dev/null
+++ b/zeppelin-web/src/assets/styles/looknfeel/report.css
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+body {
+  background: white;
+}
+
+
+/**
+ * Box and well
+ */
+.box{
+  border: 0px;
+  min-height: 0px;
+}
+
+.box:hover, 
+.well:hover {
+  background-color: white;
+  border: 0px;
+}
+
+.paragraph-col{
+  border: 0px solid white;
+}
+
+.paragraph {
+  min-height: 32px;
+}
+
+.paragraph-space {
+  margin-bottom: 5px;
+  padding: 10px !important;
+}
+
+.paragraph .control {
+  visibility : hidden;
+  right:15px;
+  top: 6px;
+}
+
+.paragraph:hover .control {
+  visibility : hidden;
+}
+
+.noteAction span, .noteAction button, .noteAction form {
+  visibility : hidden;
+}
+
+.noteAction:hover span, .noteAction:hover button, .noteAction:hover form {
+  visibility : visible;
+}
+
+.executionTime,
+.nv-controlsWrap {
+  display:none;
+}

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/styles/looknfeel/simple.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/assets/styles/looknfeel/simple.css 
b/zeppelin-web/src/assets/styles/looknfeel/simple.css
new file mode 100644
index 0000000..9edb95e
--- /dev/null
+++ b/zeppelin-web/src/assets/styles/looknfeel/simple.css
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+body {
+  background: white;
+}
+
+
+/**
+ * Box and well
+ */
+.box{
+  border: 1px solid white;
+  min-height: 20px;
+}
+
+.box:hover,
+.well:hover {
+  background-color: #ffffff;
+  border: 1px solid #DDDDDD;
+}
+
+.paragraph-col .focused{
+  box-shadow: 1px 2px 7px rgba(0, 0, 0, 0.3);
+}
+
+.paragraph-col{
+  border: 2px solid white;
+}
+
+.paragraph {
+  min-height: 32px;
+}
+
+.paragraph-space {
+  padding: 0px 2px 0px 2px !important;
+}
+
+
+.paragraph .control {
+  visibility : hidden;
+  right:15px;
+  top: 6px;
+}
+
+.paragraph:hover .control {
+  visibility : visible;
+}
+
+.noteAction span, .noteAction button, .noteAction form {
+  visibility : hidden;
+}
+
+.noteAction:hover span, .noteAction:hover button, .noteAction:hover form {
+  visibility : visible;
+}
+
+.editor,
+.executionTime,
+.nv-controlsWrap {
+  display:block;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/styles/printMode.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/assets/styles/printMode.css 
b/zeppelin-web/src/assets/styles/printMode.css
new file mode 100644
index 0000000..953b3a6
--- /dev/null
+++ b/zeppelin-web/src/assets/styles/printMode.css
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+
+body {
+  background: white;
+}
+
+.labelBtn {
+  display: none;
+}
+
+.new_h3 {
+  margin-left: 220px;
+  top: 0px;
+  position: fixed;
+}
+
+.noteAction {
+  border: 1px solid #3071a9;
+  box-shadow: none;
+}
+
+.control {
+  display: none;
+}
+
+.editor {
+  display: none;
+}
+
+.form-horizontal {
+  display: none;
+}
+
+.btn-group {
+  display: none;
+}
+
+.box {
+  border: none;
+}
+
+svg {
+  margin-left: -20px;
+}
+
+.btn-link {
+  display: none;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/components/baseUrl/baseUrl.service.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/baseUrl/baseUrl.service.js 
b/zeppelin-web/src/components/baseUrl/baseUrl.service.js
new file mode 100644
index 0000000..662d88f
--- /dev/null
+++ b/zeppelin-web/src/components/baseUrl/baseUrl.service.js
@@ -0,0 +1,66 @@
+/*
+ * 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').service('baseUrlSrv', function() {
+
+  /** Get the current port of the websocket
+  *
+  * When running Zeppelin, the body of this function will be dynamically
+  * overridden with the AppScriptServlet from zeppelin-site.xml config value.
+  *
+  * If the config value is not defined, it defaults to the HTTP port + 1
+  *
+  * In the case of running "grunt serve", this function will appear
+  * as is.
+  */
+  
+  /* @preserve AppScriptServlet - getPort */
+  this.getPort = function() {
+    var port = Number(location.port);
+    if (location.protocol !== 'https:' && !port) {
+      port = 80;
+    } else if (location.protocol === 'https:' && !port) {
+      port = 443;
+    } else if (port === 3333 || port === 9000) {
+      port = 8080;
+    }
+    return port + 1;
+  };
+  /* @preserve AppScriptServlet - close */
+
+  this.getWebsocketProtocol = function() {
+    return location.protocol === 'https:' ? 'wss' : 'ws';
+  };
+
+  this.getRestApiBase = function() {
+    var port = Number(location.port);
+    if (!port) {
+      port = 80;
+      if (location.protocol === 'https:') {
+        port = 443;
+      }
+    }
+
+    if (port === 3333 || port === 9000) {
+      port = 8080;
+    }
+    return location.protocol + '//' + location.hostname + ':' + port + 
skipTrailingSlash(location.pathname) + '/api';
+  };
+  
+  var skipTrailingSlash = function(path) {
+    return path.replace(/\/$/, '');
+  };
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/components/dropdowninput/dropdowninput.directive.js
----------------------------------------------------------------------
diff --git 
a/zeppelin-web/src/components/dropdowninput/dropdowninput.directive.js 
b/zeppelin-web/src/components/dropdowninput/dropdowninput.directive.js
new file mode 100644
index 0000000..65dd5d3
--- /dev/null
+++ b/zeppelin-web/src/components/dropdowninput/dropdowninput.directive.js
@@ -0,0 +1,25 @@
+/*
+ * 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').directive('dropdownInput', function () {
+    return {
+        restrict: 'A',
+        link: function (scope, element) {
+            element.bind('click', function (event) {
+                event.stopPropagation();
+            });
+        }
+    };
+});

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/components/modal-shortcut/modal-shortcut.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/modal-shortcut/modal-shortcut.html 
b/zeppelin-web/src/components/modal-shortcut/modal-shortcut.html
new file mode 100644
index 0000000..1fdf9ea
--- /dev/null
+++ b/zeppelin-web/src/components/modal-shortcut/modal-shortcut.html
@@ -0,0 +1,130 @@
+<!--
+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.
+-->
+
+<div class="modal fade" id="shortcutModal" tabindex="-1" role="dialog" 
aria-labelledby="myModalLabel" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal"><span 
aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+        <h4 class="modal-title" id="myModalLabel">Keyboard shortcuts</h4>
+      </div>
+      <div class="modal-body">
+
+        <h4>Control in Note</h4>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Shift</kbd> + <kbd 
class="kbd-dark">Enter</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Run the note
+          </div>
+        </div>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">p</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Move cursor Up
+          </div>
+        </div>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">n</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Move cursor Down
+          </div>
+        </div>
+
+
+        <h4>Control in Note Editor</h4>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">k</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Cut the line
+          </div>
+        </div>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">y</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Paste the line
+          </div>
+        </div>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">s</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Search inside the code
+          </div>
+        </div>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">a</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Move cursor to the beginning
+          </div>
+        </div>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">e</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Move cursor at the end
+          </div>
+        </div>
+
+        <div class="row">
+          <div class="col-md-4">
+            <div class="keys">
+              <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">N</kbd>
+            </div>
+          </div>
+          <div class="col-md-8">
+            Move cursor Down
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

Reply via email to