How to add expandable row details?

Hi,

My aim is to have expandable rows where I may display extended information for the user, like in this example from datatables.net:

Anyone have any idea how to acquire this?

Thanks,
Aroflote

Hello @Aroflote,

Yes, you can expand add rows to a datatable.

Check out the info here:

If you want to add specific information, you just need to use the callback to populate the info correctly.

Hi jinnyzor,

Thank you for your answer.

I cannot see any examples of what I want to do, but please point me in the direction you think about.

To elaborate, I don’t want to change neither the rows, nor their content.
I want to be able to expand a row to display additional information related to this row.

Aroflote

Ah, ok. Now it makes a little more sense.

Yes, this should be possible. I dont know if anyone has done this yet. I have something similar in javascript. I’ll post here in a bit with how it works.

As long as you have the information available, we should be able to get something to work.

1 Like

@Aroflote,

Do you have any examples of the data you want to work with?

Are you planning on storing the extra info in the table and just hide it? (This might be easier via javascript) Or we could store the additional information with a dcc.Store if its not possible to store it in the table.

I can tell you a bit about what I’m trying to make.

In my case, I am creating a table displaying live data for some devices.
The devices have different data, mostly numeric, as you see in this example:

The data you see is the most important data, and should be visible at all times. Some other data however is less critical, and should be possible to hide (collapse) when not necessary, and shown (uncollapsed) when wanted. This is the data I want to store in the collapsable detailed section.

One important details is that all data must be refreshed every 2nd or 5th second (depending on the data), so the collapsable part cannot be all static.

@Aroflote,

Here you go:

app.py -

import dash
from dash import html, dcc, Input, Output, dash_table
from dash.exceptions import PreventUpdate
import pandas as pd
import dash_bootstrap_components as dbc

data = pd.DataFrame({'showMore':['','',''],
                    'FullName':['Caesar Vance', 'Cara Stevens', 'Cedric Kelly'],
                     'JobDescription':['Pre-Sales Support', 'Sales Assistant', 'Senior JavaScript Developer'],
                     'Location':['New York','New York', 'Edinburgh'],
                     'Salary(Local)':['$106,450', '$145,600', '$433,060']})

data2 = data.copy()

data2['Salary(Local)'] = 'nada'

app = dash.Dash(__name__, external_stylesheets=[dbc.icons.FONT_AWESOME],
                external_scripts=[{'src':"https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"}])
app.layout = html.Div(id="parent", children=[
    html.Button('refresh',id='refresh', n_clicks=0),
    dash_table.DataTable(data=data.to_dict('records'),
                         columns=[{'name': i, 'id':i} for i in data.columns],
                         id='employeeTable',
                         style_cell={'textAlign': 'left'})
])

@app.callback(
    Output('employeeTable','data'),
    Input('refresh','n_clicks'),
    prevent_initial_call=True
)
def refreshData(n1):
    if n1 > 0:
        return data2.to_dict('records')
    return data.to_dict('records')



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

effects.js


function coolEffects() {
    console.log('adding effects')
    $("#employeeTable tr>td:first-of-type").unbind()
    $("#employeeTable tr>td:first-of-type").on("click",
        function() {
            row = $(this).closest('tr')
            if ($(row).hasClass("expanded")) {
                $(".extraInfo").remove()
                $(".expanded").removeClass("expanded")
            } else {
                $(".extraInfo").remove()
                $(".expanded").removeClass("expanded")
                $(row).addClass("expanded")
            }
            if ($(row).hasClass('expanded')) {
                info = ''
                $(row).find("td:nth-child(n+4)").each(function() {
                    info += $(this).attr('data-dash-column') + ": "+ $(this).text() +'\n'
                })
                $(row).after('<tr class="extraInfo"><td colspan="3" style="width: 100%; max-width:100%"><pre>'
                +info + '</pre></td></tr>')
            }
        }
    )
}

