Repository: zeppelin
Updated Branches:
  refs/heads/master e5ac1134c -> 13d77e07f


[ZEPPELIN-1371]add text/numeric conversion support to table display

### What is this PR for?

We people in Twitter have great demand of adding a flexible drop down menu for 
the columns in the tables which enables the text/numeric conversion.
This is because people want some columns to be of string type which, for 
example, fits to the underlying DB definition.
The use cases of the change include:
1. For now sorting on the columns is always lexicographically because Zeppelin 
front end treats the data as strings. It the values in the table can be 
convertible between string and number then we can sort the column by numeric 
values. I have filed another ticket for this: 
https://issues.apache.org/jira/browse/ZEPPELIN-1372
2. Since the users know more about their data than us it would be nice if we 
let the users decide what is the real type of their data. It is annoying if 
user wants the column to be strings but the front end forcefully inserts commas 
in it. In some scenarios, users may also want to copy/paste the table to 
somewhere else. If people want to remove the commas before other actions then 
that will be a nightmare....
### What type of PR is it?

[Improvement | Feature]
### Todos
- [ ] - Task
### What is the Jira issue?
- https://issues.apache.org/jira/browse/ZEPPELIN-1371
### How should this be tested?
1. Click on the dropdown menu would convert the text/number of that column.
2. Other functionalities esp. the sorting function should not be affected.
### Screenshots (if appropriate)

