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>