window.fetch = new Proxy(window.fetch, {
    apply(fetch, that, args) {
        // Forward function call to the original fetch
        const result = fetch.apply(that, args);

        // Do whatever you want with the resulting Promise
        result.then((response) => {
            if (args[0] == '/_dash-update-component') {
                setTimeout(function() {coolEffects()}, 1000)
            }})
        return result
        }
    }
    )

$(document).ready(function() {
    setTimeout(function() {coolEffects()}, 1000)
})

custom.css


#employeeTable tr:not(.extraInfo) > td:first-of-type:before {
    content: "\2b";
    font-family: "Font Awesome 6 Free";
    font-weight: 900;
    background-color: green;
    border-radius: 50%;
    color: white;
}

#employeeTable tr:not(.extraInfo).expanded > td:first-of-type:before {
    content: "\f068";
    font-family: "Font Awesome 6 Free";
    font-weight: 900;
    background-color: red;
    border-radius: 50%;
    color: white;
}

#employeeTable tr>td:nth-child(n+4), tr>th:nth-child(n+4) {
    display: none;
}

.extraInfo {
    border: 1pt solid black;
}

.extraInfo pre {
    text-align: left;
    white-space: pre-line;
}

The n+4 is where the magic happens, starting from that column, all the rest are hidden.

Here is the result:

The refresh button was to make sure that the table would still receive updates even in the expanded form, the extraInfo wont update until you open and close. In the current layout, there can only be one expanded details section.

5 Likes

Hi, thank you for the example.

I’m only able to produce this:


I simply added the files together in a folder like this:

image

How do you link the app.py together with logic in effect.js and styles in custom.css?

Aroflote

The JavaScript and css file need to be in assets folder that is in the same location as the app.

Wow, great! Thank you.

I see that I need to spend some time getting into custom JS/CSS templates, not being too familiar with neither from before. This is a great way to start with what I intend to make. Thank you again

Aroflote

1 Like

Hi again,

So I’ve implemented the solution to my code, and I am able to display the chosen columns in the details box as expected.

However, I’ve noticed that the data in the box doesn’t update together with the callback. To get new data, one must collapse and re-expand. I thought maybe this was handled by line 35-36 :

if args[0] == '/_dash-update-component' {
   setTimeout(function() {coolEffects()}, 1000)
}

but I guess not.

How can I update the data in the details box together with the rest of the data?

Aroflote

You’d have to add something to check for the expanded row when it is adding the event listener.

If there is one, then run the part where it makes the expanded pre again and pass the new text to the info pre.

Okay, will this be something in the right direction?
What kind of event can I use to make it trigger every time the function is called?

function coolEffects() {
    console.log('adding effects')
    $("#employeeTable tr>td:first-of-type").unbind()
    $("#employeeTable tr>td:first-of-type").on("click",
        function() {
            row = $(this).closest('tr')
            if ($(row).hasClass("expanded")) {
                $(".extraInfo").remove()
                $(".expanded").removeClass("expanded")
            } else {
                $(".extraInfo").remove()
                $(".expanded").removeClass("expanded")
                $(row).addClass("expanded")
            }
        }
    )

    $("#employeeTable tr>td:first-of-type").on("???",
        function() {
            row = $(this).closest('tr')
            if ($(row).hasClass('expanded')) {
                info = ''
                $(row).find("td:nth-child(n+4)").each(function() {
                    info += $(this).attr('data-dash-column') + ": "+ $(this).text() +'\n'
                })
                $(row).after('<tr class="extraInfo"><td colspan="3" style="width: 100%; max-width:100%"><pre>'
                + info + '</pre></td></tr>')
            }
        }
    )
}

Aroflote

You are on the right track, however, you dont need to add another event listener, as this is already triggered in the function of cooleffects.

Something like this might work:

function coolEffects() {
    console.log('adding effects')
    $("#employeeTable tr>td:first-of-type").unbind()
    $("#employeeTable tr>td:first-of-type").on("click",
        function() {
            row = $(this).closest('tr')
            if ($(row).hasClass("expanded")) {
                $(".extraInfo").remove()
                $(".expanded").removeClass("expanded")
            } else {
                $(".extraInfo").remove()
                $(".expanded").removeClass("expanded")
                $(row).addClass("expanded")
            }
            if ($(row).hasClass('expanded')) {
                info = ''
                $(row).find("td:nth-child(n+4)").each(function() {
                    info += $(this).attr('data-dash-column') + ": "+ $(this).text() +'\n'
                })
                $(row).after('<tr class="extraInfo"><td colspan="3" style="width: 100%; max-width:100%"><pre>'
                +info + '</pre></td></tr>')
            }
        }
    )

    if ($('.expanded')[0]) {
                info = ''
                $($('.expanded')[0]).find("td:nth-child(n+4)").each(function() {
                    info += $(this).attr('data-dash-column') + ": "+ $(this).text() +'\n'
                })
                $('.extraInfo').text(info)
            }
}

I havent tested it.

There is some weird formatting, but the data is being updated.
Thank you very much

1 Like

You can use html instead of text, and that might fix the formatting.

.html didn’t work but this did: children('td').children('pre').text(info)
I will look a bit around for a shorter version.

One more thing:
What causes an expanded row to collapse if I expand a different row?
Is there a way I can have several rows expanded at the same time?

Aroflote

Oh yeah. Whoops.

$(“.expandedInfo pre”).text

And I did that on purpose, we can expand multiple ones, but updating individual ones becomes more tricky.

1 Like

@Aroflote,

Give this alteration a try:

function coolEffects() {
    console.log('adding effects')
    $("#employeeTable tr>td:first-of-type").unbind()
    $("#employeeTable tr>td:first-of-type").on("click",
        function() {
            row = $(this).closest('tr')
            $(this).toggleClass('expanded')
            if ($(row).hasClass('expanded')) {
                info = ''
                $(row).find("td:nth-child(n+4)").each(function() {
                    info += $(this).attr('data-dash-column') + ": "+ $(this).text() +'\n'
                })
                $(row).after('<tr class="extraInfo"><td colspan="3" style="width: 100%; max-width:100%"><pre>'
                +info + '</pre></td></tr>')
            } else 
            {
                $(row).next().remove()
            }
        }
    )

    if ($('.expanded')[0]) {
        $('.expanded').each(function() {
            info = ''
            row = this
            $(row).find("td:nth-child(n+4)").each(function() {
                info += $(this).attr('data-dash-column') + ": "+ $(this).text() +'\n'
            })
            $(row).find('.extraInfo pre').text(info)
        })
    }
}
1 Like

@Aroflote,

Like I said, a little more tricky:

function coolEffects() {
    console.log('adding effects')
    $("#employeeTable tr>td:first-of-type").unbind()
    $("#employeeTable tr>td:first-of-type").on("click",
        function() {
            row = $(this).closest('tr')
            $(row).toggleClass('expanded')
            console.log(this)
            if ($(row).hasClass('expanded')) {
                info = ''
                $(row).find("td:nth-child(n+4)").each(function() {
                    info += $(this).attr('data-dash-column') + ": "+ $(this).text() +'\n'
                })
                $(row).after('<tr class="extraInfo"><td colspan="3" style="width: 100%; max-width:100%"><pre>'
                +info + '</pre></td></tr>')
            } else
            {
                $(row).next().remove()
            }
        }
    )

    if ($('.expanded')[0]) {
        $('.expanded').each(function() {
            info = ''
            row = this
            $(row).find("td:nth-child(n+4)").each(function() {
                info += $(this).attr('data-dash-column') + ": "+ $(this).text() +'\n'
            })
            $($(row).next().find('pre')[0]).text(info)
        })
    }
}