Skip to content

Commit

Permalink
Upgrade Tabulator to 5.x (#2898)
Browse files Browse the repository at this point in the history
* Upgrade Tabulator to 5.x

* Fix pagination

* Update docs

* Various cleanup

* Addressed review comments
  • Loading branch information
philippjfr authored Nov 9, 2021
1 parent 1e80268 commit 002d3e1
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 105 deletions.
13 changes: 9 additions & 4 deletions examples/reference/widgets/Tabulator.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@
"metadata": {},
"outputs": [],
"source": [
"pn.widgets.Tabulator(df, header_align='center', text_align={'str': 'right', 'bool': 'center' }, widths=200)"
"pn.widgets.Tabulator(df, header_align='center', text_align={'str': 'right', 'bool': 'center'}, widths=200)"
]
},
{
Expand Down Expand Up @@ -711,7 +711,7 @@
"metadata": {},
"outputs": [],
"source": [
"large_df = pd._testing.makeCustomDataframe(100000, 5) "
"large_df = pd._testing.makeCustomDataframe(100000, 5)"
]
},
{
Expand Down Expand Up @@ -739,6 +739,7 @@
"outputs": [],
"source": [
"%%time\n",
"medium_df = pd._testing.makeCustomDataframe(10000, 5)\n",
"paginated_table = pn.widgets.Tabulator(large_df, pagination='local', page_size=10)\n",
"paginated_table"
]
Expand Down Expand Up @@ -1150,7 +1151,7 @@
"Panel does not expose all options available from Tabulator, if a desired option is not natively supported, it can be set via the `configuration` argument. \n",
"This dictionary can be seen as a base dictionary which the tabulator object fills and passes to the Tabulator javascript-library.\n",
"\n",
"As an example, we can turn off sorting and resizing of columns by disabling the `headerSort` and `resizableColumn` options."
"As an example, we can turn off sorting and resizing of columns by disabling the `headerSort` and `resizable` options."
]
},
{
Expand All @@ -1167,7 +1168,11 @@
" 'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10)]\n",
"}, index=[1, 2, 3])\n",
"\n",
"df_widget = pn.widgets.Tabulator(df, configuration={\"headerSort\": False, \"resizableColumns\": False})\n",
"df_widget = pn.widgets.Tabulator(df, configuration={'columnDefaults': {\n",
" 'resizable': False,\n",
" 'headerSort': True\n",
"}})\n",
"\n",
"df_widget.servable()"
]
},
Expand Down
6 changes: 3 additions & 3 deletions panel/models/tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
from ..io.resources import bundled_files
from ..util import classproperty

JS_SRC = "https://unpkg.com/tabulator-tables@4.9.3/dist/js/tabulator.js"
MOMENT_SRC = "https://unpkg.com/moment@2.27.0/moment.js"
JS_SRC = "https://unpkg.com/tabulator-tables@5.0.7/dist/js/tabulator.js"
MOMENT_SRC = "https://cdn.jsdelivr.net/npm/luxon/build/global/luxon.min.js"

