diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index bece13fef4..fdd2ae0e6b 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -3047,3 +3047,55 @@ def test_header_filters_categorial_dtype(): def test_tabulator_aggregators(document, comm, df_agg, aggs): tabulator = Tabulator(df_agg, hierarchical=True, aggregators=aggs) tabulator.get_root(document, comm) + + +def make_column_order_df(): + return pd.DataFrame({ + 'col1': [1, 2, 3], 'col2': [4, 5, 6], + 'col3': [7, 8, 9], 'col4': [10, 11, 12], + }) + + +def test_tabulator_column_order_default(): + tab = Tabulator(make_column_order_df()) + fields = [c.field for c in tab._get_columns()] + assert fields == ['index', 'col1', 'col2', 'col3', 'col4'] + + +def test_tabulator_column_order_filters_and_reorders(): + tab = Tabulator(make_column_order_df(), column_order=['col3', 'col1']) + fields = [c.field for c in tab._get_columns()] + assert fields == ['index', 'col3', 'col1'] + + +def test_tabulator_column_order_dynamic_update(): + tab = Tabulator(make_column_order_df(), column_order=['col3', 'col1']) + tab.column_order = ['col2', 'col4', 'col1'] + assert [c.field for c in tab._get_columns()] == ['index', 'col2', 'col4', 'col1'] + + tab.column_order = None + assert [c.field for c in tab._get_columns()] == ['index', 'col1', 'col2', 'col3', 'col4'] + + +def test_tabulator_column_order_ignores_invalid_columns(): + df = pd.DataFrame({'col1': [1, 2, 3], 'col2': [4, 5, 6], 'col3': [7, 8, 9]}) + tab = Tabulator(df, column_order=['col3', 'nonexistent', 'col1']) + fields = [c.field for c in tab._get_columns()] + assert fields == ['index', 'col3', 'col1'] + + +def test_tabulator_column_order_index_not_affected(): + df = pd.DataFrame( + {'col1': [1, 2], 'col2': [3, 4]}, + index=pd.Index([10, 20], name='my_index') + ) + tab = Tabulator(df, column_order=['col2']) + fields = [c.field for c in tab._get_columns()] + assert fields == ['my_index', 'col2'] + + +def test_tabulator_column_order_model_columns(document, comm): + tab = Tabulator(make_column_order_df(), column_order=['col3', 'col1']) + model = tab.get_root(document, comm) + fields = [c.field for c in model.columns] + assert fields == ['index', 'col3', 'col1'] diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 698db5e3c4..1d66a92df3 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1261,6 +1261,14 @@ class Tabulator(BaseTable): hidden_columns = param.List(default=[], nested_refs=True, doc=""" List of columns to hide.""") + column_order = param.List(default=None, allow_None=True, doc=""" + An ordered list of columns to display. If None (default), all + columns are displayed in the order inherited from the underlying + data structure. If a list is provided, only the listed columns + will be displayed in the order they appear within the list. + Columns may be omitted to hide them. Index columns cannot be + reordered or hidden using this parameter.""") + layout = param.Selector(default='fit_data_table', objects=[ 'fit_data', 'fit_data_fill', 'fit_data_stretch', 'fit_data_table', 'fit_columns'], doc=""" @@ -1356,7 +1364,7 @@ class Tabulator(BaseTable): _content_params: ClassVar[list[str]] = _data_params + ['expanded', 'row_content', 'embed_content'] - _manual_params: ClassVar[list[str]] = BaseTable._manual_params + _config_params + _manual_params: ClassVar[list[str]] = BaseTable._manual_params + _config_params + ['column_order'] _priority_changes: ClassVar[list[str]] = ['data', 'filters'] @@ -1364,7 +1372,8 @@ class Tabulator(BaseTable): 'selection': None, 'row_content': None, 'row_height': None, 'text_align': None, 'header_align': None, 'header_filters': None, 'header_tooltips': None, 'styles': 'cell_styles', - 'title_formatters': None, 'sortable': None, 'initial_page_size': None + 'title_formatters': None, 'sortable': None, 'initial_page_size': None, + 'column_order': None, } # Determines the maximum size limits beyond which (local, remote) @@ -1404,6 +1413,23 @@ def __init__(self, value=None, **params): self.style._todo = style._todo self.param.selection.callable = self._get_selectable + def _get_columns(self) -> list: + if self.value is None: + return [] + indexes = self.indexes + all_fields = self._get_fields() + + if self.column_order is not None: + ordered_fields = indexes + [ + col for col in self.column_order + if col in all_fields and col not in indexes + ] + else: + ordered_fields = all_fields + + df = self.value.reset_index() if len(indexes) > 1 else self.value + return self._get_column_definitions(ordered_fields, df) + @param.depends('value', watch=True, on_init=True) def _apply_max_size(self): """