Search code examples
javascriptrreactable

How to render column total (with javascript) when using grouped rows in reactable r package?


I am using crosstalk in combination with reactable. In the table I use grouped rows. Now I need to add a column total, using javascript.

---
title: "Focal Chart (Top)"
output: 
  flexdashboard::flex_dashboard:
    orientation: rows
---

Inputs {.sidebar}
-------------------------------------

```{r}
library(crosstalk)
library(reactable)
cars <- MASS::Cars93[1:20, c("Manufacturer", "Model", "Type", "Price")]

data <- SharedData$new(cars)
filter_checkbox("type", "Type", data, ~Type)
    filter_slider("price", "Price", data, ~Price, width = "100%")
    filter_select("mfr", "Manufacturer", data, ~Manufacturer)
```

Row {data-height=500}
-------------------------------------

### Chart 1

```{r}
reactable(
  data,
  groupBy = "Manufacturer",
  searchable = TRUE,
columns = list(
  Price = colDef(aggregated = JS("
      function(cellInfo) {
        // Calculate sum of all values in visible rows
        var values = cellInfo.subRows.map(function(row) { return row.Price})
        var total = values.reduce(function(a, b) { return a + b }, 0)
        return total.toFixed(1)
      }
    "),  footer = JS("function(colInfo) {
        var values = colInfo.data.map(function(row) { return row[colInfo.column.id] })
        var total = values.reduce(function(a, b) { return a + b }, 0)
        return '$' + total.toFixed(2)
      }")
    ),
    Manufacturer = colDef(footer = "Total")
  ),
  defaultColDef = colDef(footerStyle = list(fontWeight = "bold"))
)
```

Row {data-height=500}
-------------------------------------

### empty

```{r}

```   

### empty

```{r}

```

As you can see, the column total is $NaN. Ofcourse this is an unwanted outcome. If I remove the groupBy parameter, the column total is $471.80, as it should be. Does anyone know how to fix this groupBy behaviour? Many thanks in advance!


Solution

  • Originally answered at https://github.com/glin/reactable/issues/50, just copying it here:

    This was an oversight in the reactable documentation -- the custom rendering docs don't mention what colInfo.data looks like when you have grouped/nested data: https://glin.github.io/reactable/articles/custom-rendering.html#footers

    Usually, data will be an array of individual row data objects, like

    {
      "Manufacturer": "Acura",
      "Model": "Integra",
      "Type": "Small",
      "Price": 15.9
    }
    

    However, when a row is aggregated, the individual sub row data will be nested under a _subRows property:

    // For an aggregated row with 2 sub rows
    {
      "Manufacturer": "Acura",
      "_subRows": [
        {
          "Manufacturer": "Acura",
          "Model": "Integra",
          "Type": "Small",
          "Price": 15.9
        },
        {
          "Manufacturer": "Acura",
          "Model": "Legend",
          "Type": "Midsize",
          "Price": 33.9
        }
      ]
    }
    

    You can insert a console.log(colInfo) in the JS function to explore the full data structure in your browser's JavaScript console:

    footer = JS("function(colInfo) {
      console.log(colInfo)
    }")
    

    Since the individual row data is now nested, it's a little more work to sum it all up, but here's one method that uses two forEach loops:

    library(reactable)
    
    data <- MASS::Cars93[1:20, c("Manufacturer", "Model", "Type", "Price")]
    
    reactable(
      data,
      groupBy = "Manufacturer",
      searchable = TRUE,
      columns = list(
        Price = colDef(
          aggregated = JS("function(cellInfo) {
            // Calculate sum of all values in visible rows
            var values = cellInfo.subRows.map(function(row) { return row.Price})
            var total = values.reduce(function(a, b) { return a + b }, 0)
            return total.toFixed(1)
          }"),
          footer = JS("function(colInfo) {
            var total = 0
            colInfo.data.forEach(function(row) { 
              row._subRows.forEach(function(data) { 
                total += data[colInfo.column.id]
              })
            })
            return '$' + total.toFixed(2)
          }")
        ),
        Manufacturer = colDef(footer = "Total")
      ),
      defaultColDef = colDef(footerStyle = list(fontWeight = "bold"))
    )