dash-table icon indicating copy to clipboard operation
dash-table copied to clipboard

With fixed rows, columns are as wide as the data and not the headers

Open chriddyp opened this issue 6 years ago • 10 comments

In this example, "Date received" is cut-off as "2015-02-01" is shorter than that column name. But in the complaints column, the column width is as wide as the cell's content. image

import dash
from dash.dependencies import Input, Output
import dash_html_components as html
import dash_design_kit as ddk
from dash_table import DataTable

import json
import pandas as pd

types = {
    'id': 'numeric',
    'ZIP code': 'text',
    'Date received': 'datetime',
    'Date sent to company': 'datetime',
}

df = pd.read_csv('1k-consumer-complaints.csv')
df['id'] = df['Unnamed: 0']
df = df.drop(['Unnamed: 0'], axis=1)
df = df.reindex(columns=['id']+df.columns[:-1].tolist())

app = dash.Dash(__name__)
app.scripts.config.serve_locally = True

app.layout = ddk.App([
    DataTable(
        id='demo-table',
        data=df.to_dict('rows'),
        columns=[{ 'id': i, 'name': i, 'type': types.get(i, 'any') } for i in df.columns],
        filtering='be',
        pagination_mode=False,
        virtualization=True,
        n_fixed_rows=2,
        style_cell={
            'min-width': '100px'
        },
        css=[
            { 'selector': '.row-1', 'rule': 'min-height: 500px;' }
        ]
    ),
    html.Pre(id='filter-input')
])


def to_string(filter):
    l_type = filter.get('type')
    l_sub = filter.get('subType')

    if l_type == 'relational-operator':
        if l_sub == '=':
            return '=='
        else:
            return l_sub
    else:
        return filter.get('value')


def handle_leaf(filter, df_filter):
        return (to_string(filter), df_filter)


def handle_default(filter, df_filter):
    left = filter.get('left', None)
    right = filter.get('right', None)

    (left_query, left_df) = to_panda_filter(left, df_filter)
    (right_query, right_df) = to_panda_filter(right, left_df)

    return ('{} {} {}'.format(
        left_query,
        to_string(filter) if left_query != '' and right_query != '' else '',
        right_query
    ).strip(), right_df)


def handle_contains(filter, df_filter):
    left = filter.get('left', None)
    right = filter.get('right', None)

    (left_query, left_df) = to_panda_filter(left, df_filter)
    (right_query, right_df) = to_panda_filter(right, left_df)

    return ('', right_df[right_df[left_query].astype(str).str.contains(right_query)])


def handle_datestartswith(filter, df_filter):
    left = filter.get('left', None)
    right = filter.get('right', None)

    (left_query, left_df) = to_panda_filter(left, df_filter)
    (right_query, right_df) = to_panda_filter(right, left_df)

    return ('', right_df[right_df[left_query].astype(str).str.startswith(right_query)])


def to_panda_filter(filter, df_filter):
    if filter is None:
        return ('', df_filter)

    l_type = filter.get('type')
    l_sub = filter.get('subType')

    left = filter.get('left', None)
    right = filter.get('right', None)

    if left is None and right is None:
        return handle_leaf(filter, df_filter)
    elif l_type == 'relational-operator' and l_sub == 'contains':
        return handle_contains(filter, df_filter)
    elif l_type == 'relational-operator' and l_sub == 'datestartswith':
        return handle_datestartswith(filter, df_filter)
    else:
        return handle_default(filter, df_filter)


@app.callback(
    [Output("demo-table", "data"),
     Output("filter-input", "children")],

    [Input("demo-table", "derived_filter_structure")]
)
def onFilterUpdate(filter):
    (pandas_query, df_filter) = to_panda_filter(filter, df)
    return [
        df_filter.query(pandas_query).to_dict('rows') if pandas_query != '' else df_filter.to_dict('rows'),
        json.dumps(filter, indent=4)
    ]


if __name__ == "__main__":
    app.run_server(debug=True)

Note: Remove limitation from https://dash.plotly.com/datatable/height (https://github.com/plotly/dash-docs/pull/847/files) once fixed

chriddyp avatar May 14 '19 04:05 chriddyp

I have the same issue. Any solutions?

liberbey avatar Oct 02 '19 15:10 liberbey

I found a temporary solution to this problem. Anyone having the same issue may use it:

def create_conditional_style(df):
    style=[]
    for col in df.columns:
        name_length = len(col)
        pixel = 50 + round(name_length*PIXEL_FOR_CHAR)
        pixel = str(pixel) + "px"
        style.append({'if': {'column_id': col}, 'minWidth': pixel})

    return style

liberbey avatar Oct 11 '19 12:10 liberbey

pixel = 50 + round(name_length*PIXEL_FOR_CHAR)

What's PIXEL_FOR_CHAR?

mccarthysean avatar Mar 13 '20 22:03 mccarthysean

Use PIXEL_FOR_CHAR=5. That worked fine for me.

saurabh-unraveldata avatar Apr 13 '20 22:04 saurabh-unraveldata

For whoever takes this, https://github.com/plotly/dash-table/issues/740 has some more examples.

chriddyp avatar Apr 14 '20 00:04 chriddyp

FYI - See https://dash.plotly.com/datatable/height for a discussion and an up to date list of workarounds: image

chriddyp avatar Aug 03 '20 19:08 chriddyp

I found a temporary solution to this problem. Anyone having the same issue may use it:

def create_conditional_style(df):
    style=[]
    for col in df.columns:
        name_length = len(col)
        pixel = 50 + round(name_length*PIXEL_FOR_CHAR)
        pixel = str(pixel) + "px"
        style.append({'if': {'column_id': col}, 'minWidth': pixel})

    return style

where to assign the returned style in the dash layout exactly please?

essefi-ahlem avatar Aug 24 '21 12:08 essefi-ahlem

Hi, is this being reviewed at all? I still have the same problem without any easy fix.

etiennecelery avatar Jul 30 '22 03:07 etiennecelery

I have the same issue as well. Currently using style_cell={'minWidth':70}, as a workaround.

asiago-pressato avatar Aug 30 '22 10:08 asiago-pressato

I have adapted the @liberbey code to take into consideration, not only the header, but also the column content length as well as the data type:

def create_conditional_style_full(df):
	PADDING = 24
	PIXEL_FOR_CHAR = 6
	style=[]
	for col in df.columns:
		if df[col].dtype == 'object':
			try:
				df[col] = pd.to_datetime(df[col])
			except ValueError:
				pass
		col_list = df[col].values.tolist()
		col_list = [s if type(s) is str else str(s) for s in col_list]
		col_list.append(col)
		name_length = len(max(col_list, key=len))
		pixel = PADDING + round(name_length*PIXEL_FOR_CHAR)
		pixel = str(pixel) + 'px'
		if pd.api.types.infer_dtype(df[col]) == 'string' or pd.api.types.infer_dtype(df[col]) == 'boolean' and not pd.api.types.is_datetime64_any_dtype(df[col]):
			style.append({'if': {'column_id': col}, 'minWidth': pixel, 'textAlign': 'left'})
		else: 		
			style.append({'if': {'column_id': col}, 'minWidth': pixel})
	return style

Then you add this to your layout like:

style_cell_conditional=create_conditional_style(df_weight),

In the function you will need to adjust PADDING and PIXEL_FOR_CHAR until your table looks OK.

Finally, in the layout, replace df_weight with the name of your dataframe.

mirix avatar Apr 27 '23 08:04 mirix