THEME_PATH = "tabulator-tables@4.9.3/dist/css/"
THEME_PATH = "tabulator-tables@5.0.7/dist/css/"
THEME_URL = f"https://unpkg.com/{THEME_PATH}"
PANEL_CDN = f'https://cdn.jsdelivr.net/npm/@holoviz/panel/dist/bundled/{THEME_PATH}'
TABULATOR_THEMES = [
Expand Down
176 changes: 78 additions & 98 deletions panel/models/tabulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,11 @@ function group_data(records: any[], columns: any[], indexes: string[], aggregato
}


// The view of the Bokeh extension/ HTML element
// Here you can define how to render the model as well as react to model changes or View events.
export class DataTabulatorView extends PanelHTMLBoxView {
model: DataTabulator;
tabulator: any;
_tabulator_cell_updating: boolean=false
_data_updating: boolean = true
_updating_page: boolean = true
_selection_updating: boolean =false
_styled_cells: any[] = []
_styles: any;
Expand All @@ -132,9 +130,7 @@ export class DataTabulatorView extends PanelHTMLBoxView {
this.tabulator.download(ftype, this.model.filename)
})

this.connect(this.model.properties.children.change, () => {
this._render_children()
})
this.connect(this.model.properties.children.change, () => this._render_children())

this.connect(this.model.properties.expanded.change, () => {
for (const row of this.tabulator.rowManager.getRows()) {
Expand All @@ -143,34 +139,20 @@ export class DataTabulatorView extends PanelHTMLBoxView {
}
})

this.connect(this.model.properties.hidden_columns.change, () => {
this.hideColumns()
})

this.connect(this.model.properties.page_size.change, () => {
this.setPageSize();
})

this.connect(this.model.properties.page.change, () => {
this.setPage();
})

this.connect(this.model.properties.max_page.change, () => {
this.setMaxPage();
})

this.connect(this.model.properties.frozen_rows.change, () => {
this.freezeRows()
})

this.connect(this.model.properties.styles.change, () => {
this._styles = this.model.styles
this.updateStyles()
})

this.connect(this.model.source.properties.data.change, () => {
this.setData()
this.connect(this.model.properties.hidden_columns.change, () => this.hideColumns())
this.connect(this.model.properties.page_size.change, () => this.setPageSize())
this.connect(this.model.properties.page.change, () => {
if (!this._updating_page)
this.setPage()
})
this.connect(this.model.properties.max_page.change, () => this.setMaxPage())
this.connect(this.model.properties.frozen_rows.change, () => this.freezeRows())
this.connect(this.model.source.properties.data.change, () => this.setData())
this.connect(this.model.source.streaming, () => this.addData())
this.connect(this.model.source.patching, () => this.updateOrAddData())
this.connect(this.model.source.selected.change, () => this.updateSelection())
Expand All @@ -185,6 +167,29 @@ export class DataTabulatorView extends PanelHTMLBoxView {
this.invalidate_layout()
}

init_callbacks(): void {
this.tabulator.on("tableBuilding", () => this.tableInit())
this.tabulator.on("tableBuilt", () => this.tableBuilt())
this.tabulator.on("rowSelectionChanged", (data: any, rows: any) => this.rowSelectionChanged(data, rows))
this.tabulator.on("rowClick", (e: any, row: any) => this.rowClicked(e, row))
this.tabulator.on("cellEdited", (cell: any) => this.cellEdited(cell))
this.tabulator.on("selectableCheck", (row: any) => {
const selectable = this.model.selectable_rows
return (selectable == null) || (selectable.indexOf(row._row.data._index) >= 0)
})
this.tabulator.on("tooltips", (cell: any) => {
return cell.getColumn().getField() + ": " + cell.getValue();
})
this.tabulator.on("scrollVertical", debounce(() => {
this.updateStyles()
}, 50, false))
this.tabulator.on("rowFormatter", (row: any) => this._render_row(row))
this.tabulator.on("dataFiltering", () => {
if (this.tabulator != null)
this.model.filters = this.tabulator.getHeaderFilters()
})
}

render(): void {
super.render()
const wait = this.setCSS()
Expand All @@ -197,17 +202,27 @@ export class DataTabulatorView extends PanelHTMLBoxView {
let configuration = this.getConfiguration()

this.tabulator = new Tabulator(container, configuration)
this.init_callbacks()
this._render_children()

// Swap pagination mode
if (this.model.pagination === 'remote') {
this.tabulator.options.pagination = this.model.pagination
this.tabulator.modules.page.mode = 'remote'
}

this.setGroupBy()
this.hideColumns()

this.el.appendChild(container)
}

tableInit(): void {
// Patch the ajax request and page data parsing methods
const ajax = this.tabulator.modules.ajax
ajax.sendRequest = () => {
return this.requestPage(ajax.params.page, ajax.params.sort)
}
this.tabulator.modules.page._parseRemoteData = (): boolean => {
return false
}
}

tableBuilt(): void {
// Set up page
if (this.model.pagination) {
this.setMaxPage()
Expand All @@ -216,27 +231,23 @@ export class DataTabulatorView extends PanelHTMLBoxView {
} else {
this.freezeRows()
}
this.el.appendChild(container)
}

tableInit(view: DataTabulatorView, tabulator: any): void {
// Patch the ajax request and page data parsing methods
const ajax = tabulator.modules.ajax
ajax.sendRequest = () => {
return view.requestPage(ajax.params.page, ajax.params.sorters)
}
tabulator.modules.page._parseRemoteData = () => {}
this.tabulator.redraw(true)
this.updateStyles()
this.updateSelection()
this._initializing = false
}

requestPage(page: number, sorters: any[]): Promise<void> {
return new Promise((resolve: any, reject: any) => {
try {
if (page != null && sorters != null) {
if (this._data_updating)
this._data_updating = false
else
this.model.sorters = sorters
this.model.page = page || 1
this.model.sorters = sorters
this._updating_page = true
try {
this.model.page = page || 1
} finally {
this._updating_page = false
}
}
resolve([])
} catch(err) {
Expand All @@ -245,16 +256,6 @@ export class DataTabulatorView extends PanelHTMLBoxView {
})
}

renderComplete(): void {
// Only have to set up styles after initial render subsequent
// styling is handled by change event on styles property
if (this._initializing) {
this.updateStyles()
this.updateSelection()
}
this._initializing = false
}

freezeRows(): void {
for (const row of this.model.frozen_rows)
this.tabulator.getRow(row).freeze()
Expand All @@ -277,44 +278,23 @@ export class DataTabulatorView extends PanelHTMLBoxView {
}

getConfiguration(): any {
const pagination = this.model.pagination == 'remote' ? 'local': (this.model.pagination || false)
// Only use selectable mode if explicitly requested otherwise manually handle selections
let selectable = this.model.select_mode === 'toggle' ? true : NaN
const that = this
let configuration = {
...this.model.configuration,
index: "_index",
nestedFieldSeparator: false,
selectable: selectable,
tableBuilding: function() { that.tableInit(that, this) },
renderComplete: () => this.renderComplete(),
rowSelectionChanged: (data: any, rows: any) => this.rowSelectionChanged(data, rows),
rowClick: (e: any, row: any) => this.rowClicked(e, row),
cellEdited: (cell: any) => this.cellEdited(cell),
columns: this.getColumns(),
layout: this.getLayout(),
pagination: pagination,
pagination: this.model.pagination != null,
paginationMode: this.model.pagination,
paginationSize: this.model.page_size,
paginationInitialPage: 1,
selectableCheck: (row: any) => {
const selectable = this.model.selectable_rows
return (selectable == null) || (selectable.indexOf(row._row.data._index) >= 0)
},
tooltips: (cell: any) => {
return cell.getColumn().getField() + ": " + cell.getValue();
},
scrollVertical: debounce(() => {
this.updateStyles()
}, 50, false),
rowFormatter: (row: any) => this._render_row(row),
dataFiltering: () => {
if (this.tabulator != null)
this.model.filters = this.tabulator.getHeaderFilters()
}
}
if (pagination) {
if (this.model.pagination === "remote") {
configuration['ajaxURL'] = "http://panel.pyviz.org"
configuration['ajaxSorting'] = true
configuration['sortMode'] = "remote"
}
const cds: any = this.model.source;
let data: any[]
Expand Down Expand Up @@ -525,25 +505,21 @@ export class DataTabulatorView extends PanelHTMLBoxView {
return view.inputEl
}

after_layout(): void {
super.after_layout()
if (this.tabulator != null)
this.tabulator.redraw(true)
this.updateStyles()
}

// Update table

setData(): void {
let data = transform_cds_to_records(this.model.source, true);
getData(): any[] {
let data = transform_cds_to_records(this.model.source, true)
if (this.model.configuration.dataTree)
data = group_data(data, this.model.columns, this.model.indexes, this.model.aggregators)
this._data_updating = true
return data
}

setData(): void {
const data = this.getData()
if (this.model.pagination != null)
this.tabulator.rowManager.setData(data, true, false)
else {
this.tabulator.setData(data)
this._data_updating = false
}
this.freezeRows()
this.updateSelection()
Expand Down Expand Up @@ -703,14 +679,18 @@ export class DataTabulatorView extends PanelHTMLBoxView {
this.tabulator.deselectRow()
this.tabulator.selectRow(indices)
// This actually places the selected row at the top of the table
this.tabulator.scrollToRow(indices[0], "bottom", false)
for (const index of indices) {
const row = this.tabulator.rowManager.findRow(index)
if (row)
this.tabulator.scrollToRow(index, "bottom", false).catch(() => {})
}
this._selection_updating = false
}

// Update model

rowClicked(e: any, row: any) {
if (this._selection_updating || this._initializing || (typeof this.model.select_mode) === 'string' || this.model.select_mode === false)
if (this._selection_updating || this._initializing || (typeof this.model.select_mode) === 'string' || this.model.select_mode === false || this.model.configuration.dataTree)
return
let indices: number[] = []
const selected = this.model.source.selected
Expand Down Expand Up @@ -756,7 +736,7 @@ export class DataTabulatorView extends PanelHTMLBoxView {
}

rowSelectionChanged(data: any, _: any): void {
if (this._selection_updating || this._initializing || (typeof this.model.select_mode) === 'boolean' || (this.model.select_mode.startsWith('checkbox')))
if (this._selection_updating || this._initializing || (typeof this.model.select_mode) === 'boolean' || (typeof this.model.select_mode) === 'number' || this.model.select_mode.startsWith('checkbox') || this.model.configuration.dataTree)
return
const indices: number[] = data.map((row: any) => row._index)
const filtered = this._filter_selected(indices)
Expand Down

0 comments on commit 002d3e1

Please sign in to comment.