rdt

Is it possible to have cells in RowGroup in DT?


Can I have real perfectly aligned cells in the grouping rows, instead of this misaligned workaround?
The use of extension "RowGroup" is mandatory because of the collapsing
Ideally cells should not be individually hardcoded in JS as in this WA

enter image description here

library(shiny)
library(DT)

ui <- fluidPage(
  tags$head(
    tags$style(HTML("
      .dt-group-header {
        display: grid;
        grid-template-columns: 12fr 16fr 16fr 16fr;
        font-weight: bold;
        cursor: pointer;
        padding: 4px 8px;
      }
      .dt-group-header div {
        padding-right: 5px;
      }
      .dt-group-header div:first-child {
        text-align: left;
      }
      .dt-group-header div:nth-child(n+2) {
        text-align: right;
      }
    "))
  ),
  DTOutput("tbl")
)

server <- function(input, output, session) {
  dat <- data.frame(
    group = c("A", "A", "B", "B", "C"),
    value1 = c(10, 20, 30, 40, 50),
    value2 = c(5, 15, 25, 35, 45),
    value3 = c(2, 4, 6, 8, 10)
  )

  # list of groups, used for collapsed state
  groups <- unique(as.character(dat$group))

  output$tbl <- renderDT({
    datatable(
      dat,
      rownames = FALSE,
      extensions = "RowGroup",
      options = list(
        order = list(list(0, "asc")),
        columnDefs = list(
          # format numeric columns
          list(
            targets = 1:3,
            render = JS(
              "function(data,type,row,meta){",
              " if(type!=='display') return data;",
              " var num=parseFloat(data);",
              " if(isNaN(num)) return data;",
              " var f=Math.abs(num).toLocaleString(undefined,{minimumFractionDigits:0});",
              " return num<0?'($'+f+')':'$'+f;",
              "}"
            )
          )
        ),
        rowGroup = list(
          dataSrc = 0,
          startRender = JS(
            paste(
              "function(rows, group){",
              " if(!window.collapsedGroups) window.collapsedGroups={};",
              " var collapsed = !!window.collapsedGroups[group];",
              " var triangle = (group !== '') ? (collapsed ? '<span>▸</span> ' : '<span>▾</span> ') : '';",
              " rows.nodes().each(function(r){ r.style.display = collapsed ? 'none' : ''; });",
              " var t1 = rows.data().pluck(1).reduce(function(a,b){ return (+a)+(+b); }, 0);",
              " var t2 = rows.data().pluck(2).reduce(function(a,b){ return (+a)+(+b); }, 0);",
              " var t3 = rows.data().pluck(3).reduce(function(a,b){ return (+a)+(+b); }, 0);",
              " function fmt(n){ var f=Math.abs(n).toLocaleString(undefined,{minimumFractionDigits:0}); return n<0?'($'+f+')':'$'+f; }",
              " return '<div class=\"dt-group-header\" data-name=\"'+group+'\">' +",
              "        '<div>'+triangle+group+'</div>' +",
              "        '<div>'+fmt(t1)+'</div>' +",
              "        '<div>'+fmt(t2)+'</div>' +",
              "        '<div>'+fmt(t3)+'</div>' +",
              "        '</div>';",
              "}",
              collapse = "\n"
            )
          )
        )
      ),
      callback = JS(
        sprintf("window.collapsedGroups={%s};", paste(sprintf("'%s':true", groups), collapse = ", ")),
        "
        table.on('click', '.dt-group-header', function(){
          var name = $(this).data('name');
          if(name !== undefined){
            window.collapsedGroups[name] = !window.collapsedGroups[name];
            table.draw(false);
          }
        });
        "
      )
    )
  })
}

shinyApp(ui, server)


Solution

  • Assuming you mean that the grouping row headers should align with the table headers, you can

    1. remove padding left and right within thead th & tbody tr th
    2. use style display: table in the header row wrappers and display: table-cell; in the cells
    3. determine the percentage ratios of the dt-columns and set the dt-group-cell.style.widths accordingly
    4. build the non-group-header cells in a loop
    library(shiny)
    library(DT)
    
    ui <- fluidPage(
      tags$head(
        tags$style(HTML("
          .dt-group-header {
            display: table;
            width: 100%;
            table-layout: fixed;
            font-weight: bold;
            cursor: pointer;
          }
          .dt-group-cell:first-child {
            display: table-cell;
            text-align: left;
          }
          .dt-group-cell:not(:first-child) {
            display: table-cell;
            text-align: right;
            padding-right: 10px; /* adjust not-group name cell-padding here 10px is perfectly aligned - 20px for offset (better overview)*/
          }
          table.dataTable thead th, table.dataTable tbody tr th {
            padding-left: 0px !important; /* remove left right padding because it makes things much more difficult*/
            padding-right: 0px !important;
          } 
          
        "))
      ),
      DTOutput("tbl")
    )
    
    server <- function(input, output, session) {
      dat <- data.frame(
        group = c("A", "A", "B", "B", "C"),
        value1 = c(10, 20, 30, 40, 50),
        value2 = c(5, 15, 25, 35, 45),
        value3 = c(2, 4, 6, 8, 10),
        value4 = c(2, 4, 6, 8, 10)
      )
      
      # list of groups, used for collapsed state
      groups <- unique(as.character(dat$group))
      
      output$tbl <- renderDT({
        datatable(
          dat,
          rownames = FALSE,
          extensions = "RowGroup",
          options = list(
            order = list(list(0, "asc")),
            columnDefs = list(
              # format numeric columns
              list(
                targets = 1:(ncol(dat)-1), # regard JS indeces
                render = JS(
                  "function(data,type,row,meta){",
                  " if(type!=='display') return data;",
                  " var num=parseFloat(data);",
                  " if(isNaN(num)) return data;",
                  " var f=Math.abs(num).toLocaleString(undefined,{minimumFractionDigits:0});",
                  " return num<0?'($'+f+')':'$'+f;",
                  "}"
                )
              )
            ),
            rowGroup = list(
              dataSrc = 0,
              startRender = JS(
                paste(
                  "function(rows, group){",
                  " if(!window.collapsedGroups) window.collapsedGroups={};",
                  " var collapsed = !!window.collapsedGroups[group];",
                  " var columnWidths = [];",  # determine the percentage ratios of each column to assign to cell styles
                  " $('#DataTables_Table_0 > thead > tr > th').each(function(i) {",
                  "   header_width = $('#DataTables_Table_0 > thead > tr').width();",
                  "   columnWidths[i] = ($(this).width()/ header_width);",
                  " });",
                  " var triangle = (group !== '') ? (collapsed ? '<span>▸</span> ' : '<span>▾</span> ') : '';",
                  " rows.nodes().each(function(r){ r.style.display = collapsed ? 'none' : ''; });",
                  " function fmt(n){ var f=Math.abs(n).toLocaleString(undefined,{minimumFractionDigits:0}); return n<0?'($'+f+')':'$'+f; }",
                  " var cells = '';",
                  " columnWidths.forEach(function(w, i){",
                  "   if(i === 0){",
                  "     cells += '<div class=\"dt-group-cell\" style=\"width:'+w+'%\">'+triangle+group+'</div>';",
                  "   } else {",
                  "     var total = rows.data().pluck(i).reduce(function(a,b){ return (+a)+(+b); }, 0);",
                  "     cells += '<div class=\"dt-group-cell\" style=\"width:'+w+'%\">'+fmt(total)+'</div>';",
                  "   }",
                  " });",
                  " return '<div class=\"dt-group-header\" data-name=\"'+group+'\">' + cells + '</div>';",
                  "}",
                  collapse = "\n"
                )
              )
            )
          ),
          callback = JS(
            sprintf("window.collapsedGroups={%s};", paste(sprintf("'%s':true", groups), collapse = ", ")),
            "
            table.on('click', '.dt-group-header', function(){
              var name = $(this).data('name');
              if(name !== undefined){
                window.collapsedGroups[name] = !window.collapsedGroups[name];
                table.draw(false);
              }
            });
            "
          )
        )
      })
    }
    
    shinyApp(ui, server)
    

    giving your desired result. Here I added dotted borderlines for clarity

    aligned cells