diff --git a/app/gui/view/graph-editor/src/builtin/visualization/java_script/table.js b/app/gui/view/graph-editor/src/builtin/visualization/java_script/table.js index 49d5fa889792..0abc083096e1 100644 --- a/app/gui/view/graph-editor/src/builtin/visualization/java_script/table.js +++ b/app/gui/view/graph-editor/src/builtin/visualization/java_script/table.js @@ -28,7 +28,19 @@ class TableVisualization extends Visualization { constructor(data) { super(data) - this.setPreprocessor('Standard.Visualization.Table.Visualization', 'prepare_visualization') + this.setRowLimitAndPage(1000, 0) + } + + setRowLimitAndPage(row_limit, page) { + if (this.row_limit !== row_limit || this.page !== page) { + this.row_limit = row_limit + this.page = page + this.setPreprocessor( + 'Standard.Visualization.Table.Visualization', + 'prepare_visualization', + this.row_limit.toString() + ) + } } onDataReceived(data) { @@ -79,21 +91,46 @@ class TableVisualization extends Visualization { return content } + function cellRenderer(params) { + if (params.value === null) { + return 'Nothing' + } else if (params.value === undefined) { + return '' + } else if (params.value === '') { + return 'Empty' + } + return params.value.toString() + } + if (!this.tabElem) { while (this.dom.firstChild) { this.dom.removeChild(this.dom.lastChild) } const style = - '.ag-theme-alpine { --ag-grid-size: 3px; --ag-list-item-height: 20px; display: inline; }' + '.ag-theme-alpine { --ag-grid-size: 3px; --ag-list-item-height: 20px; display: inline; }\n' + + '.vis-status-bar { height: 20x; background-color: white; font-size:14px; white-space:nowrap; padding: 0 5px; overflow:hidden; border-radius: 16px }\n' + + '.vis-status-bar > button { width: 12px; margin: 0 2px; display: none }\n' + + '.vis-tbl-grid { height: calc(100% - 20px); width: 100%; }\n' const styleElem = document.createElement('style') styleElem.innerHTML = style this.dom.appendChild(styleElem) + const statusElem = document.createElement('div') + statusElem.setAttributeNS(null, 'id', 'vis-tbl-status') + statusElem.setAttributeNS(null, 'class', 'vis-status-bar') + this.dom.appendChild(statusElem) + this.statusElem = statusElem + + const gridElem = document.createElement('div') + gridElem.setAttributeNS(null, 'id', 'vis-tbl-grid') + gridElem.className = 'vis-tbl-grid' + this.dom.appendChild(gridElem) + const tabElem = document.createElement('div') tabElem.setAttributeNS(null, 'id', 'vis-tbl-view') tabElem.setAttributeNS(null, 'class', 'scrollable ag-theme-alpine') - this.dom.appendChild(tabElem) + gridElem.appendChild(tabElem) this.tabElem = tabElem this.agGridOptions = { @@ -106,6 +143,7 @@ class TableVisualization extends Visualization { resizable: true, minWidth: 25, headerValueGetter: params => params.colDef.field, + cellRenderer: cellRenderer, }, onColumnResized: e => this.lockColumnSize(e), } @@ -198,28 +236,106 @@ class TableVisualization extends Visualization { dataTruncated = parsedData.all_rows_count !== rowData.length } - // If the table contains more rows than an upper limit, the engine will send only some of all rows. + // Update Status Bar + this.updateStatusBarControls( + parsedData.all_rows_count === undefined ? 1 : parsedData.all_rows_count, + dataTruncated + ) + // If data is truncated, we cannot rely on sorting/filtering so will disable. - // A pinned row is added to tell the user the row count and that filter/sort is disabled. - const col_span = '__COL_SPAN__' - if (dataTruncated) { - columnDefs[0].colSpan = p => p.data[col_span] || 1 - } this.agGridOptions.defaultColDef.filter = !dataTruncated this.agGridOptions.defaultColDef.sortable = !dataTruncated this.agGridOptions.api.setColumnDefs(columnDefs) - if (dataTruncated) { - const field = columnDefs[0].field - const extraRow = { - [field]: `Showing ${rowData.length} of ${parsedData.all_rows_count} rows. Sorting and filtering disabled.`, - [col_span]: columnDefs.length, - } - this.agGridOptions.api.setPinnedTopRowData([extraRow]) - } this.agGridOptions.api.setRowData(rowData) this.updateTableSize(this.dom.getAttributeNS(null, 'width')) } + makeOption(value, label) { + const optionElem = document.createElement('option') + optionElem.value = value + optionElem.appendChild(document.createTextNode(label)) + return optionElem + } + + makeButton(label, onclick) { + const buttonElem = document.createElement('button') + buttonElem.name = label + buttonElem.appendChild(document.createTextNode(label)) + buttonElem.addEventListener('click', onclick) + return buttonElem + } + + // Updates the status bar to reflect the current row limit and page, shown at top of the visualization. + // - Creates the row dropdown and page buttons. + // - Updated the row counts and filter available options. + updateStatusBarControls(all_rows_count, dataTruncated) { + const pageLimit = Math.ceil(all_rows_count / this.row_limit) + if (this.page > pageLimit) { + this.page = pageLimit + } + + if (this.statusElem.childElementCount === 0) { + this.statusElem.appendChild( + this.makeButton('«', () => this.setRowLimitAndPage(this.row_limit, 0)) + ) + this.statusElem.appendChild( + this.makeButton('‹', () => this.setRowLimitAndPage(this.row_limit, this.page - 1)) + ) + + const selectElem = document.createElement('select') + selectElem.name = 'row-limit' + selectElem.addEventListener('change', e => { + this.setRowLimitAndPage(e.target.value, this.page) + }) + this.statusElem.appendChild(selectElem) + + const rowCountSpanElem = document.createElement('span') + this.statusElem.appendChild(rowCountSpanElem) + + this.statusElem.appendChild( + this.makeButton('›', () => this.setRowLimitAndPage(this.row_limit, this.page + 1)) + ) + this.statusElem.appendChild( + this.makeButton('»', () => this.setRowLimitAndPage(this.row_limit, pageLimit - 1)) + ) + } + + // Enable/Disable Page buttons + this.statusElem.children.namedItem('«').disabled = this.page === 0 + this.statusElem.children.namedItem('‹').disabled = this.page === 0 + this.statusElem.children.namedItem('›').disabled = this.page === pageLimit - 1 + this.statusElem.children.namedItem('»').disabled = this.page === pageLimit - 1 + + // Update row limit dropdown and row count + const rowCountElem = this.statusElem.getElementsByTagName('span')[0] + const rowLimitElem = this.statusElem.children.namedItem('row-limit') + if (all_rows_count > 1000) { + rowLimitElem.style.display = 'inline-block' + const rowCounts = [1000, 2500, 5000, 10000, 25000, 50000, 100000].filter( + r => r <= all_rows_count + ) + if ( + all_rows_count < rowCounts[rowCounts.length - 1] && + rowCounts.indexOf(all_rows_count) === -1 + ) { + rowCounts.push(all_rows_count) + } + rowLimitElem.innerHTML = '' + rowCounts.forEach(r => { + const option = this.makeOption(r, r.toString()) + rowLimitElem.appendChild(option) + }) + rowLimitElem.value = this.row_limit + + rowCountElem.innerHTML = dataTruncated + ? ` of ${all_rows_count} rows (Sorting/Filtering disabled).` + : ` rows.` + } else { + rowLimitElem.style.display = 'none' + rowCountElem.innerHTML = all_rows_count === 1 ? '1 row.' : `${all_rows_count} rows.` + } + } + updateTableSize(clientWidth) { // Check the grid has been initialised and return if not. if (!this.agGridOptions) { diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso index 39a4794cd60e..e97d9bec3014 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso @@ -40,6 +40,8 @@ from project.Data.Json import Json, Invalid_JSON, JS_Object from project.Data.Numbers import Decimal, Integer, Number, Number_Parse_Error from project.Data.Text.Text_Sub_Range import Codepoint_Ranges, Text_Sub_Range +from project.Metadata import make_single_choice + import project.Data.Index_Sub_Range as Index_Sub_Range_Module polyglot java import com.ibm.icu.lang.UCharacter @@ -333,7 +335,8 @@ Text.match self pattern=".*" case_sensitivity=Case_Sensitivity.Sensitive = Split with a vector of strings. 'azbzczdzezfzg'.split ['b', 'zez'] == ['az', 'zczd', 'fzg'] -Text.split : Text | Vector Text -> Case_Sensitivity -> Boolean -> Vector Text | Illegal_Argument +@delimiter (make_single_choice [',', ';', '|', ['{tab}', "'\t'"], ['{space}', "' '"], ['{newline}', "['\n', '\r\n', '\r']"], ['Custom', ""]]) +Text.split : Text | Vector Text -> Case_Sensitivity -> Boolean -> Vector Text | Illegal_Argument Text.split self delimiter="," case_sensitivity=Case_Sensitivity.Sensitive use_regex=False = delimiter_is_empty = case delimiter of _ : Text -> delimiter.is_empty diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso index 6f669108d347..c985a248e65e 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso @@ -77,3 +77,11 @@ type Widget ## Describes a file chooser. File_Browse label:(Nothing | Text)=Nothing display:Display=Display.When_Modified action:File_Action=File_Action.Open file_types:(Vector Pair)=[Pair.new "All Files" "*.*"] + +## PRIVATE +make_single_choice : Vector -> Display -> Widget +make_single_choice values display=Display.Always = + make_option value = case value of + _ : Vector -> Choice.Option value.first value.second + _ : Text -> Choice.Option value value.pretty + Widget.Single_Choice (values.map make_option) Nothing display diff --git a/distribution/lib/Standard/Visualization/0.0.0-dev/src/Table/Visualization.enso b/distribution/lib/Standard/Visualization/0.0.0-dev/src/Table/Visualization.enso index 16d12e2819fa..8b4be931d5f0 100644 --- a/distribution/lib/Standard/Visualization/0.0.0-dev/src/Table/Visualization.enso +++ b/distribution/lib/Standard/Visualization/0.0.0-dev/src/Table/Visualization.enso @@ -39,6 +39,10 @@ prepare_visualization y max_rows=1000 = dataframe = x.read max_rows all_rows_count = x.row_count make_json_for_table dataframe [] all_rows_count + _ : Function -> + pairs = [['_display_text_', '[Function '+x.to_text+']']] + value = JS_Object.from_pairs pairs + JS_Object.from_pairs [["json", value]] _ -> js_value = x.to_js_object value = if js_value.is_a JS_Object . not then js_value else @@ -96,7 +100,7 @@ make_json_for_object_matrix current vector idx=0 = if idx == vector.length then _ -> js_object = row.to_js_object if js_object.is_a JS_Object . not then False else - if js_object.field_names.sort == ["type" , "constructor"] then False else + if js_object.field_names.sort == ["constructor", "type"] then False else pairs = js_object.field_names.map f-> [f, make_json_for_value (js_object.get f)] JS_Object.from_pairs pairs if to_append == False then Nothing else @@ -184,4 +188,5 @@ make_json_for_value val level=0 = case val of truncated = val.columns.take 5 . map _.name prepared = if val.column_count > 5 then truncated + ["… " + (val.column_count - 5).to_text+ " more"] else truncated "Table{" + val.row_count.to_text + " rows x [" + (prepared.join ", ") + "]}" + _ : Function -> "[Function "+val.to_text+"]" _ -> val.to_display_text