Announcing Dash Bio 1.0.0 🎉 : a one-stop-shop for bioinformatics and drug development visualizations.

Expand/collapse rows of datatable


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(
    columns=[{"name": i, "id": i} for i in df.columns],

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:

Hello @dhove, what you could do is to retrieve the active_cell (CTRL-F active_cell in, 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?


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([
    columns=[{"name": i, "id": i} for i in startDF.columns],

@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()
        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], 
        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:
                lastColNumberList += [currentColumn]
                currentGrouping = columnNames[:currentColumn]
                groupingColList += [currentGrouping]
                presentingColList += [currentGrouping  + [columnNames[-1]]]
                rowValueList += [pd.DataFrame(data=[tableData.iloc[currentRow, :currentColumn].values], 

            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__':

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(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.Div(id={"type":"hidden", "index": 1}),
    id={"type": "collapsed", "index": 1}

row2 = html.Tr([
    html.Td(html.I(id={"type":"icon", "index": 2})),
    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.Div(id={"type":"hidden", "index": 2}),
    id={"type": "collapsed", "index": 2}

row3 = html.Tr([
    html.Td(html.I(id={"type":"icon", "index": 3})),
    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.Div(id={"type":"hidden", "index": 3}),
    id={"type": "collapsed", "index": 3}

row4 = html.Tr([
    html.Td(html.I(id={"type":"icon", "index": 4})),
    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.Div(id={"type":"hidden", "index": 4}),
    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([
    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}),

    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)

    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)

    [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"
        return hidden, "hidden", "far fa-eye"


if __name__ == '__main__':