![image](https://cloud.githubusercontent.com/assets/3334391/18445617/76071f8e-78ec-11e6-93a6-fdcf7b85b6e9.png)
![image](https://cloud.githubusercontent.com/assets/3334391/18446601/16cff4f0-78f1-11e6-9fd6-4010b5d77f17.png)
### Questions:
- Does the licenses files need update? No
- Is there breaking changes for older versions? Probably a noticeable change in 
UI
- Does this needs documentation? No

Author: Peilin Yang <yangpei...@gmail.com>

Closes #1500 from Peilin-Yang/ypeilin/table_num_cell_format and squashes the 
following commits:

e3425ee [Peilin Yang] make clean code
ba60cb0 [Peilin Yang] update test case
cdc06b6 [Peilin Yang] refactor based on the most recent master
06b4ae7 [Peilin Yang] merge
4e2a403 [Peilin Yang] bug fix
61824d3 [Peilin Yang] update test case
df2effb [Peilin Yang] update the test case
39c5408 [Peilin Yang] update the test case in SparkParagraphIT
030a1e6 [Peilin Yang] add dropdown menu applied on the latest branch


Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/13d77e07
Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/13d77e07
Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/13d77e07

Branch: refs/heads/master
Commit: 13d77e07f834ff375941841e5b2e8cc344702749
Parents: e5ac113
Author: Peilin Yang <yangpei...@gmail.com>
Authored: Wed Nov 30 08:45:03 2016 -0500
Committer: Alexander Bezzubov <b...@apache.org>
Committed: Fri Dec 2 10:56:57 2016 +0900

----------------------------------------------------------------------
 .../zeppelin/integration/SparkParagraphIT.java  |   3 +-
 .../src/app/handsontable/handsonHelper.js       | 199 +++++++++++++++++++
 .../src/app/notebook/paragraph/paragraph.css    |  48 +++++
 zeppelin-web/src/app/tabledata/tabledata.js     |  20 +-
 .../builtins/visualization-table.js             |  44 +---
 zeppelin-web/src/index.html                     |   1 +
 6 files changed, 262 insertions(+), 53 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/13d77e07/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java
index 4bb3158..ceebd4d 100644
--- 
a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java
@@ -185,8 +185,7 @@ public class SparkParagraphIT extends AbstractZeppelinIT {
       WebElement paragraph1Result = driver.findElement(By.xpath(
           getParagraphXPath(1) + 
"//div[contains(@id,\"_graph\")]/div/div/div/div/div[1]"));
       collector.checkThat("Paragraph from SparkParagraphIT of testSqlSpark 
result: ",
-          paragraph1Result.getText().toString(), 
CoreMatchers.equalTo("age\njob\nmarital\neducation\nbalance\n" +
-          "30 unemployed married primary 1,787"));
+          paragraph1Result.getText().toString(), 
CoreMatchers.equalTo("age\n▼\njob\n▼\nmarital\n▼\neducation\n▼\nbalance\n▼\n30
 unemployed married primary 1787"));
     } catch (Exception e) {
       handleException("Exception in SparkParagraphIT while testSqlSpark", e);
     }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/13d77e07/zeppelin-web/src/app/handsontable/handsonHelper.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/handsontable/handsonHelper.js 
b/zeppelin-web/src/app/handsontable/handsonHelper.js
new file mode 100644
index 0000000..c4127d1
--- /dev/null
+++ b/zeppelin-web/src/app/handsontable/handsonHelper.js
@@ -0,0 +1,199 @@
+/*
+ * 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';
+
+var zeppelin = zeppelin || {};
+
+/**
+ * HandsonHelper class
+ */
+zeppelin.HandsonHelper = function(columns, rows, comment) {
+  this.columns = columns || [];
+  this.rows = rows || [];
+  this.comment = comment || '';
+};
+
+zeppelin.HandsonHelper.prototype.getHandsonTableConfig = function(columns, 
columnNames, resultRows) {
+  return {
+    colHeaders: columnNames,
+    data: resultRows,
+    rowHeaders: false,
+    stretchH: 'all',
+    sortIndicator: true,
+    columns: columns,
+    columnSorting: true,
+    contextMenu: false,
+    manualColumnResize: true,
+    manualRowResize: true,
+    readOnly: true,
+    readOnlyCellClassName: '',
+    fillHandle: false,
+    fragmentSelection: true,
+    disableVisualSelection: true,
+    cells: function(ro, co, pro) {
+      var cellProperties = {};
+      var colType = columns[co].type;
+      cellProperties.renderer = function(instance, td, row, col, prop, value, 
cellProperties) {
+        _cellRenderer(instance, td, row, col, prop, value, cellProperties, 
colType);
+      };
+      return cellProperties;
+    },
+    afterGetColHeader: function(col, TH) {
+      var instance = this;
+      var menu = _buildDropDownMenu(columns[col].type);
+      var button = _buildTypeSwitchButton();
+
+      _addButtonMenuEvent(button, menu);
+
+      Handsontable.Dom.addEvent(menu, 'click', function(event) {
+        if (event.target.nodeName === 'LI') {
+          _setColumnType(columns, event.target.data.colType, instance, col);
+        }
+      });
+      if (TH.firstChild.lastChild.nodeName === 'BUTTON') {
+        TH.firstChild.removeChild(TH.firstChild.lastChild);
+      }
+      TH.firstChild.appendChild(button);
+      TH.style['white-space'] = 'normal';
+    }
+  };
+};
+
+/*
+** Private Service Functions
+*/
+
+function _addButtonMenuEvent(button, menu) {
+  Handsontable.Dom.addEvent(button, 'click', function(event) {
+    var changeTypeMenu;
+    var position;
+    var removeMenu;
+
+    document.body.appendChild(menu);
+
+    event.preventDefault();
+    event.stopImmediatePropagation();
+
+    changeTypeMenu = document.querySelectorAll('.changeTypeMenu');
+
+    for (var i = 0, len = changeTypeMenu.length; i < len; i++) {
+      changeTypeMenu[i].style.display = 'none';
+    }
+    menu.style.display = 'block';
+    position = button.getBoundingClientRect();
+
+    menu.style.top = (position.top + (window.scrollY || window.pageYOffset)) + 
2 + 'px';
+    menu.style.left = (position.left) + 'px';
+
+    removeMenu = function(event) {
+      if (menu.parentNode) {
+        menu.parentNode.removeChild(menu);
+      }
+    };
+    Handsontable.Dom.removeEvent(document, 'click', removeMenu);
+    Handsontable.Dom.addEvent(document, 'click', removeMenu);
+  });
+}
+
+function _buildDropDownMenu(activeCellType) {
+  var menu = document.createElement('UL');
+  var types = ['text', 'numeric', 'date'];
+  var item;
+
+  menu.className = 'changeTypeMenu';
+
+  for (var i = 0, len = types.length; i < len; i++) {
+    item = document.createElement('LI');
+    if ('innerText' in item) {
+      item.innerText = types[i];
+    } else {
+      item.textContent = types[i];
+    }
+
+    item.data = {'colType': types[i]};
+
+    if (activeCellType === types[i]) {
+      item.className = 'active';
+    }
+    menu.appendChild(item);
+  }
+
+  return menu;
+}
+
+function _buildTypeSwitchButton() {
+  var button = document.createElement('BUTTON');
+
+  button.innerHTML = '\u25BC';
+  button.className = 'changeType';
+
+  return button;
+}
+
+function _isNumeric(value) {
+  if (!isNaN(value)) {
+    if (value.length !== 0) {
+      if (Number(value) <= Number.MAX_SAFE_INTEGER && Number(value) >= 
Number.MIN_SAFE_INTEGER) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+function _cellRenderer(instance, td, row, col, prop, value, cellProperties, 
colType) {
+  if (colType === 'numeric' && _isNumeric(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);
+  }
+}
+
+function _dateValidator(value, callback) {
+  var d = moment(value);
+  return callback(d.isValid());
+}
+
+function _numericValidator(value, callback) {
+  return callback(_isNumeric(value));
+}
+
+function _setColumnType(columns, type, instance, col) {
+  columns[col].type = type;
+  _setColumnValidator(columns, col);
+  instance.updateSettings({columns: columns});
+  instance.validateCells(null);
+  if (_isColumnSorted(instance, col)) {
+    instance.sort(col, instance.sortOrder);
+  }
+}
+
+function _isColumnSorted(instance, col) {
+  return instance.sortingEnabled && instance.sortColumn === col;
+}
+
+function _setColumnValidator(columns, col) {
+  if (columns[col].type === 'numeric') {
+    columns[col].validator = _numericValidator;
+  } else if (columns[col].type === 'date') {
+    columns[col].validator = _dateValidator;
+  } else {
+    columns[col].validator = null;
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/13d77e07/zeppelin-web/src/app/notebook/paragraph/paragraph.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css 
b/zeppelin-web/src/app/notebook/paragraph/paragraph.css
index 60b93d0..430aad9 100644
--- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css
+++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css
@@ -498,3 +498,51 @@ table.table-striped {
   padding-left: 4px !important;
   width: 20px;
 }
+
+
+.changeType {
+  border: 1px solid #bbb;
+  color: #bbb;
+  background: #eee;
+  border-radius: 2px;
+  padding: 2px;
+  font-size: 9px;
+  float: right;
+  line-height: 9px;
+  margin: 3px 3px 0 0;
+}
+.changeType:hover {
+  border: 1px solid #777;
+  color: #777;
+  cursor: pointer;
+}
+.changeType.pressed {
+  background-color: #999;
+}
+.changeTypeMenu {
+  position: absolute;
+  border: 1px solid #ccc;
+  margin-top: 22px;
+  box-shadow: 0 1px 3px -1px #323232;
+  background: white;
+  padding: 0;
+  font-size: 13px;
+  display: none;
+  z-index: 10;
+}
+.changeTypeMenu li {
+  text-align: left;
+  list-style: none;
+  padding: 2px 20px;
+  cursor: pointer;
+  margin-bottom: 0;
+}
+.changeTypeMenu li.active:before {
+  font-size: 12px;
+  content: "\2714";
+  margin-left: -15px;
+  margin-right: 3px;
+}
+.changeTypeMenu li:hover {
+  background: #eee;
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/13d77e07/zeppelin-web/src/app/tabledata/tabledata.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/tabledata/tabledata.js 
b/zeppelin-web/src/app/tabledata/tabledata.js
index 0efcc54..ff80fd9 100644
--- a/zeppelin-web/src/app/tabledata/tabledata.js
+++ b/zeppelin-web/src/app/tabledata/tabledata.js
@@ -59,9 +59,8 @@ zeppelin.TableData.prototype.loadParagraphResult = 
function(paragraphResult) {
       if (i === 0) {
         columnNames.push({name: col, index: j, aggr: 'sum'});
       } else {
-        var parsedCol = this.parseTableCell(col);
-        cols.push(parsedCol);
-        cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, 
value: parsedCol});
+        cols.push(col);
+        cols2.push({key: (columnNames[i]) ? columnNames[i].name : undefined, 
value: col});
       }
     }
     if (i !== 0) {
@@ -73,18 +72,3 @@ zeppelin.TableData.prototype.loadParagraphResult = 
function(paragraphResult) {
   this.columns = columnNames;
   this.rows = rows;
 };
-
-zeppelin.TableData.prototype.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;
-};

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/13d77e07/zeppelin-web/src/app/visualization/builtins/visualization-table.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table.js 
b/zeppelin-web/src/app/visualization/builtins/visualization-table.js
index b801649..7406cd8 100644
--- a/zeppelin-web/src/app/visualization/builtins/visualization-table.js
+++ b/zeppelin-web/src/app/visualization/builtins/visualization-table.js
@@ -39,39 +39,17 @@ zeppelin.TableVisualization.prototype.render = 
function(tableData) {
     this.hot.destroy();
   }
 
-  this.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);
-        }
-      };
-      return cellProperties;
-    }
-  });
+  if (!this.columns) {
+    this.columns = Array.apply(null, 
Array(tableData.columns.length)).map(function() {
+      return {type: 'text'};
+    });
+  }
+
+  var handsonHelper = new zeppelin.HandsonHelper();
+
+  this.hot = new Handsontable(container, handsonHelper.getHandsonTableConfig(
+    this.columns, columnNames, resultRows));
+  this.hot.validateCells(null);
 };
 
 zeppelin.TableVisualization.prototype.destroy = function() {

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/13d77e07/zeppelin-web/src/index.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html
index 5f34491..d6feabc 100644
--- a/zeppelin-web/src/index.html
+++ b/zeppelin-web/src/index.html
@@ -174,6 +174,7 @@ limitations under the License.
     <script src="app/tabledata/tabledata.js"></script>
     <script src="app/tabledata/transformation.js"></script>
     <script src="app/tabledata/pivot.js"></script>
+    <script src="app/handsontable/handsonHelper.js"></script>
     <script src="app/visualization/visualization.js"></script>
     <script src="app/visualization/builtins/visualization-table.js"></script>
     <script 
src="app/visualization/builtins/visualization-nvd3chart.js"></script>

Reply via email to