Expand/collapse rows of datatable

Hi,

I am trying to make a datatable that the user can “drill into” somehow, for instance by collapsing or expanding rows, and I’m hoping someone can point me in the right direction.

In the example below I would like for the user to be able to click on ‘car’ or ‘bmw’ to collapse or expand the rows where those columns have the same values. How the aggregation of the strings should work does not matter so much to me. It could show an empty cell, or the first value, or anything else. I would like to be able to control how the numbers are aggregated (sum, average, weighted average, …).

import pandas as pd
import dash
import dash_table

data = [['car', 'ford', 'focus', 10],
        ['car', 'bmw', 'm3', 2],
        ['car', 'bmw', 'i3',  2],
        ['motorcycle', 'yamaha', 'td2', 3]
        ]

df = pd.DataFrame(data=data, columns=['vehicle', 'manufacturer', 'model', 'sold'])

app = dash.Dash(__name__)

app.layout = dash_table.DataTable(
    id='table',
    columns=[{"name": i, "id": i} for i in df.columns],
    data=df.to_dict('records'),
)

if __name__ == '__main__':
    app.run_server(debug=False, port=myport, host=myip)

If that is not possible I had thought about having new datatables generated when a cell was clicked, but I’m not sure where to start with that. I’m also open to other similar suggestions.

I hope this was not too unclear :slight_smile:

1 Like

