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

Choose position to freeze tabulator columns. #6309

Merged
merged 2 commits into from
Feb 9, 2024
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
30 changes: 29 additions & 1 deletion examples/reference/widgets/Tabulator.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@
"* **`expanded`** (`list`): The currently expanded rows as a list of integer indexes.\n",
"* **`filters`** (`list`): A list of client-side filter definitions that are applied to the table.\n",
"* **`formatters`** (`dict`): A dictionary mapping from column name to a bokeh `CellFormatter` instance or *Tabulator* formatter specification.\n",
"* **`frozen_columns`** (`list`): List of columns to freeze, preventing them from scrolling out of frame. Column can be specified by name or index.\n",
"* **`frozen_columns`** (`list` or `dict`): Defines the frozen columns:\n",
" * `list`\n",
" List of columns to freeze, preventing them from scrolling out of frame. Column can be specified by name or index.\n",
" * `dict`\n",
" Dict of columns to freeze and the position in table (`'left'` or `'right'`) to freeze them in. Column names or index can be used as keys. If value does not match\n",
" `left` or `right` then the default behaviour is to not be frozen at all.\n",
"* **`frozen_rows`**: (`list`): List of rows to freeze, preventing them from scrolling out of frame. Rows can be specified by positive or negative index.\n",
"* **`groupby`** (`list`): Groups rows in the table by one or more columns.\n",
"* **`header_align`** (`dict` or `str`): A mapping from column name to header alignment or a fixed header alignment, which should be one of `'left'`, `'center'`, `'right'`.\n",
Expand Down Expand Up @@ -635,6 +640,29 @@
"pn.widgets.Tabulator(df, frozen_columns=['index'], width=400)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"By default, columns given in the list format are frozen to the left hand side of the table. If you want to customize where columns are frozen to on the table, you can specify this with a dictionary:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.widgets.Tabulator(df, frozen_columns={'index': 'left', 'float': 'right'}, width=400)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The 'index' column will be frozen on the left side of the table, and the 'float' on the right. Non-frozen columns will scroll between these two."
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
74 changes: 74 additions & 0 deletions panel/tests/ui/widgets/test_tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,80 @@ def test_tabulator_frozen_columns(page, df_mixed):
assert int_bb == page.locator('text="int"').bounding_box()


def test_tabulator_frozen_columns_with_positions(page, df_mixed):
widths = 100
width = int(((df_mixed.shape[1] + 1) * widths) / 2)
frozen_cols = {"float": "left", "int": "right"}
widget = Tabulator(df_mixed, frozen_columns=frozen_cols, width=width, widths=widths)

serve_component(page, widget)

expected_text = """
float
index
str
bool
date
datetime
int
3.14
idx0
A
true
2019-01-01
2019-01-01 10:00:00
1
6.28
idx1
B
true
2020-01-01
2020-01-01 12:00:00
2
9.42
idx2
C
true
2020-01-10
2020-01-10 13:00:00
3
-2.45
idx3
D
false
2019-01-10
2020-01-15 13:00:00
4
"""
# Check that the whole table content is on the page, it is not in the
# same order as if the table was displayed without frozen columns
table = page.locator('.pnx-tabulator.tabulator')
expect(table).to_have_text(
expected_text,
use_inner_text=True
)

float_bb = page.locator('text="float"').bounding_box()
int_bb = page.locator('text="int"').bounding_box()
str_bb = page.locator('text="str"').bounding_box()

# Check that the float column is rendered before the int col
assert float_bb['x'] < int_bb['x']

# Check the bool is before int column
assert str_bb['x'] < int_bb['x']

# Scroll to the right, and give it a little extra time
page.locator('text="2019-01-01 10:00:00"').scroll_into_view_if_needed()

# Check that the position of one of the non-frozen columns has indeed moved
wait_until(lambda: page.locator('text="str"').bounding_box()['x'] < str_bb['x'], page)

# Check that the two frozen columns haven't moved after scrolling right
assert float_bb == page.locator('text="float"').bounding_box()
assert int_bb == page.locator('text="int"').bounding_box()


def test_tabulator_frozen_rows(page):
arr = np.array(['a'] * 10)

Expand Down
41 changes: 26 additions & 15 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1051,9 +1051,12 @@ class Tabulator(BaseTable):
List of client-side filters declared as dictionaries containing
'field', 'type' and 'value' keys.""")

frozen_columns = param.List(default=[], nested_refs=True, doc="""
List indicating the columns to freeze. The column(s) may be
selected by name or index.""")
frozen_columns = param.ClassSelector(class_=(list, dict), default=[], nested_refs=True, doc="""
One of:
- List indicating the columns to freeze. The column(s) may be
selected by name or index.
- Dict indicating columns to freeze as keys and their freeze location
as values, freeze location is either 'right' or 'left'.""")

frozen_rows = param.List(default=[], nested_refs=True, doc="""
List indicating the rows to freeze. If set, the
Expand Down Expand Up @@ -1771,23 +1774,31 @@ def _config_columns(self, column_objs: List[TableColumn]) -> List[Dict[str, Any]
"frozen": True,
"width": 40,
})

ordered = []
for col in self.frozen_columns:
if isinstance(col, int):
ordered.append(column_objs.pop(col))
else:
cols = [c for c in column_objs if c.field == col]
if cols:
ordered.append(cols[0])
column_objs.remove(cols[0])
ordered += column_objs
if isinstance(self.frozen_columns, dict):
left_frozen_columns = [col for col in column_objs if
self.frozen_columns.get(col.field, self.frozen_columns.get(column_objs.index(col))) == "left"]
right_frozen_columns = [col for col in column_objs if
self.frozen_columns.get(col.field, self.frozen_columns.get(column_objs.index(col))) == "right"]
non_frozen_columns = [col for col in column_objs if
col.field not in self.frozen_columns and column_objs.index(col) not in self.frozen_columns]
ordered_columns = left_frozen_columns + non_frozen_columns + right_frozen_columns
else:
ordered_columns = []
for col in self.frozen_columns:
if isinstance(col, int):
ordered_columns.append(column_objs.pop(col))
else:
cols = [c for c in column_objs if c.field == col]
if cols:
ordered_columns.append(cols[0])
column_objs.remove(cols[0])
ordered_columns += column_objs

grouping = {
group: [str(gc) for gc in group_cols]
for group, group_cols in self.groups.items()
}
for i, column in enumerate(ordered):
for i, column in enumerate(ordered_columns):
field = column.field
matching_groups = [
group for group, group_cols in grouping.items()
Expand Down
Loading