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

Add support for automatically determining optimal Tabulator page_size #6978

Merged
merged 4 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 4 additions & 1 deletion examples/reference/widgets/Tabulator.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@
"* **`header_filters`** (`boolean`/`dict`): A boolean enabling filters in the column headers or a dictionary providing filter definitions for specific columns.\n",
"* **`hidden_columns`** (`list`): List of columns to hide.\n",
"* **`hierarchical`** (boolean, default=False): Whether to render multi-indexes as hierarchical index (note hierarchical must be enabled during instantiation and cannot be modified later)\n",
"* **`initial_page_size`** (`int`, `default=20`): If pagination is enabled and `page_size` this determines the initial size of each page before rendering.\n",
"* **`layout`** (`str`, `default='fit_data_table'`): Describes the column layout mode with one of the following options `'fit_columns'`, `'fit_data'`, `'fit_data_stretch'`, `'fit_data_fill'`, `'fit_data_table'`. \n",
"* **`page`** (`int`, `default=1`): Current page, if pagination is enabled.\n",
"* **`page_size`** (`int`, `default=20`): Number of rows on each page, if pagination is enabled.\n",
"* **`page_size`** (`int | None`, `default=None`): Number of rows on each page, if pagination is enabled. By default the number of rows is automatically determined based on the number of rows that fit on screen. If None the initial amount of data is determined by the `initial_page_size`. \n",
"* **`pagination`** (`str`, `default=None`): Set to `'local` or `'remote'` to enable pagination; by default pagination is disabled with the value set to `None`.\n",
"* **`row_content`** (`callable`): A function that receives the expanded row (`pandas.Series`) as input and should return a Panel object to render into the expanded region below the row.\n",
"* **`selection`** (`list`): The currently selected rows as a list of integer indexes.\n",
Expand Down Expand Up @@ -822,6 +823,8 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that the default `page_size` is None, which means it will measure the height of the rows and try to fit the appropriate number of rows into the available space. To override the number of rows sent to the frontend before the measurement has taken place set the `initial_page_size`.\n",
"\n",
"Contrary to the `'remote'` option, `'local'` pagination transfers all of the data but still allows to display it on multiple pages:"
]
},
Expand Down
2 changes: 1 addition & 1 deletion panel/models/tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class DataTabulator(HTMLBox):

page = Nullable(Int)

page_size = Int()
page_size = Nullable(Int)

max_page = Int()

Expand Down
31 changes: 28 additions & 3 deletions panel/models/tabulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,31 @@ export class DataTabulatorView extends HTMLBoxView {
this.setStyles()

if (this.model.pagination) {
if (this.model.page_size == null) {
const table = this.shadow_el.querySelector(".tabulator-table")
const holder = this.shadow_el.querySelector(".tabulator-tableholder")
if (table != null && holder != null) {
const table_height = holder.clientHeight
let height = 0
let page_size = null
const heights = []
for (let i = 0; i<table.children.length; i++) {
const row_height = table.children[i].clientHeight
heights.push(row_height)
height += row_height
if (height > table_height) {
page_size = i
break
}
}
if (height < table_height) {
page_size = table.children.length
philippjfr marked this conversation as resolved.
Show resolved Hide resolved
const remaining = table_height - height
page_size += Math.floor(remaining / Math.min(...heights))
}
this.model.page_size = page_size
}
}
this.setMaxPage()
this.tabulator.setPage(this.model.page)
}
Expand Down Expand Up @@ -664,7 +689,7 @@ export class DataTabulatorView extends HTMLBoxView {
layout: this.getLayout(),
pagination: this.model.pagination != null,
paginationMode: this.model.pagination,
paginationSize: this.model.page_size,
paginationSize: this.model.page_size || 20,
paginationInitialPage: 1,
groupBy: this.groupBy,
rowFormatter: (row: any) => this._render_row(row),
Expand Down Expand Up @@ -1342,7 +1367,7 @@ export namespace DataTabulator {
layout: p.Property<typeof TableLayout["__type__"]>
max_page: p.Property<number>
page: p.Property<number>
page_size: p.Property<number>
page_size: p.Property<number | null>
pagination: p.Property<string | null>
select_mode: p.Property<any>
selectable_rows: p.Property<number[] | null>
Expand Down Expand Up @@ -1388,7 +1413,7 @@ export class DataTabulator extends HTMLBox {
max_page: [ Float, 0 ],
pagination: [ Nullable(Str), null ],
page: [ Float, 0 ],
page_size: [ Float, 0 ],
page_size: [ Nullable(Float), null ],
select_mode: [ Any, true ],
selectable_rows: [ Nullable(List(Float)), null ],
source: [ Ref(ColumnDataSource) ],
Expand Down
23 changes: 23 additions & 0 deletions panel/tests/ui/widgets/test_tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3370,6 +3370,29 @@ def test_tabulator_update_hidden_columns(page):
), page)


def test_tabulator_remote_pagination_auto_page_size_grow(page, df_mixed):
nrows, ncols = df_mixed.shape
widget = Tabulator(df_mixed, pagination='remote', initial_page_size=1, height=200)

serve_component(page, widget)

expect(page.locator('.tabulator-table')).to_have_count(1)

wait_until(lambda: widget.page_size == 4, page)


def test_tabulator_remote_pagination_auto_page_size_shrink(page, df_mixed):
nrows, ncols = df_mixed.shape
widget = Tabulator(df_mixed, pagination='remote', initial_page_size=10, height=150)

serve_component(page, widget)

expect(page.locator('.tabulator-table')).to_have_count(1)

wait_until(lambda: widget.page_size == 3, page)



class Test_RemotePagination:

@pytest.fixture(autouse=True)
Expand Down
45 changes: 25 additions & 20 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,13 +1084,16 @@ class Tabulator(BaseTable):
'fit_data', 'fit_data_fill', 'fit_data_stretch', 'fit_data_table',
'fit_columns'])