Hello @dhove, what you could do is to retrieve the active_cell (CTRL-F active_cell in https://dash.plot.ly/datatable/interactivity), and then do the collapse using tools provided by the Pandas library, such as query and groupby, then update the Dash Datatable accordingly. What do you think?

2 Likes

Alternatively, create several DataTables and hide/show them with html.Details & html.Summary

I ended up using @Emmanuelle suggestion. Although my execution is not the most elegant in the world, it is fairly dynamic, which I needed. So thanks for the input both of you :slight_smile:

In case anyone else has similar needs, I’ll leave my solution here as a starting point for them.


import dash
from dash.dependencies import Input, Output, State
import dash_table
import dash_html_components as html
import pandas as pd
import numpy as np

def defineDF():
    data = [['car', 'ford', 'focus', 'blue', 10],
            ['car', 'ford', 'focus', 'red', 12],
            ['car', 'bmw', 'm3', 'blue', 2],
            ['car', 'bmw', 'm3', 'black', 7],
            ['car', 'bmw', 'i3', 'red', 2],
            ['car', 'bmw', 'i3', 'blue', 2],
            ['motorcycle', 'yamaha', 'td2', 'black', 3],
            ['motorcycle', 'yamaha', 'td2', 'green', 4],
            ['motorcycle', 'honda', 'crf', 'white', 9]
            ]
    
    df = pd.DataFrame(data=data, columns=['vehicle', 'manufacturer', 'model', 'color', 'sold'])
    
    return df

startDF = defineDF()

app = dash.Dash(__name__)

app.layout = html.Div([
    dash_table.DataTable(
    id='testtable',
    columns=[{"name": i, "id": i} for i in startDF.columns],
    data=startDF.to_dict('records'),
    editable=True,
    selected_rows=[],
    ),
    html.Div(id='datatable-row-ids-container')
])


@app.callback(Output('testtable', 'data'),
              [Input('testtable', 'active_cell')],
              [State('testtable', 'data'),
               State('testtable', 'columns')],
              )
def updateGrouping(active_cell, power_position, power_position_cols):
    if active_cell is None:
        returndf = defineDF()
    else:
        currentColNames = [iii['name'] for iii in power_position_cols]
        tableData = pd.DataFrame(data=power_position, columns=currentColNames)
        columnNames = tableData.columns.values.tolist()
        
        lastColNumberList = [active_cell['column'] + 1]
        groupingColList = [columnNames[:lastColNumberList[0]]]
        presentingColList = [columnNames[:lastColNumberList[0]] + [columnNames[-1]]]

        rowNumber = active_cell['row']
        rowValueList = [pd.DataFrame(data=[tableData.iloc[rowNumber, :lastColNumberList[0]].values], 
                                     columns=groupingColList[0])]
        
        cellValue = tableData.iloc[rowNumber, lastColNumberList[0] - 1]

        if cellValue is None:
            nullElements = np.where(pd.isnull(tableData))
            nullRowColumn = np.column_stack((nullElements[0].tolist(), nullElements[1].tolist()))
            nullRowColumnDF = pd.DataFrame(nullRowColumn, columns=['row', 'column'])
            nullRowColumnDF = nullRowColumnDF.drop_duplicates(subset=['row'], keep='first')
            for index, rowColPair in nullRowColumnDF.iterrows():
                currentRow = rowColPair['row']
                currentColumn = rowColPair['column']
                if currentRow == rowNumber:
                    continue
                lastColNumberList += [currentColumn]
                currentGrouping = columnNames[:currentColumn]
                groupingColList += [currentGrouping]
                presentingColList += [currentGrouping  + [columnNames[-1]]]
                rowValueList += [pd.DataFrame(data=[tableData.iloc[currentRow, :currentColumn].values], 
                                              columns=currentGrouping)]

            tableData = defineDF()

        returndf = tableData.fillna('-') 
        for iii in range(len(rowValueList)):
            rowValueDF = rowValueList[iii]
            rowValueDF = rowValueDF.fillna('-')
            groupingCols = groupingColList[iii]
            presentingCols = presentingColList[iii]
            lastColNumber = lastColNumberList[iii]
            
            numberEmptyCells = len(np.where(rowValueDF.iloc[0,:] == '-')[0])
            lastColNumber -= numberEmptyCells
            rowValueDF = rowValueDF[:(len(rowValueDF.columns)-numberEmptyCells)]
            relevantRows = pd.merge(returndf.iloc[:, :lastColNumber].reset_index(), rowValueDF, how='inner').set_index('index')
            rowsToGroup = returndf.loc[relevantRows.index, :]
            rowsNotToGroup = returndf.drop(relevantRows.index)
            
            groupedDF = rowsToGroup.groupby(groupingCols, sort=False).sum().reset_index()[presentingCols]
            lenGrouopedDF = len(groupedDF.index)
            groupedDF = groupedDF.set_index(rowsToGroup.index[:lenGrouopedDF])
            returndf = groupedDF.append(rowsNotToGroup, ignore_index=False, sort=False)[columnNames]
            returndf = returndf.sort_index(inplace=False).reset_index(drop=True)
        returndf = returndf.replace({'-': None})
        
    return returndf.to_dict('rows')


if __name__ == '__main__':
    app.run_server(debug=False)
2 Likes

thank you for sharing your code! I’m glad it worked out for you.

Is there any way by which I can make accordion tables>

Hi @Emmanuelle! Is there a solution for row aggregation/grouping available in DataTable?

I think the better way is using html.Table instead of datatable which may require extra work.

Below is some code I’m working on. FYI.

import dash, random
import dash_bootstrap_components as dbc

from dash import dcc, html
from dash.dependencies import Input, Output, State, MATCH, ALL
from dash.exceptions import PreventUpdate

table_header = [
    html.Thead(html.Tr([html.Th("Open/Close"), html.Th("First Name"), html.Th("Last Name"), html.Th("Select")]))
]

row1 = html.Tr([
    html.Td(html.I(id={"type":"icon", "index": 1})),
    html.Td("Arthur"),
    html.Td(dbc.Input(value="test 1", type="text", id={"type":"txt-input", "index": 1})),
    html.Td(dbc.Select(id={"type":"select-input", "index": 1}, value="1", options=[{"label": "option 1", "value":"1"}, {"label": "option 2", "value":"2"}])),
    ],
    id={"type":"tr", "index": 1}
)

row1_collapsed = html.Tr(
    html.Td(
        html.Div(id={"type":"hidden", "index": 1}),
        colSpan=4,
    ),
    hidden=True,
    id={"type": "collapsed", "index": 1}
)

row2 = html.Tr([
    html.Td(html.I(id={"type":"icon", "index": 2})),
    html.Td("Ford"),
    html.Td(dbc.Input(value="test 1", type="text", id={"type":"txt-input", "index": 2})),
    html.Td(dbc.Select(id={"type":"select-input", "index": 2}, value="1", options=[{"label": "option 1", "value":"1"}, {"label": "option 2", "value":"2"}])),
    ],
    id={"type":"tr", "index": 2}
)

row2_collapsed = html.Tr(
    html.Td(
        html.Div(id={"type":"hidden", "index": 2}),
        colSpan=4,
    ),
    hidden=True,
    id={"type": "collapsed", "index": 2}
)

row3 = html.Tr([
    html.Td(html.I(id={"type":"icon", "index": 3})),
    html.Td("Zaphod"),
    html.Td(dbc.Input(value="test 1", type="text", id={"type":"txt-input", "index": 3}, disabled=True)),
    html.Td(dbc.Select(id={"type":"select-input", "index": 3}, value="1", disabled=True, options=[{"label": "option 1", "value":"1"}, {"label": "option 2", "value":"2"}])),
    ],
    id={"type":"tr", "index": 3}
)

row3_collapsed = html.Tr(
    html.Td(
        html.Div(id={"type":"hidden", "index": 3}),
        colSpan=4,
    ),
    hidden=True,
    id={"type": "collapsed", "index": 3}
)

row4 = html.Tr([
    html.Td(html.I(id={"type":"icon", "index": 4})),
    html.Td("Trillian"),
    html.Td(dbc.Input(value="test 1", type="text", id={"type":"txt-input", "index": 4}, debounce=True)),
    html.Td(dbc.Select(id={"type":"select-input", "index": 4}, value="1", options=[{"label": "option 1", "value":"1"}, {"label": "option 2", "value":"2"}])),
    ],
    id={"type":"tr", "index": 4}
)

row4_collapsed = html.Tr(
    html.Td(
        html.Div(id={"type":"hidden", "index": 4}),
        colSpan=4,
    ),
    hidden=True,
    id={"type": "collapsed", "index": 4}
)

table_body = [html.Tbody([row1, row1_collapsed, row2, row2_collapsed, row3, row3_collapsed, row4, row4_collapsed])]

table = dbc.Table(table_header + table_body, bordered=True)

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME])

