Skip to content

Commit

Permalink
Implement support for multi-index columns in Tabulator (#7108)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Aug 9, 2024
1 parent 07f9be3 commit 41f59e6
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 26 deletions.
48 changes: 28 additions & 20 deletions panel/models/tabulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,30 @@ const datetimeEditor = function(cell: any, onRendered: any, success: any, cancel
return input
}

function find_column(group: any, field: string): any {
if (group.columns != null) {
for (const col of group.columns) {
const found = find_column(col, field)
if (found) {
return found
}
}
} else {
return group.field === field ? group : null
}
}

function clone_column(group: any): any {
if (group.columns == null) {
return {...group}
}
const group_columns = []
for (const col of group.columns) {
group_columns.push(clone_column(col))
}
return {...group, columns: group_columns}
}

export class DataTabulatorView extends HTMLBoxView {
declare model: DataTabulator

Expand Down Expand Up @@ -849,13 +873,8 @@ export class DataTabulatorView extends HTMLBoxView {
columns.push({field: "_index", frozen: true, visible: false})
if (config_columns != null) {
for (const column of config_columns) {
if (column.columns != null) {
const group_columns = []
for (const col of column.columns) {
group_columns.push({...col})
}
columns.push({...column, columns: group_columns})
} else if (column.formatter === "expand") {
const new_column = clone_column(column)
if (column.formatter === "expand") {
const expand = {
hozAlign: "center",
cellClick: (_: any, cell: any) => {
Expand All @@ -869,7 +888,6 @@ export class DataTabulatorView extends HTMLBoxView {
}
columns.push(expand)
} else {
const new_column = {...column}
if (new_column.formatter === "rowSelection") {
new_column.cellClick = (_: any, cell: any) => {
cell.getRow().toggleSelect()
Expand All @@ -883,18 +901,8 @@ export class DataTabulatorView extends HTMLBoxView {
let tab_column: any = null
if (config_columns != null) {
for (const col of columns) {
if (col.columns != null) {
for (const c of col.columns) {
if (column.field === c.field) {
tab_column = c
break
}
}
if (tab_column != null) {
break
}
} else if (column.field === col.field) {
tab_column = col
tab_column = find_column(col, column.field)
if (tab_column != null) {
break
}
}
Expand Down
38 changes: 38 additions & 0 deletions panel/tests/widgets/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,44 @@ def test_tabulator_multi_index_remote_pagination(document, comm):
assert np.array_equal(model.source.data['C'], np.array(['foo1', 'foo2', 'foo3']))


def test_tabulator_multi_index_columns(document, comm):
level_1 = ['A', 'A', 'A', 'B', 'B', 'B']
level_2 = ['one', 'one', 'two', 'two', 'three', 'three']
level_3 = ['X', 'Y', 'X', 'Y', 'X', 'Y']

# Combine these into a MultiIndex
multi_index = pd.MultiIndex.from_arrays([level_1, level_2, level_3], names=['Level 1', 'Level 2', 'Level 3'])

# Create a DataFrame with this MultiIndex as columns
df = pd.DataFrame(np.random.randn(4, 6), columns=multi_index)

table = Tabulator(df)

model = table.get_root(document, comm)

assert model.configuration['columns'] == [
{'field': 'index', 'sorter': 'number'},
{'title': 'A', 'columns': [
{'title': 'one', 'columns': [
{'field': 'A_one_X', 'sorter': 'number'},
{'field': 'A_one_Y', 'sorter': 'number'},
]},
{'title': 'two', 'columns': [
{'field': 'A_two_X', 'sorter': 'number'}
]},
]},
{'title': 'B', 'columns': [
{'title': 'two', 'columns': [
{'field': 'B_two_Y', 'sorter': 'number'},
]},
{'title': 'three', 'columns': [
{'field': 'B_three_X', 'sorter': 'number'},
{'field': 'B_three_Y', 'sorter': 'number'}
]},
]}
]


def test_tabulator_expanded_content(document, comm):
df = makeMixedDataFrame()

Expand Down
34 changes: 28 additions & 6 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def _compute_renamed_cols(self):
self._renamed_cols.clear()
return
self._renamed_cols = {
str(col) if str(col) != col else col: col for col in self._get_fields()
('_'.join(col) if isinstance(col, tuple) else str(col)) if str(col) != col else col: col for col in self._get_fields()
}

def _reset_selection(self, event):
Expand Down Expand Up @@ -274,12 +274,14 @@ def _get_column_definitions(self, col_names: list[str], df: pd.DataFrame) -> lis
else:
col_kwargs['width'] = 0

title = self.titles.get(col, str(col))
col_name = '_'.join(col) if isinstance(col, tuple) else col
title = self.titles.get(col, str(col_name))
if col in indexes and len(indexes) > 1 and self.hierarchical:
title = 'Index: {}'.format(' | '.join(indexes))
elif col in self.indexes and col.startswith('level_'):
title = ''
column = TableColumn(field=str(col), title=title,

column = TableColumn(field=str(col_name), title=title,
editor=editor, formatter=formatter,
**col_kwargs)
columns.append(column)
Expand Down Expand Up @@ -1903,6 +1905,7 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any]
}
for i, column in enumerate(ordered_columns):
field = column.field
index = self._renamed_cols[field]
matching_groups = [
group for group, group_cols in grouping.items()
if field in group_cols
Expand Down Expand Up @@ -1934,14 +1937,13 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any]
title_formatter = dict(title_formatter)
col_dict['titleFormatter'] = title_formatter.pop('type')
col_dict['titleFormatterParams'] = title_formatter
col_name = self._renamed_cols[field]
if field in self.indexes:
if len(self.indexes) == 1:
dtype = self.value.index.dtype
else:
dtype = self.value.index.get_level_values(self.indexes.index(field)).dtype
else:
dtype = self.value.dtypes[col_name]
dtype = self.value.dtypes[index]
if dtype.kind == 'M':
col_dict['sorter'] = 'timestamp'
elif dtype.kind in 'iuf':
Expand Down Expand Up @@ -1971,7 +1973,27 @@ def _config_columns(self, column_objs: list[TableColumn]) -> list[dict[str, Any]
if isinstance(self.widths, dict) and isinstance(self.widths.get(field), str):
col_dict['width'] = self.widths[field]
col_dict.update(self._get_filter_spec(column))
if matching_groups:

if isinstance(index, tuple):
if columns:
children = columns
last = children[-1]
for group in index[:-1]:
if 'title' in last and last['title'] == group:
new = False
children = last['columns']
else:
new = True
children.append({
'columns': [],
'title': group,
})
last = children[-1]
if new:
children = last['columns']
children.append(col_dict)
column.title = index[-1]
elif matching_groups:
group = matching_groups[0]
if group in groups:
groups[group]['columns'].append(col_dict)
Expand Down

0 comments on commit 41f59e6

Please sign in to comment.