Multi Index/Levels DataTable or AG Grid

Hi!
I just started learning dash and have seen the existence of DataTable and AG Grid and wanted to leverage all their great functionalities, only problem is that I have some dataframe which has multi indices and levels, and wanted to know if it was possible to display it as a DataTable or AG Grid.
I would like ideally the end result to look like this


but from what I’ve seen it seems that neither DataTable or AG Grid allow to display the rows name, let alone multi indices rows (meaning that are sub divided in two categories, in the picture Divider1 and Divider2). Is this even possible ?
So far I was only able to create the table in an html Table using the function here (https://community.plotly.com/t/dash-datatable-multi-index-tables-in-dash/6386/10) but it has some limitations.
Thank you very much for your help and time

Hi @marsdev,

Yes, this is possible, but it’s necessary to flatten the data first. Neither DataTable or AG Grid can accept multi level indexes.

Find more information about Column Groups here:

And Row spanning here:

1 Like

Hi, I’ve checked the resources you sent but unless I’m mistaking there isn’t a way to obtain visually what I want even when flattening the dataframe ?

If you provide some sample data (the flattened version), we can help create what you are looking for. Please just provide a few rows dummy data.

Absolutely, this is the data that is received:
data = [['', 'Levels', '1', '', '2', '', '3', '', '4', '', '5', '', '6', '', ''], ['Categories', 'Tag / Vers', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', ''], ['FirstCat', 'Divider1', '6', '7', '2', '3', '8', '8', '6', '7', '2', '4', '8', '8', ''], ['', 'Divider2', '0', '1', '0', '1', '0', '0', '0', '1', '0', '2', '0', '0', ''], ['SecondCat', 'Divider1', '1', '1', '3', '2', '2', '1', '1', '2', '2', '2', '3', '1', ''], ['', 'Divider2', '0', '0', '1', '0', '1', '0', '0', '1', '0', '0', '2', '0', '']]

I then have some code (let me know if needed) that converts the previous data to the following dataframe (multi level/indices) which looks like this :


What I did now, instead of transforming it into a multi level/indices dataframe I can simply take the data 2d list and simply do that:
df = pd.DataFrame(data)

which gets me something like this:


The result seems to keep what I want to have so I’m good with it, please if you can let me know how can I transform this to a DataTable or AG Grid it would be a huge help. Thank you for your time!

Could you send the data as well?

The data provided is a list of list, thats exactly what I give: data = [['', 'Levels', '1', '', '2', '', '3', '', '4', '', '5', '', '6', '', ''], ['Categories', 'Tag / Vers', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', ''], ['FirstCat', 'Divider1', '6', '7', '2', '3', '8', '8', '6', '7', '2', '4', '8', '8', ''], ['', 'Divider2', '0', '1', '0', '1', '0', '0', '0', '1', '0', '2', '0', '0', ''], ['SecondCat', 'Divider1', '1', '1', '3', '2', '2', '1', '1', '2', '2', '2', '3', '1', ''], ['', 'Divider2', '0', '0', '1', '0', '1', '0', '0', '1', '0', '0', '2', '0', '']]
the next I do operation is: df = pd.DataFrame(data) , and you get the same result as the last screen I sent

Unless you want something else ?

You already provided that data. Can you please include the code to make the the dataframe in the second image? With data in this format, it’s possible to make a table like your last image using AG Grid Column Groups and Row Spanning (links shared above)

This is the code I used, you need to pass the data I sent as is and just put False for is_supps

def table_to_df():
    
    data = [['', 'Levels', '1', '', '2', '', '3', '', '4', '', '5', '', '6', '', ''], ['Categories', 'Tag / Vers', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', 'first', 'sec', ''], ['FirstCat', 'Divider1', '6', '7', '2', '3', '8', '8', '6', '7', '2', '4', '8', '8', ''], ['', 'Divider2', '0', '1', '0', '1', '0', '0', '0', '1', '0', '2', '0', '0', ''], ['SecondCat', 'Divider1', '1', '1', '3', '2', '2', '1', '1', '2', '2', '2', '3', '1', ''], ['', 'Divider2', '0', '0', '1', '0', '1', '0', '0', '1', '0', '0', '2', '0', '']]
    row_categories = list(row[0] for row in data[2:][::2])
    column_subcategories = ['main', 'ref']
    severities = data[0][2::2]
    severities.pop()

    row_index = pd.MultiIndex.from_product([row_categories, ['All', 'Unique']], names=['variant', 'tag'])
    column_index = pd.MultiIndex.from_product([severities, column_subcategories], names=['version', 'level'])

    df = pd.DataFrame(index=row_index, columns=column_index)

    for i in range(2, len(data)):

        criteria = data[i][0] if data[i][0] != '' else data[i - 1][0]

        row_type = data[i][1]
        values = data[i][2:]

        for j in range(0, len(severities)):
            severity = severities[j]
            main_value = values[j * 2]
            ref_value = values[(j * 2) + 1]
            df.loc[(criteria, row_type), (severity, 'main')] = main_value
            df.loc[(criteria, row_type), (severity, 'ref')] = ref_value

        df.replace('', pd.NA, inplace=True)

        df.fillna(0, inplace=True)
        df.loc[(criteria, row_type)].fillna(0, inplace=True)

    for category in df.index.levels[0]:
        for row_type in df.index.levels[1]:

            s = df.loc[(category, row_type)] == 0
            if s.all():
                df.drop((category, row_type), inplace=True)

    try:
        df = df.astype(int)
    except ValueError:
        return df

    return df

Can you provide the reformat_supp_value function too? Or better yet a complete minimal example I can just run?

Done, Ive modified the function, you just need to import pandas and it should work!

@marsdev

This is still a multi level dataframe.
It needs to be flat.

For example, there needs to one column header for each column. it would need to look something like like:

variant   tag           main_1  ref_1   main_2  ref_2    etc....
FirstCat  Divider1       6        7        2        3   .
FirstCat  Divider2       0        1        0        1                  


If thats the format you want then just do the assumption that this is the data and it will yield the result you want:

import pandas as pd

def table_to_df():
    data = [
            ['Categories', 'Tag / Vers', 'first_1', 'sec_1', 'first_2', 'sec_2', 'first+3', 'sec_3', 'first_4', 'sec_4', 'first_5', 'sec_5',
             'first_6', 'sec_6', ''],
            ['FirstCat', 'Divider1', '6', '7', '2', '3', '8', '8', '6', '7', '2', '4', '8', '8', ''],
            ['First Cat', 'Divider2', '0', '1', '0', '1', '0', '0', '0', '1', '0', '2', '0', '0', ''],
            ['SecondCat', 'Divider1', '1', '1', '3', '2', '2', '1', '1', '2', '2', '2', '3', '1', ''],
            ['SecondCat', 'Divider2', '0', '0', '1', '0', '1', '0', '0', '1', '0', '0', '2', '0', '']]



    df = pandas.DataFrame(data)

But this is not at all what I had in mind, it loses a lot of readability to not have a multi level header and just have the same column name repeated but with the “_x” suffix. Is this the only way ? I have seen some table with multi level headers

@marsdev

As I mentioned above, it’s possible to format the grid so it looks the way you want. If you are working with dataframes it’s easier to turn mulit-level data into a flat dataframe with pandas first.

Note that the grid can accept more complex data (such as a dict or a list in cells) but it cannot accept a dataframe directly. (ie you can’t do rowData=df It needs to be in a json format.

Please be sure to read the sections on Column Groups and Row Spanning I referenced above for more information

Note also that if you would like to sort and/or filter the grid, it will be easier if you don’t include the row spanning.

from dash import Dash, html
import dash_ag_grid as dag
import pandas as pd

app = Dash(__name__)

data = [
    ['Categories', 'Tag / Vers', 'first_1', 'sec_1', 'first_2', 'sec_2', 'first_3', 'sec_3', 'first_4', 'sec_4', 'first_5', 'sec_5',
     'first_6', 'sec_6', ''],
    ['FirstCat', 'Divider1', '6', '7', '2', '3', '8', '8', '6', '7', '2', '4', '8', '8', ''],
    ['FirstCat', 'Divider2', '0', '1', '0', '1', '0', '0', '0', '1', '0', '2', '0', '0', ''],
    ['SecondCat', 'Divider1', '1', '1', '3', '2', '2', '1', '1', '2', '2', '2', '3', '1', ''],
    ['SecondCat', 'Divider2', '0', '0', '1', '0', '1', '0', '0', '1', '0', '0', '2', '0', '']
]

df = pd.DataFrame(data)
# make the first row the column names
df = pd.DataFrame(data[1:], columns=data[0])

# Replace empty strings with NaN
df.replace("", pd.NA, inplace=True)

columnDefs = [
    {
        "field": "Categories",
        "rowSpan": {"function": "params.node.id %2 === 0 ? 2 : 1"},
        "cellStyle": {"backgroundColor": "var(--ag-header-background-color"},
        "minWidth": 125
    },
    {"field": "Tag / Vers"},
] + [
    {
        "headerName": f"Level { i}",
        "children": [
            {"field": f"first_{i}", "headerName": "First"},
            {"field": f"sec_{i}", "headerName": "Second"},
        ],
    }
    for i in range(1, 7)
]


app.layout = html.Div(
    [
        dag.AgGrid(
            id="grid",
            rowData=df.to_dict("records"),
            columnDefs=columnDefs,
            columnSize="sizeToFit",
            defaultColDef={"minWidth": 100, "sortable": False},
            dashGridOptions={"suppressRowTransform": True},
        )
    ]
)

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

3 Likes

Thank you very much for your time and your wonderful solution! I just wanted to know, besides all the native features that AG Grid has, do you think its more computationally efficient than building an html table (as I mentioned in my first post here, using the multiindex_table function https://community.plotly.com/t/dash-datatable-multi-index-tables-in-dash/6386/10)

Also Ive tried using the code but theres just a little problem, when I run the server I get:

Error loading layout

And after further inspection on the browser:

TypeError: Type is not JSON serializable: NAType

And at the end:

The debugger caught an exception in your WSGI application. You can now look at the traceback which led to the error. If you enable JavaScript you can also use additional features such as code execution (if the evalex feature is enabled), automatic pasting of the exceptions and much more.

Brought to you by DON’T PANIC, your friendly Werkzeug powered traceback interpreter.

Console Locked

The console is locked and needs to be unlocked by entering the PIN. You can find the PIN printed out on the standard output of your shell that runs the server.

EDIT:

After removing this df.replace("", pd.NA, inplace=True) it now works