initial_page_size = param.Integer(default=20, bounds=(1, None), doc="""
Initial page size if page_size is None and therefore automatically set.""")

pagination = param.ObjectSelector(default=None, allow_None=True,
objects=['local', 'remote'])

page = param.Integer(default=1, doc="""
Currently selected page (indexed starting at 1), if pagination is enabled.""")

page_size = param.Integer(default=20, bounds=(1, None), doc="""
page_size = param.Integer(default=None, bounds=(1, None), doc="""
Number of rows to render per page, if pagination is enabled.""")

row_content = param.Callable(doc="""
Expand Down Expand Up @@ -1162,7 +1165,7 @@ class Tabulator(BaseTable):
'selection': None, 'row_content': None, 'row_height': None,
'text_align': None, 'embed_content': None, 'header_align': None,
'header_filters': None, 'styles': 'cell_styles',
'title_formatters': None, 'sortable': None
'title_formatters': None, 'sortable': None, 'initial_page_size': None
}

# Determines the maximum size limits beyond which (local, remote)
Expand Down Expand Up @@ -1255,7 +1258,7 @@ def _process_event(self, event) -> None:

event_col = self._renamed_cols.get(event.column, event.column)
if self.pagination == 'remote':
nrows = self.page_size
nrows = self.page_size or self.initial_page_size
event.row = event.row+(self.page-1)*nrows

idx = self._index_mapping.get(event.row, event.row)
Expand Down Expand Up @@ -1343,7 +1346,7 @@ def _get_data(self):
import pandas as pd
df = self._filter_dataframe(self.value)
df = self._sort_df(df)
nrows = self.page_size
nrows = self.page_size or self.initial_page_size
start = (self.page-1)*nrows

page_df = df.iloc[start: start+nrows]
Expand Down Expand Up @@ -1383,8 +1386,9 @@ def _get_style_data(self, recompute=True):
return {}
offset = 1 + len(self.indexes) + int(self.selectable in ('checkbox', 'checkbox-single')) + int(bool(self.row_content))
if self.pagination == 'remote':
start = (self.page-1)*self.page_size
end = start + self.page_size
page_size = self.page_size or self.initial_page_size
start = (self.page - 1) * page_size
end = start + page_size

