plotly / dash-table

OBSOLETE: now part of https://github.com/plotly/dash
https://dash.plotly.com
MIT License
420 stars 72 forks source link

[Feature Request] Multi header from multi index dataframe #414

Open philspbr opened 5 years ago

philspbr commented 5 years ago

It would be great to have the possibility to dynamically create tables and headers from multi index data frames as requested here: https://community.plot.ly/t/dash-datatable-multi-index-tables-in-dash/6386

MachineGunMichael commented 4 years ago

Are there any updates regarding this request?

shsab commented 3 years ago

As a workaround I am converting the df using the function below. This is not the best possible solution or a well written code but does the job. Help me improve it :) :

def df_to_table(df: pd.DataFrame,
                id: str = 'table',
                col_with_min: str = '80px',
                col_with_max: str = '120px',
                row_deletable: bool = True,
                row_selectable: str = 'single',
                exclude_cols: list = [],
                cell_conditional_style: list = [],
                data_conditional_style: list = [],
                style_table: dict = None,
                export: bool = False,
                page_size: int = 10,
                pct_cols: list = None):
    """
    Converts a dataframe to dash compatible HTML table

    :param df:
    :param id:
    :param col_with_min:
    :param col_with_max:
    :param row_deletable:
    :param row_selectable:
    :param exclude_cols:
    :param cell_conditional_style:
    :param data_conditional_style:
    :param style_table:
    :param export:
    :param page_size:
    :return:
    """
    is_multi_index = isinstance(df.columns, pd.core.indexes.multi.MultiIndex)

    _id = 'id'
    if is_multi_index:
        levels = len(df.columns.to_list()[0])
        _id = tuple([''] * (levels - 1) + ['id'])

    if isinstance(exclude_cols, list):
        _exclude_cols = exclude_cols + [_id]
    else:
        logger.warning(
            'Excluded columns should be a list of columns names! The provided value ignored!'
        )
        _exclude_cols = [_id]

    _df = df.copy()

    if _id not in df.columns:
        _df[_id] = df.index

    style_cell_conditional = []
    for col in df.columns:
        if col == 'id':
            continue
        if _df[col].dtype == 'O':
            style_cell_conditional.append({
                'if': {
                    'column_id': f'{col[-1] if is_multi_index else col}'
                },
                'textAlign': 'left',
                'height': 'auto',
                'minHeight': '10px',
                # all three widths are needed
                'minWidth': col_with_min,
                'width': 'auto',
                'maxWidth': col_with_max,
                'whiteSpace': 'normal'
            })
        else:
            style_cell_conditional.append({
                'if': {
                    'column_id': f'{col[-1] if is_multi_index else col}'
                },
                'textAlign': 'right',
                'whiteSpace': 'normal',
                'height': 'auto',
                'minHeight': '10px',
                # all three widths are needed
                'minWidth': col_with_min,
                'width': 'auto',
                'maxWidth': col_with_max,
            })

    style_cell_conditional = style_cell_conditional + cell_conditional_style

    def set_type(col):
        if _df[col].dtype == 'O':
            return 'text'
        if _df[col].dtype in ('float64', 'int64'):
            return 'numeric'
        if _df[col].dtype in ('datetime64[ns]'):
            return 'datetime'

    if is_multi_index:
        col_names = [{
            "name": list(i),
            "id": i[-1],
            'type': set_type(i),
            "format": Format(
                nully='N/A',
                precision=2,
                scheme=Scheme.fixed,
                sign=Sign.positive,
                symbol=Symbol.yes,
                symbol_suffix='%') if i in pct_cols or '%' in i[-1] else ''
        } for i in _df.columns if i not in _exclude_cols]

        def row_to_dict(row):
            _d = {}
            for idx in row.index:
                _d[idx[-1]] = row[idx]
            return _d

        _data = _df.apply(lambda row: row_to_dict(row), axis=1).to_list()
    else:
        col_names = [{
            "name": i,
            "id": i,
            'type': set_type(i),
            "format": Format(
                nully='N/A',
                precision=2,
                scheme=Scheme.fixed,
                sign=Sign.positive,
                symbol=Symbol.yes,
                symbol_suffix='%') if i in pct_cols or '%' in i else ''
        } for i in _df.columns if i not in _exclude_cols]
        _data = _df.to_dict('records')

    style_header = {
        'textAlign': 'center',
        'fontWeight': 'bold',
        'backgroundColor': 'rgb(190,190,190)',
    }

    style_data_conditional = [{
        'if': {
            'row_index': 'odd'
        },
        'backgroundColor': 'rgb(248, 248, 248)',
        'whiteSpace': 'normal'
    }]
    style_data_conditional = style_data_conditional + data_conditional_style
    kwargs = dict()
    if export:
        kwargs = dict(export_columns='all',
                      export_format='xlsx',
                      export_headers='display',
                      style_table={} if style_table is None else style_table)

    return ddt.DataTable(
        id=id,
        columns=col_names,
        data=_data,
        page_size=page_size,
        style_cell_conditional=style_cell_conditional,
        merge_duplicate_headers=True,
        # style_as_list_view=True,
        style_header=style_header,
        style_data_conditional=style_data_conditional,
        filter_action="native",
        sort_action="native",
        sort_mode="multi",
        row_deletable=row_deletable,
        row_selectable=row_selectable,
        **kwargs)

However, I am forced to move away from dash due to some restrictions in exporting data tables, and managing routes. I also hit a performance issue when the number of graphs and users increased. I switched to Flask. Still using Plotly.

alexcjohnson commented 3 years ago

Related: https://github.com/plotly/dash-table/issues/645