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

Implement support for multi-index columns in Tabulator #7108

Merged
merged 2 commits into from
Aug 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
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
Loading