Dash AgGrid Dynamic Row Spanning

Hi,

Out of curiosity, is it possible to dynamically span rows in Dash AgGrid? All the examples I am seeing are hard-coded on number of rows to span. Thanks in advance.

Yes, it’s possible. The rowSpan prop accepts a function. Can you say more about how you would like to specify the number of rows to span?

1 Like

For a table with columns “Student” and 5 other columns corresponding to test scores I would like the student column to span however many rows are present for the specific student i.e the rows represent different years. How can I do this dynamically since not all students have the same number of rows?

Are you ready for this?

Check this out, this will support sorting and filters:

app.py

import dash_ag_grid as dag
from dash import Dash, html, Output, Input

app = Dash(__name__)

data = [
    {
        'localTime': '5:00am',
        'show': {'name': 'Wake Up Dublin', 'presenter': 'Andrew Connell'},
        'a': 0.231, 'b': 0.523, 'c': 0.423, 'd': 0.527,
    },
    {'localTime': '5:15am',
     'show': {'name': 'Wake Up Dublin', 'presenter': 'Andrew Connell'},'a': 0.423, 'b': 0.452, 'c': 0.523, 'd': 0.543},
    {'localTime': '5:30am', 'show': {'name': 'Wake Up Dublin', 'presenter': 'Andrew Connell'},'a': 0.537, 'b': 0.246, 'c': 0.426, 'd': 0.421},
    {'localTime': '5:45am', 'show': {'name': 'Wake Up Dublin', 'presenter': 'Andrew Connell'},'a': 0.893, 'b': 0.083, 'c': 0.532, 'd': 0.983},
    {
        'localTime': '6:00am',
        'show': {'name': 'Pure Back In The Day', 'presenter': 'Kevin Flanagan'},
        'a': 0.231, 'b': 0.523, 'c': 0.423, 'd': 0.527,
    },
    {'localTime': '6:15am', 'a': 0.423, 'b': 0.452, 'c': 0.523, 'd': 0.543, 'show': {'name': 'Pure Back In The Day', 'presenter': 'Kevin Flanagan'},},
    {'localTime': '6:30am', 'a': 0.537, 'b': 0.246, 'c': 0.426, 'd': 0.421, 'show': {'name': 'Pure Back In The Day', 'presenter': 'Kevin Flanagan'},},
    {'localTime': '6:45am', 'a': 0.893, 'b': 0.083, 'c': 0.532, 'd': 0.983, 'show': {'name': 'Pure Back In The Day', 'presenter': 'Kevin Flanagan'},},
    {
        'localTime': '7:00am',
        'show': {'name': 'The Queens Breakfast', 'presenter': 'Tony Smith'},
        'a': 0.231, 'b': 0.523, 'c': 0.423, 'd': 0.527,
    },
    {'localTime': '7:15am', 'a': 0.423, 'b': 0.452, 'c': 0.523, 'd': 0.543, 'show': {'name': 'The Queens Breakfast', 'presenter': 'Tony Smith'},},
    {'localTime': '7:30am', 'a': 0.537, 'b': 0.246, 'c': 0.426, 'd': 0.421, 'show': {'name': 'The Queens Breakfast', 'presenter': 'Tony Smith'},},
    {'localTime': '7:45am', 'a': 0.893, 'b': 0.083, 'c': 0.532, 'd': 0.983, 'show': {'name': 'The Queens Breakfast', 'presenter': 'Tony Smith'},},
    {
        'localTime': '8:00am',
        'show': {'name': 'Cosmetic Surgery', 'presenter': 'Niall Crosby'},
        'a': 0.231, 'b': 0.523, 'c': 0.423, 'd': 0.527,
    },
    {'localTime': '8:15am', 'a': 0.423, 'b': 0.452, 'c': 0.523, 'd': 0.543, 'show': {'name': 'Cosmetic Surgery', 'presenter': 'Niall Crosby'},},
    {'localTime': '8:30am', 'a': 0.537, 'b': 0.246, 'c': 0.426, 'd': 0.421, 'show': {'name': 'Cosmetic Surgery', 'presenter': 'Niall Crosby'},},
    {'localTime': '8:45am', 'a': 0.893, 'b': 0.083, 'c': 0.532, 'd': 0.983, 'show': {'name': 'Cosmetic Surgery', 'presenter': 'Niall Crosby'},},
    {
        'localTime': '8:00am',
        'show': {'name': 'Brickfield Park Sessions', 'presenter': 'Bricker McGee'},
        'a': 0.231, 'b': 0.523, 'c': 0.423, 'd': 0.527,
    },
    {'localTime': '8:15am', 'a': 0.423, 'b': 0.452, 'c': 0.523, 'd': 0.543, 'show': {'name': 'Brickfield Park Sessions', 'presenter': 'Bricker McGee'},},
    {'localTime': '8:30am', 'a': 0.537, 'b': 0.246, 'c': 0.426, 'd': 0.421, 'show': {'name': 'Brickfield Park Sessions', 'presenter': 'Bricker McGee'},},
    {'localTime': '8:45am', 'a': 0.893, 'b': 0.083, 'c': 0.532, 'd': 0.983, 'show': {'name': 'Brickfield Park Sessions', 'presenter': 'Bricker McGee'},},
]