# Map column indexes in the data to indexes after frozen_columns are applied
column_mapper = {}
Expand Down Expand Up @@ -1428,7 +1432,7 @@ def _get_selectable(self):
return None
df = self._processed
if self.pagination == 'remote':
nrows = self.page_size
nrows = self.page_size or self.initial_page_size
start = (self.page-1)*nrows
df = df.iloc[start:(start+nrows)]
return self.selectable_rows(df)
Expand All @@ -1445,7 +1449,7 @@ def _get_children(self, old={}):
from ..pane import panel
df = self._processed
if self.pagination == 'remote':
nrows = self.page_size
nrows = self.page_size or self.initial_page_size
start = (self.page-1)*nrows
df = df.iloc[start:(start+nrows)]
children = {}
Expand Down Expand Up @@ -1518,7 +1522,7 @@ def _update_children(self, *events):
def _stream(self, stream, rollover=None, follow=True):
if self.pagination == 'remote':
length = self._length
nrows = self.page_size
nrows = self.page_size or self.initial_page_size
max_page = max(length//nrows + bool(length%nrows), 1)
if self.page != max_page:
return
Expand All @@ -1532,7 +1536,7 @@ def stream(self, stream_value, rollover=None, reset_index=True, follow=True):
self._apply_update([], {'follow': follow}, model, ref)
if follow and self.pagination:
length = self._length
nrows = self.page_size
nrows = self.page_size or self.initial_page_size
self.page = max(length//nrows + bool(length%nrows), 1)
super().stream(stream_value, rollover, reset_index)
if follow and self.pagination:
Expand All @@ -1545,8 +1549,8 @@ def _patch(self, patch):
self._update_cds()
return
if self.pagination == 'remote':
nrows = self.page_size
start = (self.page-1)*nrows
nrows = self.page_size or self.initial_page_size
start = (self.page - 1) * nrows
end = start+nrows
filtered = {}
for c, values in patch.items():
Expand Down Expand Up @@ -1591,7 +1595,7 @@ def _update_selectable(self):

def _update_max_page(self):
length = self._length
nrows = self.page_size
nrows = self.page_size or self.initial_page_size
max_page = max(length//nrows + bool(length%nrows), 1)
self.param.page.bounds = (1, max_page)
for ref, (model, _) in self._models.items():
Expand All @@ -1615,8 +1619,8 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None):
indices.append((ind, iloc))
except KeyError:
continue
nrows = self.page_size
start = (self.page-1)*nrows
nrows = self.page_size or self.initial_page_size
start = (self.page - 1) * nrows
end = start+nrows
p_range = self._processed.index[start:end]
kwargs['indices'] = [iloc - start for ind, iloc in indices
Expand All @@ -1632,8 +1636,8 @@ def _update_column(self, column: str, array: np.ndarray):
with pd.option_context('mode.chained_assignment', None):
self._processed[column] = array
return
nrows = self.page_size
start = (self.page-1)*nrows
nrows = self.page_size or self.initial_page_size
start = (self.page - 1) * nrows
end = start+nrows
index = self._processed.iloc[start:end].index.values
self.value.loc[index, column] = array
Expand All @@ -1653,7 +1657,7 @@ def _update_selection(self, indices: list[int] | SelectionEvent):
ilocs = [] if indices.flush else self.selection.copy()
indices = indices.indices

nrows = self.page_size
nrows = self.page_size or self.initial_page_size
start = (self.page-1)*nrows
index = self._processed.iloc[[start+ind for ind in indices]].index
for v in index.values:
Expand All @@ -1678,7 +1682,8 @@ def _get_properties(self, doc: Document) -> dict[str, Any]:
properties['indexes'] = self.indexes
if self.pagination:
length = self._length
properties['max_page'] = max(length//self.page_size + bool(length%self.page_size), 1)
page_size = self.page_size or self.initial_page_size
properties['max_page'] = max(length//page_size + bool(length % page_size), 1)
if isinstance(self.selectable, str) and self.selectable.startswith('checkbox'):
properties['select_mode'] = 'checkbox'
else:
Expand Down Expand Up @@ -1720,7 +1725,7 @@ def _get_model(
model.children = self._get_model_children(
child_panels, doc, root, parent, comm
)
self._link_props(model, ['page', 'sorters', 'expanded', 'filters'], doc, root, comm)
self._link_props(model, ['page', 'sorters', 'expanded', 'filters', 'page_size'], doc, root, comm)
self._register_events('cell-click', 'table-edit', 'selection-change', model=model, doc=doc, comm=comm)
return model

Expand Down
Loading