app.layout = html.Div([
    table,
    html.Div(id={"type": "div", "index": 1}),
    html.Div(id={"type": "div", "index": 2}),
    html.Div(id={"type": "div", "index": 3}),
    html.Div(id={"type": "div", "index": 4}),
])


@app.callback(
    Output({"type": "txt-input", "index": MATCH}, 'value'),
    [Input({'type': 'txt-input', 'index': MATCH}, 'value'),
     Input({'type': 'txt-input', 'index': MATCH}, 'id'),
    ]
)
def update_text_input(value, id):
    print("values: " + str(value))
    print("id: " + str(id))
    return str(value)

@app.callback(
    Output({"type": "select-input", "index": MATCH}, 'value'),
    [Input({'type': 'select-input', 'index': MATCH}, 'value'),
     Input({'type': 'select-input', 'index': MATCH}, 'id'),
    ]
)
def update_select_input(value, id):
    print("values: " + str(value))
    print("id: " + str(id))
    return str(value)

@app.callback(
    [Output({"type": "collapsed", "index": MATCH}, 'hidden'),
     Output({"type": "hidden", "index": MATCH}, 'children'),
     Output({"type": "icon", "index": MATCH}, 'className'),
    ],
    [Input({'type': 'icon', 'index': MATCH}, 'n_clicks'),
     Input({'type': 'tr', 'index': MATCH}, 'id'),
     Input({'type': 'collapsed', 'index': MATCH}, 'hidden'),
    ]
)
def update_select_input(n_clicks, id, hidden):
    print("n_clicks: " + str(n_clicks))
    print("hidden:" + str(hidden))
    if n_clicks and n_clicks > 0:
        return not hidden, str(id) + " exposed", "far fa-eye-slash"
    else:
        return hidden, "hidden", "far fa-eye"

app.run_server(debug=True)

if __name__ == '__main__':
    app.run_server(debug=True)
1 Like

Can you explain this or give us the code how do we do expandable/collapsable rows of a data table please. I have this same requirement it should be like pivot in excel.

You could try the Dash Pivottable

Hi,

I’m new to building Plotly Dash apps and I’ve been exploring the capabilities of the enterprise version, particularly the rowGroup option which allows for creating expandable and collapsible tables. However, as I’m currently using the community version, I’m looking for a way to achieve similar functionality without access to the enterprise features.

Could someone please guide me on how to implement expandable and collapsible tables in Plotly Dash using the community version? Any help or pointers would be greatly appreciated.

Thank you!