columnDefs = [
    {'field': 'localTime'},
    {
        'field': 'show',
        'cellRenderer': "RowSpanningComplexCellRenderer",
        'rowSpan': {"function": "rowSpanningComplex(params, 'show')"},
        'cellClassRules': {"show-cell": "params.value && params.data.rowSpanning"},
        "width": 300
    },
    {'field': 'a'},
    {'field': 'b'},
    {'field': 'c'},
    {'field': 'd'},
    {'field': 'rowSpanning', 'hide': True}
]

app.layout = html.Div(
    [
        dag.AgGrid(
            id="grid",
            rowData=data,
            columnDefs=columnDefs,
            defaultColDef={"resizable": True, 'sortable': True, 'filter': True, 'flex': 1},
            dashGridOptions={"suppressRowTransform": True},
        ),
    ],
)

app.clientside_callback(
    """async function () {
        var api = await dash_ag_grid.getApiAsync("grid")
        api.redrawRows();        
        return dash_clientside.no_update
    }""",
    Output("grid", "id"),
    Input("grid", "virtualRowData"),
)

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

js file

var dagcomponentfuncs = (window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {});

dagfuncs.rowSpanningComplex = function (params, key) {
    spanning = 0
    var found;
    var firstFound;
    var included;
    params.api.forEachNodeAfterFilterAndSort((node) => {
        if (JSON.stringify(node.data[key]) == JSON.stringify(params.data[key]) || params.node.rowIndex == node.rowIndex) {
            if (!found && found != 0 && !included) {
                found = node.rowIndex
                firstFound = node.rowIndex
                if (found == params.node.rowIndex) {
                    included = true
                }
                spanning++
            } else {
                if ((node.rowIndex - found) == 1) {
                    found = node.rowIndex
                    if (found == params.node.rowIndex) {
                        included = true
                    }
                    spanning++
            }
        }
         }
         else if (!included) {
            found = null
            spanning = 0
        }
    })
    params.node.setDataValue('rowSpanning', included ? (firstFound == params.node.rowIndex ? spanning || 1 : 0) : 1)

    return included ? (firstFound == params.node.rowIndex ? spanning || 1 : 0) : 1;
}

var dagcomponentfuncs = (window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {});

dagcomponentfuncs.RowSpanningComplexCellRenderer = function (props) {
    let children;
    if (props.value && props.data.rowSpanning) {
        children = [
            React.createElement('div', {className: 'show-name'}, props.value.name),
            React.createElement('div', {className: 'show-presenter'}, props.value.presenter),
        ]
    }
    return React.createElement('div', null, children)
}

css file

.show-cell {
    background: #119dff;
    border-left: 1px solid lightgrey !important;
    border-right: 1px solid lightgrey !important;
    border-bottom: 1px solid lightgrey !important;
}

.show-name {
    font-weight: bold;
}

.show-presenter {
    font-style: italic;
}

3 Likes

Thanks for your reply. However, i tried replicating the above only to get the below…Am I missing something?

You need to be using the newest version of dash ag grid, this api was added recently.

Updated the DAG version to 2.3.0 and even updated my version of dash but still getting the same result :frowning:

Same thing about the function doesn’t exist?

No, it seemed to fix that but the spanned column is not displaying…

You need to make sure you have the Js files in your asset folder, along with the css file. Then it should look the same.

Already do…is there a specific naming convention for these files?
image

Just checking, but did you refresh the page?

You got the naming convention right. The CSS files just need to end with .css and JavaScript .js

I ran the example @jinnyzor posted and it worked just like in the images. To ensure it’s actually running the correct version, try including:


print(dag.__version__)