From 603f8cd279a9683b4b1742399b0046633d8832fe Mon Sep 17 00:00:00 2001 From: J01024 Date: Mon, 5 Feb 2024 21:09:15 +0000 Subject: [PATCH] Choose position to freeze tabulator columns. --- examples/reference/widgets/Tabulator.ipynb | 30 ++++++++- panel/tests/ui/widgets/test_tabulator.py | 74 ++++++++++++++++++++++ panel/widgets/tables.py | 41 +++++++----- 3 files changed, 129 insertions(+), 16 deletions(-) diff --git a/examples/reference/widgets/Tabulator.ipynb b/examples/reference/widgets/Tabulator.ipynb index cf99f53653..c061c224ef 100644 --- a/examples/reference/widgets/Tabulator.ipynb +++ b/examples/reference/widgets/Tabulator.ipynb @@ -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", @@ -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": {}, diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 0c4e21e3ed..fc310df93f 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -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) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 8f7eeca56b..3ee409deb5 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -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 @@ -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()