Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Expanding and collapsing child rows in Shiny DataTable

I'm having trouble replicating a datatable object in Shiny. Currently, I can display what I want when I run the data table portion of the code outside of a Shiny environment. However, when I run the entire code, it's not displaying the child table.

library(DT)
library(data.table)
library(shiny)

shinyApp(
 ui = fluidPage(DT::dataTableOutput('tbl')),
 server = function(input, output) {
 output$tbl = DT::renderDataTable(

  datatable({
    #Transform dataframe to data.table and turn the dataframe rowname into a data.table column called model
    mtcars_dt = data.table(mtcars)
    mtcars_dt[["model"]] = rownames(mtcars)
    setcolorder(mtcars_dt,c(
      which(colnames(mtcars_dt) %in% c("mpg","cyl","model")),
      which(!colnames(mtcars_dt) %in% c("mpg","cyl","model"))
    ))

    #Turn data table into a nested data.table by mpg, cyl
    mtcars_dt <- mtcars_dt[, list(cars=list(.SD)), by = list(mpg,cyl)]


    #configure datatable. Hide row number and cars columns [0,4] and enable details control on plus sign column[1]
    #turn rows into child rows and remove from parent
    cbind(' ' = '&oplus;', mtcars_dt)}, 

    escape = -2,
    options = list(
      columnDefs = list(
        list(visible = FALSE, targets = c(0,4)),
        list(orderable = FALSE, className = 'details-control', targets = 1)
      )
    ),
    callback = JS("
                  table.column(1).nodes().to$().css({cursor: 'pointer'});

                  // Format cars object into another table
                  var format = function(d) {
                  if(d != null){ 
                  var result = ('<table id=\"child_' + d[2] + '_' + d[3] + '\">').replace('.','_') + '<thead><tr>'
                  for (var col in d[4]){
                  result += '<th>' + col + '</th>'
                  }
                  result += '</tr></thead></table>'
                  return result
                  }else{
                  return '';
                  }
                  }

                  var format_datatable = function(d) {
                  var dataset = [];
                  for (i = 0; i < + d[4]['model'].length; i++) {
                  var datarow = [];
                  for (var col in d[4]){
                  datarow.push(d[4][col][i])
                  }
                  dataset.push(datarow)
                  }
                  var subtable = $(('table#child_' + d[2] + '_' + d[3]).replace('.','_')).DataTable({
                  'data': dataset,
                  'autoWidth': true, 
                  'deferRender': true, 
                  'info': false, 
                  'lengthChange': false, 
                  'ordering': true, 
                  'paging': false, 
                  'scrollX': false, 
                  'scrollY': false, 
                  'searching': false 
                  });
                  };

                  table.on('click', 'td.details-control', function() {
                  var td = $(this), row = table.row(td.closest('tr'));
                  if (row.child.isShown()) {
                  row.child.hide();
                  td.html('&oplus;');
                  } else {
                  row.child(format(row.data())).show();
                  td.html('&CircleMinus;');
                  format_datatable(row.data())
                  }
                  });")
      )
    )
  }
)

Thanks for your help!

like image 499
Jeff Zhou Avatar asked Oct 30 '22 08:10

Jeff Zhou


1 Answers

The key here seemed to be the difference between an object and an array. When using shiny, row.data() is an array and its fifth element is an array as well (here I clicked on the second row in the main table):

["2", "&oplus;", 22.8, 4, Array(2)]

Outside of the shiny environment, row.data() looks like this:

["2", "&oplus;", 22.8, 4, Object]

As you can see, the fifth element is an object! Why this is the case, I cannot tell. I guess there might be a difference in the versions of the libraries used behind the scenes.

To get this working we need to make 2 changes:

1. Change format():


var format = function(d) {
  if(d != null) { 
    var result = ('<table id=\"child_' + d[2] + '_' + d[3] + '\">').replace('.','_') + '<thead><tr>'
    for (var col in d[4][0]) {
       result += '<th>' + col + '</th>'
    }
    result += '</tr></thead></table>'
    return result
  } else {
    return '';
  }
}

Here we just added [0] in line 4. As shown above, d[4] is an array. In case of the second row of data it consists of 2 objects. (var col in d[4]) would return 0 and 1 (indices of the objects) whereas (var col in d[4][0]) returns the elements of the first object (so the column names).

2. Change format_datatable():


var format_datatable = function(d) {
  var dataset = [];
  for (i = 0; i <=  d[4].length-1; i++) {
    var datarow = $.map(d[4][i], function(value, index) {
      return [value];
    });
    dataset.push(datarow);
  }
  //  ...
  //  the rest did not change
}

Here we convert each car model (so each element in d[4]), which is represented as an object, into an array using $.map(). After that we just add this array to dataset.

The code that creates the subtable expects these arrays. Data is handled differently by DataTables with respect to its type which can be checked here.

like image 72
Martin Schmelzer Avatar answered Nov 15 '22 05:11

Martin Schmelzer