Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to the Table visualization. #6653

Merged
merged 8 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -79,21 +91,46 @@ class TableVisualization extends Visualization {
return content
}

function cellRenderer(params) {
if (params.value === null) {
return '<span style="color:grey; font-style: italic;">Nothing</span>'
} else if (params.value === undefined) {
return ''
} else if (params.value === '') {
return '<span style="color:grey; font-style: italic;">Empty</span>'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about

Suggested change
return '<span style="color:grey; font-style: italic;">Empty</span>'
return '<span style="color:grey; font-style: italic;">Empty Text</span>'

or

Suggested change
return '<span style="color:grey; font-style: italic;">Empty</span>'
return '<span style="color:grey; font-style: italic;">""</span>'

Empty is not that clear to me that this is a text.

Personally I like "" most.

But if you think that for data analysts Empty will be clearer, I'm not against it.

}
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 = {
Expand All @@ -106,6 +143,7 @@ class TableVisualization extends Visualization {
resizable: true,
minWidth: 25,
headerValueGetter: params => params.colDef.field,
cellRenderer: cellRenderer,
},
onColumnResized: e => this.lockColumnSize(e),
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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