Skip to content

Child Row & List Structure Error after updating to DT 0.4 #525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
amjadtalib opened this issue Apr 11, 2018 · 17 comments · Fixed by #530
Closed

Child Row & List Structure Error after updating to DT 0.4 #525

amjadtalib opened this issue Apr 11, 2018 · 17 comments · Fixed by #530
Milestone

Comments

@amjadtalib
Copy link

amjadtalib commented Apr 11, 2018

Hi, I'd like to echo a bug that's been surfaced in a closed issue and is causing problems only after updating to the latest versions (0.3 & 0.4) of DT.

The recent bug has been mentioned in the posts from this year on issue 9 from the rstudio/shiny-examples repo by @davlee1972 @Plum57. One of the solutions proposed by converting nested data.tables to JSON strings is not effective for large datasets.

The code I'm using is as follows, substituted with sample data:

# dt_url <- "http://cran.r-project.org/src/contrib/Archive/DT/DT_0.2.tar.gz"
# install.packages(dt_url, repos=NULL, type="source")

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

ui <- fluidPage(
  tags$head(
    tags$style(HTML("@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');"))
  ),
  fluidRow(DT::dataTableOutput(width = "100%", "table"))
)

server <- function(input, output, session) {

  # ---- Sample Set ----
  rn <- 1000
  DT <- data.table(
    ID = seq(1, rn, by=1),
    Date = sample(seq(as.Date('2018/01/01'), as.Date('2018/04/01'), by="day"), rn, replace = TRUE), 
    Customer = sample(LETTERS[1:5], rn, replace = TRUE, prob = runif(5)),
    Location = c(sample(LETTERS[10:26], floor(runif(rn, min=0, max=101/2)), replace = TRUE), sample(LETTERS[10:26], floor(runif(rn, min=0, max=101/2)), replace = TRUE)),
    Price = round(runif(rn, min=50, max=100), 2),
    Cost = round(runif(rn, min=20, max=80), 2),
    VIP = sample(c("VIP", "REG"), rn, replace = TRUE)
  )
  # ---- Creating Parent Table w/ Children ----
  loc_ref <- unique(DT[,.(Location)])
  children <- DT[, .(Details=.(.SD)), by = Location]
  
  parent <- DT[ 
    , .(
      "Dates" = paste0(min(format(Date, "%D")), " - ", max(format(Date, "%D")))
      , "Customers" = uniqueN(Customer)
      , "Total Volume" = .N
      , "Total Margin" = sum(Price - Cost, na.rm = TRUE)
      , "Average Margin" = round(((mean(Price, na.rm = TRUE) - mean(Cost, na.rm = TRUE))/mean(Price, na.rm = TRUE))*100, 2)
      , "Average Cost" = mean(Cost, na.rm = TRUE)
      , "Average Price" = mean(Price, na.rm = TRUE)
      , "VIP" = paste0(round((sum(grepl("VIP", VIP, fixed = TRUE, useBytes = TRUE)) / .N)*100, 1),"%")
    )
    , by = Location]
  
  volume <- DT[, .(Orders = .N), by = .(Location, Week = floor_date(Date, "week"))]
  volume <- dcast.data.table(volume, Location ~ Week, fun.aggregate = cumsum, fill = "0", value.var = "Orders")
  if (ncol(volume) > 4) {
    volume <- volume[, .("WoW Volume" = .(paste0(.SD))), .SDcols=c(c(ncol(volume)-3):c(ncol(volume))), by = Location]
    volume$`WoW Volume` <- lapply(volume[,`WoW Volume`], as.numeric)
  } else {
    volume <- volume[, .("WoW Volume" = .(paste0(.SD))), .SDcols=c(2:c(ncol(volume))), by = Location]  
    volume$`WoW Volume` <- lapply(volume[,`WoW Volume`], as.numeric)
  }
  
  parent <- parent[volume, on = "Location"]
  setcolorder(parent, c(1:4,10,5:9))
  
  loc_ref <- loc_ref[parent, on = "Location"]
  loc_ref <- loc_ref[children, on = "Location"]
  loc_ref[, ' ' := ' ']
  setcolorder(loc_ref, c(length(loc_ref),c(1:(length(loc_ref) - 1))))
  loc_ref <- loc_ref[order(-`Total Volume`)]
  
  output$table <- DT::renderDataTable({
    DT::datatable(
      data = loc_ref,
      rownames = FALSE,
      escape = FALSE,
      extensions = c('Scroller'),
      options = list(
        server = TRUE,
        searching = FALSE,
        order = list(list(4 , 'desc')),
        dom = 'tfrilp',
        autoWidth = TRUE,
        stripeClasses = list(),
        deferRender = TRUE,
        scrollX = TRUE,
        lengthChange = FALSE,
        scrollY = "51vh",
        scroller = TRUE,
        scollCollapse = TRUE,
        columnDefs = list(
          list(visible = FALSE, targets = -1 ),
          list(orderable = FALSE, className = 'details-control',  targets = 0, render = JS("function(data, type, full){ return '<i class=\"fa fa-plus\" aria-hidden=\"true\">' + data + '</i>' }") ),
          list(targets = 5, render = JS("function(data, type, full){ return '<span class=spark>' + data + '</span>' }")),
          list(targets = 7, render = JS("function(data, type, full){ return '<span class=sparkSamples>' + data + '</span>' }"))          
        )
      ),
      callback = JS("
                    table.column(1).nodes().to$().css({cursor: 'pointer'})
                    
                    // Generate the child table layout
                    var generateChildTableLayout = function(parentRowData) {
                    if (parentRowData != null){
                    var result = ('<table id=\"' + parentRowData[1] + '\"><thead><tr>')
                    for (var col in parentRowData[parentRowData.length - 1][0]){
                    result += '<th>' + col + '</th>'
                    }
                    result += '</tr></thead></table>'
                    return result
                    } else {
                    return ''
                    }
                    }
                    
                    // Generate the child table content
                    var generateChildTableContent = function(parentRowData) {

                    var childRowData = parentRowData[parentRowData.length - 1].map(Object.values);

                    var subtable = $('table#' + parentRowData[1]).DataTable({
                    'data': childRowData,
                    'autoWidth': false,
                    'deferRender': true,
                    'class': 'stripe',
                    'info': false,
                    'lengthChange': false,
                    'ordering': false,
                    'paging': false,
                    'scrollX': false,
                    'scrollY': false,
                    'searching': false
                    }).draw()
                    }
                    
                    table.on('click', 'td.details-control', function() {
                    var table = $(this).closest('table')
                    var tr = $(this).closest('tr');
                    var td = $(this)
                    var tdi = tr.find('i.fa').first();
                    var row = $(table).DataTable().row(td.closest('tr'))
                    if (row.child.isShown()) {
                    row.child.hide()
                    tdi.removeClass('fa-minus');
                    tdi.addClass('fa-plus');
                    tdi.innerText = ' ';
                    } else {
                    var parentRowData = row.data();
                    row.child(generateChildTableLayout(parentRowData)).show()
                    generateChildTableContent(parentRowData)
                    tdi.removeClass('fa-plus');
                    tdi.addClass('fa-minus');
                    tdi.innerText = ' ';
                    }
                    })
                    ")      ) %>% formatCurrency(c(7, 8, 9, 10), currency = "$")
  })

}  

shinyApp(ui = ui, server = server)

With DT version 0.2, the app renders fine and the rows work as needed:

Parent Table:
parent_table
Child Table:
child_table

After updating to version 0.4, the following error appears:

DataTables warning: table id=DataTables_Table_0 - Error in [<-.data.frame(*tmp*, , j, value = list(structure(list(ID = c(3, : replacement element 1 is a matrix/data frame of 121 rows, need 16
data.table structure:
v0_4_error
list structure:
v0_4_error_list

The list replacement values vary by what is actually in the tables. I've noticed that this issue is for lists as well as data.tables in a given row. This image shows that the error appears for the "WoW Volume" Column that is a list of integer values and is not nested as a child row. The reason for the list structure is to use the sparkline package.

@yihui @jcheng5 Thank you for your work in making this package possible. I hope this post contains all the relevant information required to diagnose the issue. Please let me know if there's something else I can provide.

@shrektan
Copy link
Collaborator

it will be better if you can minimize the example code...

@amjadtalib
Copy link
Author

@shrektan sorry, I tried to keep it minimal and reproducible, but here's another example that's been used before in the linked issue. It uses mtcars, but I think the code is a bit long by nature of what's done to create the nested table.

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

ui <- fluidPage(fluidRow(DT::dataTableOutput(width = "100%", "table")))

server <- function(input, output) {
  
  output$table = DT::renderDataTable({
    
    mtcars_dt = data.table(mtcars)
    setkey(mtcars_dt,mpg,cyl)
    mpg_dt = unique(mtcars_dt[, list(mpg, cyl)])
    setkey(mpg_dt, mpg, cyl)
    cyl_dt = unique(mtcars_dt[, list(cyl)])
    setkey(cyl_dt, cyl)
    
    mtcars_dt = mtcars_dt[,list(mtcars=list(.SD)), by = list(mpg,cyl)]
    mtcars_dt[, ' ' := '&#9658;']
    
    mpg_dt = merge(mpg_dt,mtcars_dt, all.x = TRUE )
    setkey(mpg_dt, cyl)
    setcolorder(mpg_dt, c(length(mpg_dt),c(1:(length(mpg_dt) - 1))))
    
    mpg_dt = mpg_dt[,list(mpg=list(.SD)), by = cyl]
    mpg_dt[, ' ' := '&#9658;']
    
    cyl_dt = merge(cyl_dt,mpg_dt, all.x = TRUE )
    setcolorder(cyl_dt, c(length(cyl_dt),c(1:(length(cyl_dt) - 1))))
    
    DT::datatable(
      data = cyl_dt,
      rownames = FALSE,
      escape = -1,
      extensions = c('Scroller'),
      options = list(
        dom = 'Bfrti',
        autoWidth = TRUE,
        stripeClasses = list(),
        deferRender = TRUE,
        scrollX = TRUE,
        scrollY = "51vh",
        scroller = TRUE,
        scollCollapse = TRUE,
        columnDefs = list(
          list(orderable = FALSE, className = 'details-control', targets = 0),
          list(visible = FALSE, targets = -1 )
        )
      ),
      callback = JS("
                    table.column(1).nodes().to$().css({cursor: 'pointer'})
                    
                    // Format child object into another table
                    var format = function(d) {
                    if(d != null){ 
                    var result = ('<table id=\"' + d[1] + '\"><thead><tr>').replace('.','_')
                    for (var col in d[d.length - 1][0]){
                    result += '<th>' + col + '</th>'
                    }
                    result += '</tr></thead></table>'
                    return result
                    }else{
                    return ''
                    }
                    }
                    
                    var format_datatable = function(d) {
                    if(d != null){
                    if ('SOME CHECK' == 'LAST SET OF CHILD TABLES') {
                    var subtable = $(('table#' + d[1]).replace('.','_')).DataTable({
                    'data': d[d.length - 1].map(Object.values),
                    'autoWidth': false, 
                    'deferRender': true, 
                    'stripeClasses': [],
                    'info': false, 
                    'lengthChange': false, 
                    'ordering': false, 
                    'paging': false, 
                    'scrollX': false, 
                    'scrollY': false, 
                    'searching': false 
                    }).draw()
                    }else{
                    var subtable = $(('table#' + d[1]).replace('.','_')).DataTable({
                    'data': d[d.length - 1].map(Object.values),
                    'autoWidth': false, 
                    'deferRender': true,
                    'stripeClasses': [],
                    'info': false, 
                    'lengthChange': false, 
                    'ordering': false, 
                    'paging': false, 
                    'scrollX': false, 
                    'scrollY': false, 
                    'searching': false,
                    'columnDefs': [{'orderable': false, 'className': 'details-control', 'targets': 0},
                    {'visible': false, 'targets': -1}]
                    }).draw()
                    }
                    }
                    }
                    
                    table.on('click', 'td.details-control', function() {
                    var table = $(this).closest('table')
                    var td = $(this)
                    var row = $(table).DataTable().row(td.closest('tr'))
                    if (row.child.isShown()) {
                    row.child.hide()
                    td.html('&#9658;')
                    } else {
                    row.child(format(row.data())).show()
                    format_datatable(row.data())
                    td.html('&#9660;')
                    }
                    })
                    
                    ")
      )
  })
  }

shinyApp (ui = ui, server = server)

The recent reproduction issue was raised by @Plum57 on Jan 31st.

@shrektan
Copy link
Collaborator

Thanks for the following up.

  • I bet that you need to set server = FALSE, because I'm afraid the current server-side processing mode may not support this.
  • It will not throw the warning anymore by changing to server = FALSE but the sub-table still can't be displayed, which I will investigate...

@shrektan
Copy link
Collaborator

shrektan commented Apr 14, 2018

@amjadtalib

I've created a worked example. What's wrong with your code is, probably due to the upgrading of the underlying datatable javascript library, in my opinion:

  1. The data returned by the row.data() needs to be converted to an array of objects.
  2. The columns need to be set explicitly as well.
  3. Set server = FALSE in renderDataTable() because it's not a regular data.frame and the current implementation of server-side process mode may not support this.

Check https://datatables.net/manual/data/ for more info.

Moreover, it's an illustration of what minimal really means. It's not about the absolute of the code length but about removing all the unnecessary code.

For instance, you don't need to provide such complex data. The style of the table can be more concise as well. So that we can save the time to ensure the issue is not caused by those codes.

Minimal Example

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

dat <- local({
  n <- 6
  DT <- data.table(
    HEADER = '&#9658;',
    A = rep_len(LETTERS, n),
    B = rep_len(1:10, n),
    C = rep_len(c("GRP1", "GRP2"), n)
  )
  DT[, .(D=.(.SD)), by = .(HEADER, C)]
})


callback_js <-
  JS("
     var init_tbl = function(id) {
       return '<table id=\"' + id + '\" class=\"display\" width=\"100%\"></table>';
     }
     
     // you need this converter ==========================
     var converter = function(x) {
       var res = [];
       for (var i = 0; i < x[Object.keys(x)[0]].length; ++i) res.push({});
       for (var key in x) {
         var elem = x[key];
          for (var i = 0; i < elem.length; ++ i) {
            res[i][key] = elem[i];
          }
       }
       return res;
     }    

     var init_datatable = function(id, d) {
       d = converter(d);
       id = '#'+id;
       $(id).DataTable({
         data: d,
         // you need set the columns ==========================
         columns: [
           { data: 'A' },
           { data: 'B' }
         ]  
       }).draw();
     }
     var sub_tbl_id = 0;
     table.on('click', 'td.details-control', function() {
       var table = $(this).closest('table');
       var td = $(this);
       var row = $(table).DataTable().row(td.closest('tr'));
       if (row.child.isShown()) {
         row.child.hide();
         td.html('&#9658;');
      } else {
         sub_tbl_id = sub_tbl_id + 1;
         var id = 'sub_tbl_id_' + sub_tbl_id;
         row.child(init_tbl(id)).show();
         init_datatable(id, row.data()[3]);
         td.html('&#9660;');
      }
     })
")

shinyApp(
  ui = fluidPage(DT::DTOutput("tbl")),
  server = function(input, output) {
    output$tbl <- DT::renderDT({
      datatable(
        dat,
        escape = FALSE,
        options = list(
          columnDefs = list(
            list(orderable = FALSE, className = 'details-control', targets = 1),
            list(visible = FALSE, targets = -1)
          )
        ),
        callback = callback_js
      )
    }, server = FALSE) # you need set the server to FALSE
  },
  options = list(port = 33333)
)

image

You original example (Modified. Work now.)

# dt_url <- "http://cran.r-project.org/src/contrib/Archive/DT/DT_0.2.tar.gz"
# install.packages(dt_url, repos=NULL, type="source")

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

ui <- fluidPage(
  tags$head(
    tags$style(HTML("@import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');"))
  ),
  fluidRow(DT::dataTableOutput(width = "100%", "table"))
)

dat <- local({
  # ---- Sample Set ----
  rn <- 1000
  DT <- data.table(
    ID = seq(1, rn, by=1),
    Date = sample(seq(as.Date('2018/01/01'), as.Date('2018/04/01'), by="day"), rn, replace = TRUE), 
    Customer = sample(LETTERS[1:5], rn, replace = TRUE, prob = runif(5)),
    Location = c(sample(LETTERS[10:26], floor(runif(rn, min=0, max=101/2)), replace = TRUE), sample(LETTERS[10:26], floor(runif(rn, min=0, max=101/2)), replace = TRUE)),
    Price = round(runif(rn, min=50, max=100), 2),
    Cost = round(runif(rn, min=20, max=80), 2),
    VIP = sample(c("VIP", "REG"), rn, replace = TRUE)
  )
  # ---- Creating Parent Table w/ Children ----
  loc_ref <- unique(DT[,.(Location)])
  children <- DT[, .(Details=.(.SD)), by = Location]
  
  parent <- DT[ 
    , .(
      "Dates" = paste0(min(format(Date, "%D")), " - ", max(format(Date, "%D")))
      , "Customers" = uniqueN(Customer)
      , "Total Volume" = .N
      , "Total Margin" = sum(Price - Cost, na.rm = TRUE)
      , "Average Margin" = round(((mean(Price, na.rm = TRUE) - mean(Cost, na.rm = TRUE))/mean(Price, na.rm = TRUE))*100, 2)
      , "Average Cost" = mean(Cost, na.rm = TRUE)
      , "Average Price" = mean(Price, na.rm = TRUE)
      , "VIP" = paste0(round((sum(grepl("VIP", VIP, fixed = TRUE, useBytes = TRUE)) / .N)*100, 1),"%")
    )
    , by = Location]
  
  volume <- DT[, .(Orders = .N), by = .(Location, Week = floor_date(Date, "week"))]
  volume <- dcast.data.table(volume, Location ~ Week, fun.aggregate = cumsum, fill = "0", value.var = "Orders")
  if (ncol(volume) > 4) {
    volume <- volume[, .("WoW Volume" = .(paste0(.SD))), .SDcols=c(c(ncol(volume)-3):c(ncol(volume))), by = Location]
    volume$`WoW Volume` <- lapply(volume[,`WoW Volume`], as.numeric)
  } else {
    volume <- volume[, .("WoW Volume" = .(paste0(.SD))), .SDcols=c(2:c(ncol(volume))), by = Location]  
    volume$`WoW Volume` <- lapply(volume[,`WoW Volume`], as.numeric)
  }
  
  parent <- parent[volume, on = "Location"]
  setcolorder(parent, c(1:4,10,5:9))
  
  loc_ref <- loc_ref[parent, on = "Location"]
  loc_ref <- loc_ref[children, on = "Location"]
  loc_ref[, ' ' := ' ']
  setcolorder(loc_ref, c(length(loc_ref),c(1:(length(loc_ref) - 1))))
  loc_ref <- loc_ref[order(-`Total Volume`)]
  
})

server <- function(input, output, session) {
  
 
  output$table <- DT::renderDataTable({
    DT::datatable(
      data = dat,
      rownames = FALSE,
      escape = FALSE,
      extensions = c('Scroller'),
      options = list(
        server = TRUE,
        searching = FALSE,
        order = list(list(4 , 'desc')),
        dom = 'tfrilp',
        autoWidth = TRUE,
        stripeClasses = list(),
        deferRender = TRUE,
        scrollX = TRUE,
        lengthChange = FALSE,
        scrollY = "51vh",
        scroller = TRUE,
        scollCollapse = TRUE,
        columnDefs = list(
          list(visible = FALSE, targets = -1 ),
          list(orderable = FALSE, className = 'details-control',  targets = 0, render = JS("function(data, type, full){ return '<i class=\"fa fa-plus\" aria-hidden=\"true\">' + data + '</i>' }") ),
          list(targets = 5, render = JS("function(data, type, full){ return '<span class=spark>' + data + '</span>' }")),
          list(targets = 7, render = JS("function(data, type, full){ return '<span class=sparkSamples>' + data + '</span>' }"))          
        )
      ),
      callback = JS("
table.column(1).nodes().to$().css({cursor: 'pointer'})

// Generate the child table layout
var generateChildTableLayout = function(parentRowData) {
  if (parentRowData != null){
    var result = ('<table id=\"' + parentRowData[1] + '\"><thead><tr>')
    for (var col in parentRowData[parentRowData.length - 1][0]){
    result += '<th>' + col + '</th>'
  }
    result += '</tr></thead></table>'
    return result
  } else {
    return ''
  }
}

var converter = function(x) {
  var res = [];
  for (var i = 0; i < x[Object.keys(x)[0]].length; ++i) res.push({});
  for (var key in x) {
    var elem = x[key];
    for (var i = 0; i < elem.length; ++ i) {
      res[i][key] = elem[i];
    }
  }
  return res;
}  

var cols = function(x) {
  var res = [];
  for (var key in x) {
    res.push({'data': key});
  }
  return res;
}

// Generate the child table content
var generateChildTableContent = function(parentRowData) {
var childRowData = converter(parentRowData[parentRowData.length - 1]);
var childColumns = cols(parentRowData[parentRowData.length - 1]);

var subtable = $('table#' + parentRowData[1]).DataTable({
  'data': childRowData,
  'columns': childColumns,
  'autoWidth': false,
  'deferRender': true,
  'class': 'stripe',
  'info': false,
  'lengthChange': false,
  'ordering': false,
  'paging': false,
  'scrollX': false,
  'scrollY': false,
  'searching': false
  }).draw()
}

table.on('click', 'td.details-control', function() {
  var table = $(this).closest('table')
  var tr = $(this).closest('tr');
  var td = $(this)
  var tdi = tr.find('i.fa').first();
  var row = $(table).DataTable().row(td.closest('tr'))
  if (row.child.isShown()) {
    row.child.hide()
    tdi.removeClass('fa-minus');
    tdi.addClass('fa-plus');
    tdi.innerText = ' ';
  } else {
    var parentRowData = row.data();
    row.child(generateChildTableLayout(parentRowData)).show()
    generateChildTableContent(parentRowData)
    tdi.removeClass('fa-plus');
    tdi.addClass('fa-minus');
    tdi.innerText = ' ';
}
})
                    ")      ) %>% formatCurrency(c(7, 8, 9, 10), currency = "$")
  }, server = FALSE)
  
  }  

shinyApp(ui = ui, server = server)

@amjadtalib
Copy link
Author

amjadtalib commented Apr 17, 2018

@shrektan Thank you for this explanation and example. I see what you mean in your example, but wanted to provide a dynamic example that could better represent the real-world use case. Of course, I see the merit in minimalism, too.

Now concerning the response, I applied the converter, and the solution worked, but requiring client-side processing is a serious limitation. The dataset I'm working with has over 120k rows and I'm afraid it will struggle without using server processing.

Since the server-side processing used to be an option before 0.4, do you believe that this may return as a possibility soon? I'm not sure if it's better to stay in 0.2 with server processing or upgrade to 0.4 with client processing.

EDIT: Upon profiling, I see that the difference is too large to use client-side processing. My datatable render function increased from 1440ms to 11240ms.

@shrektan
Copy link
Collaborator

@amjadtalib Understood. I'll take a deeper look later to see if it's easy to make the server-side mode processing possible for your case.

@shrektan
Copy link
Collaborator

@amjadtalib I've filed a PR #530 which should fix this issue.

However, in the server-side processing mode (after this PR gets merged), your original example code could be able to work without any modifications (you don't need that converter() anymore) .

It seems like the JSON objects sent to the browser are different for the client mode and the server-side processing mode, regarding this kind of complex data (with sub data.frame in the column). It means that you need to different callback codes for different modes.

I didn't look into the root cause but if there's a real-world use case for this, please let me know.

Thanks.

@yihui yihui added this to the v0.5 milestone Apr 17, 2018
@amjadtalib
Copy link
Author

@shrektan this is awesome, thank you for taking care of this so promptly! I installed your branch and it works great.

I guess it's good to know about the converter requirements if using client-side processing. It may be worth updating this page to reflect that information: https://rstudio.github.io/DT/002-rowdetails.html

I don't see any need for client-side on my end. Thank you again for the great work. It looks like @yihui merged this with the master. Does this make the recent code immediately available if downloading from github? Also, is there an expected timeline for 0.5 for be available through CRAN?

@shrektan
Copy link
Collaborator

shrektan commented Apr 17, 2018

You're welcome.

Yes, it means you are able to install the latest version of DT via devtools::install_github('rstudio/DT'), immediately.

About the expected timeline on CRAN, @yihui can answer this.

@yihui
Copy link
Member

yihui commented Apr 17, 2018

V0.5 can be released to CRAN any time if you have difficulties with installing from Github.

@piyuw
Copy link

piyuw commented Jul 18, 2018

@yihui @shrektan i used a piece of code that davlee1972 had written in a previous post for creating child rows. this worked example was done on the mtcars file within R. I have no background in javascript and therefore reversed engineered the piece of code to try and fit it to my data.

however, ive been struggling with it as the parent/child layers present themselves in chrome, however the data is absent.

the piece of code im using is the following -

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

ui <- fluidPage(fluidRow(DT::dataTableOutput(width = "100%", "table")))

server <- function(input, output) {

output$table = DT::renderDataTable({

DT::datatable(
  data = df,
  rownames = FALSE,
  escape = -1,
  extensions = c('Scroller'),
  options = list(
    dom = 'Bfrti',
    autoWidth = TRUE,
    stripeClasses = list(),
    deferRender = TRUE,
    scrollX = TRUE,
    scrollY = "51vh",
    scroller = TRUE,
    scollCollapse = TRUE,
    columnDefs = list(
      list(orderable = FALSE, className = 'details-control', targets = 0),
      list(visible = FALSE, targets = -1 )
    )
  ),
  callback = JS("
                table.column(1).nodes().to$().css({cursor: 'pointer'})
                
                // Format child object into another table
                var format = function(d) {
                if(d != null){ 
                var result = ('<table id=\"' + d[1] + '\"><thead><tr>').replace('.','_')
                for (var col in d[d.length - 1][0]){
                result += '<th>' + col + '</th>'
                }
                result += '</tr></thead></table>'
                return result
                }else{
                return ''
                }
                }
                
                var format_datatable = function(d) {
                if(d != null){
                if ('SOME CHECK' == 'LAST SET OF CHILD TABLES') {
                var subtable = $(('table#' + d[1]).replace('.','_')).DataTable({
                'data': d[d.length - 1].map(Object.values),
                'autoWidth': true, 
                'deferRender': true, 
                'stripeClasses': [],
                'info': false, 
                'lengthChange': false, 
                'ordering': false, 
                'paging': false, 
                'scrollX': false, 
                'scrollY': false, 
                'searching': false 
                }).draw()
                }else{
                var subtable = $(('table#' + d[1]).replace('.','_')).DataTable({
                'data': d[d.length - 1].map(Object.values),
                'autoWidth': true, 
                'deferRender': true,
                'stripeClasses': [],
                'info': false, 
                'lengthChange': false, 
                'ordering': false, 
                'paging': false, 
                'scrollX': false, 
                'scrollY': false, 
                'searching': false,
                'columnDefs': [{'orderable': false, 'className': 'details-control', 'targets': 0},
                {'visible': false, 'targets': -1}]
                }).draw()
                }
                }
                }
                
                table.on('click', 'td.details-control', function() {
                var table = $(this).closest('table')
                var td = $(this)
                var row = $(table).DataTable().row(td.closest('tr'))
                if (row.child.isShown()) {
                row.child.hide()
                td.html('&#9658;')
                } else {
                row.child(format(row.data())).show()
                format_datatable(row.data())
                td.html('&#9660;')
                }
                })
                
                ")
  )

})
}

shinyApp (ui = ui, server = server)

however, the output looks something like the below -
image

there should be groups and the corresponding amount next to it in the "AccountReffulname" and "Fullamount" columns, however doesnt show up.

The same piece of code, if the mtcars file is used produces the desired output as per the following post -> rstudio/shiny-examples#9

i even tried renaming the fields of my dataset to match the code devlee1972 wrote so that the child/parent relationships would be the same, however finally concluded that it is not this but something else.

it would be much appreciated if you guys could shed some light as to why this may be the case.

Thanks
piyuw

@shrektan
Copy link
Collaborator

@piyuw I received your email.

First of all, please make sure that you're using the dev version of DT (otherwise, install the dev version by devtools::install_github('rstudio/DT') and try again).

If the dev version still fails, please take some time and provide a minimal reproducible example for us.

Thanks.

@piyuw
Copy link

piyuw commented Jul 21, 2018

@shrektan

Hi Shrektan!

thank you so much for getting back to me, you are a life saver!

I've already gone through all the previous posts that discussed the issues of the current CRAN DT package and what has been update within github. therefore, i've already carried out the first step you recommended, however, unfortunately this has been unsuccessful.

I've create a sample set of data and tried to best replicate the current data structure. As such the code :

`
#Create sample data
df <- data.frame(

      Summary_date = c(rep("2017-07-31",3),rep("2017-08-30",3),rep("2017-09-30",3))
      ,AccountRefFullName = c( "CERELAC MIXED FRUIT"
                          ,"CERELAC RED RICE"
                          ,"CERELAC VEGE & RICE"
                          ,"CHICKEN STOCK 24*500G"
                          ,"COFFEE-MATE 2(500*2"
                          ,"ELLO CHOCO MOUSSE 12*600"
                          ,"LACTOGEN 1 20*240G"
                          ,"LACTOGEN 2 20*350G"
                          ,"LACTOGEN 3 20*350G")
      
      ,fullamount = c(-2596.7
                     ,-2670.66
                     ,-4451.1
                     ,-315930.42
                     ,-55652.88
                     ,-0.0399999999990541
                     ,-8816
                     ,-10844.6
                     ,-16617.74
                                )

      ,V1 = c(
        "Total Income"
        ,"Total Income"
        ,"Total Income"
        ,"Total Expenses"
        ,"Total Expenses"
        ,"Total Expenses"
        ,"COGS"
        ,"COGS"
        ,"COGS"  )
      
      ,V2 = c(rep(1000000,3),rep(3450000,3),rep(2342232342,3))
      ,V0 = c(rep(1,3),rep(2,3),rep(3,3))
                    )

#Create data frame that acts as the foundation for the Javascript Callback function.
df$AccountRefFullName <- as.character(df$AccountRefFullName)
tempnew <- data.table(df)
child_2lvl <- unique(tempnew[,list(Summary_date,V0,V1,V2,AccountRefFullName,fullamount)])
child_1lvl <- unique(tempnew[,list(Summary_date,V0,V1,V2)])
child_SmryDate <- unique(tempnew[,list(Summary_date)])

child_2lvl = data.table(child_2lvl[,-2])
child_2lvl <- child_2lvl[,list(Details=list(.SD)), by = .(Summary_date, V1,V2)]
child_2lvl[, ' ' := '►']

child_1lvl = merge(child_1lvl,child_2lvl,all.x = T, by = c("Summary_date", "V1","V2"))
setcolorder(child_1lvl, c(length(child_1lvl),c(1:(length(child_1lvl) - 1))))
orderCols <- c("Summary_date","V0")
child_1lvl = arrange(child_1lvl[ ,] , eval(parse(text = orderCols)))
orderCols <- c("Summary_date")
child_1lvl = arrange(child_1lvl[ ,] , eval(parse(text = orderCols)))
child_1lvl = child_1lvl[,!names(child_1lvl) %in% c("V0")]
child_1lvl = data.table(child_1lvl)
child_1lvl = child_1lvl[,list(Details1=list(.SD)), by = .(Summary_date)]
child_1lvl[, ' ' := '►']
setcolorder(child_1lvl, c(length(child_1lvl),c(1:(length(child_1lvl) - 1))))

Run App

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

ui <- fluidPage(fluidRow(DT::dataTableOutput(width = "100%", "table")))

server <- function(input, output) {

output$table = DT::renderDataTable({

# mtcars_dt = data.table(mtcars)
# setkey(mtcars_dt,mpg,cyl)
# mpg_dt = unique(mtcars_dt[, list(mpg, cyl)])
# setkey(mpg_dt, mpg, cyl)hh
# cyl_dt = unique(mtcars_dt[, list(cyl)])
# setkey(cyl_dt, cyl)
# 
# mtcars_dt = mtcars_dt[,list(mtcars=list(.SD)), by = list(mpg,cyl)]
# mtcars_dt[, ' ' := '&#9658;']
# 
# mpg_dt = merge(mpg_dt,mtcars_dt, all.x = TRUE )
# setkey(mpg_dt, cyl)
# setcolorder(mpg_dt, c(length(mpg_dt),c(1:(length(mpg_dt) - 1))))
# 
# mpg_dt = mpg_dt[,list(mpg=list(.SD)), by = cyl]
# mpg_dt[, ' ' := '&#9658;']
# 
# cyl_dt = merge(cyl_dt,mpg_dt, all.x = TRUE )
# setcolorder(cyl_dt, c(length(cyl_dt),c(1:(length(cyl_dt) - 1))))

DT::datatable(
  data = child_1lvl,
  rownames = FALSE,
  escape = -1,
  extensions = c( 'Scroller'),
  options = list(
    dom = 'Bfrti',
    autoWidth = TRUE,
    stripeClasses = list(),
    deferRender = TRUE,
    scrollX = TRUE,
    scrollY = "51vh",
    scroller = TRUE,
    scollCollapse = TRUE,
    columnDefs = list(
      list(orderable = FALSE, className = 'details-control', targets = 0),
      list(visible = FALSE, targets = -1 )
    )
  ),
  callback = JS("
                table.column(1).nodes().to$().css({cursor: 'pointer'})
                
                
                
                // Format child object into another table
                var format = function(d) {
                if(d != null){ 
                var result = ('<table id=\"' + d[1] + '\"><thead><tr>').replace('.','_')
                for (var col in d[d.length - 1][0]){
                result += '<th>' + col + '</th>'
                }
                result += '</tr></thead></table>'
                return result
                }else{
                return ''
                }
                }
                
                var format_datatable = function(d) {
                if(d != null){
                if ('SOME CHECK' == 'LAST SET OF CHILD TABLES') {
                var subtable = $(('table#' + d[1]).replace('.','_')).DataTable({
                'data': d[d.length - 1].map(Object.values),
                'autoWidth': true, 
                'deferRender': true, 
                'stripeClasses': [],
                'info': false, 
                'lengthChange': false, 
                'ordering': false, 
                'paging': false, 
                'scrollX': false, 
                'scrollY': false, 
                'searching': false 
                }).draw()
                }else{
                var subtable = $(('table#' + d[1]).replace('.','_')).DataTable({
                'data': d[d.length - 1].map(Object.values),
                'autoWidth': true, 
                'deferRender': true,
                'stripeClasses': [],
                'info': false, 
                'lengthChange': false, 
                'ordering': false, 
                'paging': false, 
                'scrollX': false, 
                'scrollY': false, 
                'searching': false,
                'columnDefs': [{'orderable': false, 'className': 'details-control', 'targets': 0},
                {'visible': false, 'targets': -1}]
                }).draw()
                }
                }
                }
                
                
                
                //var sub_tbl_id = 0;
                table.on('click', 'td.details-control', function() {
                var table = $(this).closest('table')
                var td = $(this)
                var row = $(table).DataTable().row(td.closest('tr'))
                if (row.child.isShown()) {
                row.child.hide()
                td.html('&#9658;')
                } else {
                row.child(format(row.data())).show()
                format_datatable(row.data())
                td.html('&#9660;')
                }
                })
                
                
                
                
                ")
  )

},server = TRUE)
}

shinyApp (ui = ui, server = server)

`

what youll notice is the following output :

image

In this output, you'll notice that whilst the parent child relationship works for the "Summary Date" and "V1", and "V2" columns, it fails for the "AccountRefFullName" and "Full Amount" Columns where the expectation is that more data should exist as a parent/child relationship.

my understanding is that (happy to be corrected) : that the current java script is equipped to automatically find the parent child relationship in the way the data is structured and then print it on to web page. if this is not the case, please let me know where in the java script i need to specify the different parent/child relationship - I dont have much knowledge of Java unfortunately :(

The commented out code of the MTCars (default with R) file is what came with the orignal script, however, works for this but not for my data. My data has the same level of parent/child relationships as per the original MTcars data had. The only different is the data in my view (happy to be corrected) :).

your help would be much appreciated! if i havent been clear enough, please let me know

thanks in advance
Piyuw

@shrektan
Copy link
Collaborator

@piyuw My eyes really hurt 🤣

Use three backticks (i.e., ```) will enable you to include a multiple lines code example. Moreover, would you please preview the comment before submitting in the next time? Otherwise we have to do clean the code by ourselves in order to see what's really wrong.

You example shows that you are trying to implement a double nested datatable feature and the second datatable can't be displayed properly, right?

The thing is that you only claimed to build one nested datatable in your example's JS code. I will try to give you a workable double nested example later when I have time...

@shrektan
Copy link
Collaborator

@piyuw Sorry, I tried but failed... I don't think I can figure it out in an hour so I choose to give up. However, if you insist, you may take the chance on StackOverflow...

@piyuw
Copy link

piyuw commented Jul 27, 2018

@shrektan...

Hi, apologies for the code submission! I followed the ' ' fornat but it clearly hasn't worked 🤔😓.

Yes it is a double nested table, but moreover I want to understand where on the JS do I need to change for it to automatically identify the number of nested layers? I can do the trial and error part, I just need some sample JS code that picks up how many layers of nests exists from the last column in the data table.

I tried Stackoverflow but so far haven't had any luck.

@yihui any chance you might be able to assist in how to create the JS with some sample data for having a double or 'n' layers of nested table?

Please let me know your thoughts.

Thanks in advance!
piyuw

@piyuw
Copy link

piyuw commented Nov 10, 2018

@shrektan
Hi shrektan,

Been a while since my last post, i was wondering if you figured out how to do a double nested datatable feature or a # nested datatable feature with the code i provided you in july ? or why the prescribed code wasnt resulting in the expected result.

i was wondering if you had a chance to speak to @yihui as well regarding the matter ?

your thoughts would be much appreciated!

cheers
piyuw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants