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 hierarchical aggregation for Tabulator #2624

Merged
merged 4 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions panel/models/tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class DataTabulator(HTMLBox):

hidden_columns = List(String)

indexes = List(String)

layout = Enum('fit_data', 'fit_data_fill', 'fit_data_stretch', 'fit_data_table', 'fit_columns', default="fit_data")

source = Instance(ColumnDataSource)
Expand Down
52 changes: 50 additions & 2 deletions panel/models/tabulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,45 @@ import {PanelHTMLBoxView, set_size} from "./layout"

declare const Tabulator: any;

function find_group(key: any, value: string, records: any[]): any {
for (const record of records) {
if (record[key] == value)
return record
}
return null
}

function group_data(records: any[], columns: any[], indexes: string[]): any[] {
const grouped = []
const index_field = columns[0].field
for (const record of records) {
const key = indexes[0]
const value = record[indexes[0]]
let group = find_group(key, value, grouped)
if (group == null) {
group = {_children: []}
group[index_field] = value
grouped.push(group)
}
let subgroup = group
for (const index of indexes.slice(1)) {
for (const column of columns.slice(1))
group[column.field] = ""
subgroup = find_group(index, record[index], subgroup._children)
if (subgroup == null) {
subgroup = {_children: []}
subgroup[index_field] = record[index]
}
group._children.push(subgroup)
group = subgroup
}
for (const column of columns.slice(1))
subgroup[column.field] = record[column.field]
}
return grouped
}


// The view of the Bokeh extension/ HTML element
// Here you can define how to render the model as well as react to model changes or View events.
export class DataTabulatorView extends PanelHTMLBoxView {
Expand Down Expand Up @@ -199,6 +238,8 @@ export class DataTabulatorView extends PanelHTMLBoxView {
data = []
else
data = transform_cds_to_records(cds, true)
if (configuration.dataTree)
data = group_data(data, this.model.columns, this.model.indexes)
return {
...configuration,
"data": data,
Expand Down Expand Up @@ -250,7 +291,10 @@ export class DataTabulatorView extends PanelHTMLBoxView {
tab_column.formatter = "tickCross"
else {
tab_column.formatter = (cell: any) => {
return column.formatter.doFormat(cell.getRow(), cell, cell.getValue(), null, null)
const formatted = column.formatter.doFormat(cell.getRow(), cell, cell.getValue(), null, null)
const node = div()
node.innerHTML = formatted
return node.children[0].innerHTML
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This completely breaks HTMLTemplateFormatter:

pn.widgets.Tabulator(pd.DataFrame({'link': ['foo']}), 
                     formatters={'link': HTMLTemplateFormatter(
                         template='<a href="<%= value %>">Link</a>'
                     )})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, can you open an issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, #2730

}
}
}
Expand Down Expand Up @@ -318,7 +362,9 @@ export class DataTabulatorView extends PanelHTMLBoxView {
// Update table

setData(): void {
const data = transform_cds_to_records(this.model.source, true);
let data = transform_cds_to_records(this.model.source, true);
if (this.model.configuration.dataTree)
data = group_data(data, this.model.columns, this.model.indexes)
if (this.model.pagination != null)
this.tabulator.rowManager.setData(data, true, false)
else
Expand Down Expand Up @@ -559,6 +605,7 @@ export namespace DataTabulator {
frozen_rows: p.Property<number[]>
groupby: p.Property<string[]>
hidden_columns: p.Property<string[]>
indexes: p.Property<string[]>
layout: p.Property<typeof TableLayout["__type__"]>
max_page: p.Property<number>
page: p.Property<number>
Expand Down Expand Up @@ -599,6 +646,7 @@ export class DataTabulator extends HTMLBox {
frozen_rows: [ Array(Number), [] ],
groupby: [ Array(String), [] ],
hidden_columns: [ Array(String), [] ],
indexes: [ Array(String), [] ],
layout: [ TableLayout, "fit_data" ],
max_page: [ Number, 0 ],
pagination: [ Nullable(String), null ],
Expand Down
18 changes: 11 additions & 7 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class BaseTable(ReactiveData, Widget):
Bokeh CellFormatter to use for a particular column
(overrides the default chosen based on the type).""")

hierarchical = param.Boolean(default=False, constant=True, doc="""
Whether to generate a hierachical index.""")

row_height = param.Integer(default=40, doc="""
The height of each table row.""")

Expand Down Expand Up @@ -91,7 +94,7 @@ def _get_columns(self):

indexes = self.indexes
col_names = list(self.value.columns)
if len(indexes) == 1:
if not self.hierarchical or len(indexes) == 1:
col_names = indexes + col_names
else:
col_names = indexes[-1:] + col_names
Expand Down Expand Up @@ -162,7 +165,7 @@ def _get_column_definitions(self, col_names, df):
col_kwargs['width'] = 0

title = self.titles.get(col, str(col))
if col in indexes and len(indexes) > 1 and self.hierarchical:
if col in indexes and len(indexes) > 1 and getattr(self, 'hierarchical', False):
title = 'Index: %s' % ' | '.join(indexes)
column = TableColumn(field=str(col), title=title,
editor=editor, formatter=formatter,
Expand Down Expand Up @@ -575,9 +578,6 @@ class DataFrame(BaseTable):
``"none"``
Do not automatically compute column widths.""")

hierarchical = param.Boolean(default=False, constant=True, doc="""
Whether to generate a hierachical index.""")

fit_columns = param.Boolean(default=None, doc="""
Whether columns should expand to the available width. This
results in no horizontal scrollbar showing up, but data can
Expand Down Expand Up @@ -765,11 +765,13 @@ class Tabulator(BaseTable):

_data_params = ['value', 'page', 'page_size', 'pagination', 'sorters']

_config_params = ['frozen_columns', 'groups', 'selectable']
_config_params = ['frozen_columns', 'groups', 'selectable', 'hierarchical']

_manual_params = BaseTable._manual_params + _config_params

_rename = {'disabled': 'editable', 'selection': None, 'selectable': 'select_mode'}
_rename = {
'disabled': 'editable', 'selection': None, 'selectable': 'select_mode'
}

def __init__(self, value=None, **params):
configuration = params.pop('configuration', {})
Expand Down Expand Up @@ -1035,6 +1037,7 @@ def _get_properties(self, source):
if self.pagination:
length = 0 if self._processed is None else len(self._processed)
props['max_page'] = length//self.page_size + bool(length%self.page_size)
props['indexes'] = self.indexes
return props

def _get_model(self, doc, root=None, parent=None, comm=None):
Expand Down Expand Up @@ -1140,6 +1143,7 @@ def _get_configuration(self, columns):
raise ValueError("Groups must be defined either explicitly "
"or via the configuration, not both.")
configuration['columns'] = self._config_columns(columns)
configuration['dataTree'] = self.hierarchical
return configuration

def download(self, filename='table.csv'):
Expand Down