Replacing initial Dataframe from an editable DataTable and using that new data frame in another table

你。 提交于 2021-01-29 12:32:15

问题


I have a nested DataTable in my Shiny app. Some of the columns in the child table are editable by the user. The goal here is to have the user edit the values and then the data frame would be replaced with those new values. Then I would like to take that replaced data frame and use it for another table. I need to use the edited values for calculations in the other table.

workflow:

create nested data table -> user edits values -> initial data is replaced by new data -> use the new data for another table

I've tried to use reactiveValues(), dataTableProxy(), replaceData() and observeEvert() to follow that workflow but have had no luck.

# Parent table
structure(list(Market = c("ABILENE-SWEETWATER", "ALBANY-SCHENECTADY-TROY, NY"
), `Gross CPP` = c("$1.94", "$7.89"), `Gross CPM` = c("$1.02", 
"$0.82"), `Historical Composite Gross CPP (if applicable)` = c("$0", 
"$0"), `Historical Composite Gross CPM (if applicable)` = c("$0", 
"$0")), .Names = c("Market", "Gross CPP", "Gross CPM", "Historical Composite Gross CPP (if applicable)", 
"Historical Composite Gross CPM (if applicable)"), row.names = c(NA, 
-2L), class = "data.frame")

# Child table
structure(list(Market = c("ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY"), Daypart = c("Daytime", "Early Fringe", 
"Early Morning", "Early News", "Late Fringe", "Late News", "Prime Access", 
"Prime Time", "tv_2", "tv_3", "tv_cross_screen", "Daytime", "Early Fringe", 
"Early Morning", "Early News", "Late Fringe", "Late News", "Prime Access", 
"Prime Time", "tv_2", "tv_3", "tv_cross_screen"), `Mix (%)` = c(15, 
10, 15, 10, 5, 5, 10, 10, 0, 0, 0, 15, 10, 15, 10, 5, 5, 10, 
10, 0, 0, 0), `Spot:30 (%)` = c(15, 10, 15, 10, 5, 5, 10, 10, 
0, 0, 0, 15, 10, 15, 10, 5, 5, 10, 10, 0, 0, 0), `Spot:15 (%)` = c(0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
), `Gross CPP ($)` = c(18, 18, 16, 23, 24, 40, 26, 44, 0, 0, 
0, 77, 71, 61, 78, 109, 145, 93, 213, 0, 0, 0), `Gross CPM ($)` = c(1.57, 
1.57, 1.39, 2, 2.09, 3.49, 2.27, 3.83, 23, 21, 13, 6.71, 6.19, 
5.32, 6.8, 9.5, 12.63, 8.1, 18.56, 23, 21, 13), `Historical Composite CPP ($)` = c(0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
), `Historical Composite CPM ($)` = c(0, 0, 0, 0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), .Names = c("Market", 
"Daypart", "Mix (%)", "Spot:30 (%)", "Spot:15 (%)", "Gross CPP ($)", 
"Gross CPM ($)", "Historical Composite CPP ($)", "Historical Composite CPM ($)"
), class = "data.frame", row.names = c(NA, -22L))

  # Module to create the nested structure of the table
  NestedData <- function(dat, children) {
    stopifnot(length(children) == nrow(dat))
    g <- function(d){
      if(is.data.frame(d)){
        purrr::transpose(d)
      }else{
        purrr::transpose(NestedData(d[[1]], children = d$children))
      }
    }
    subdats <- lapply(children, g)
    oplus <- sapply(subdats, function(x) if(length(x)) "<img src=\'https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\'/>" else "")
    cbind(" " = oplus, dat, "_details" = I(subdats), stringsAsFactors = FALSE)
  }
  # Bind the market level and mix breakout data together for the final table
  market_mix_table <- reactive({
    # Take a dependency on input$goButton
    input$goButton
    isolate({
      # The rolled up market view - Parent table
      parent <- market_level_view()
      # The daypart breakout for each market - Child table
      child <- mix_breakout_digital_elements()
      # Make the dataframe
      # This must be met length(children) == nrow(dat)
      Dat <- NestedData(
        dat = parent,
        children = split(child, child$Market)
      )
      return(Dat)
    })
  })
  # Render the table
  output$daypartTable <- DT::renderDataTable({
    # Whether to show row names (set TRUE or FALSE)
    rowNames <- FALSE
    colIdx <- as.integer(rowNames)
    # The data
    Dat <- market_mix_table()
    parentRows <- which(Dat[,1] != "")
    excelTitle <- paste(
      input$name,
      input$medium,
      input$quarter,
      "Market CPM-CPP Breakout",
      sep=" "
    )
    ## If the JS stops working take the coed and put it here
    callback_js = JS(
      "var ok = true;",
      "function onUpdate(updatedCell, updatedRow, oldValue) {",
      "  var column = updatedCell.index().column;",
      "  if(column === 8){",
      "    ok = false;",
      "  }else if(column === 7){",
      "    ok = true;",
      "  }",
      "}",
      sprintf("var parentRows = [%s];", toString(parentRows-1)),
      sprintf("var j0 = %d;", colIdx),
      "var nrows = table.rows().count();",
      "for(var i=0; i < nrows; ++i){",
      "  if(parentRows.indexOf(i) > -1){",
      "    table.cell(i,j0).nodes().to$().css({cursor: 'pointer'});",
      "  }else{",
      "    table.cell(i,j0).nodes().to$().removeClass('details-control');",
      "  }",
      "}",
      "",
      "// make the table header of the nested table",
      "var format = function(d, childId){",
      "  if(d != null){",
      "    var html = ",
      "      '<table class=\"display compact hover\" ' + ",
      "      'style=\"padding-left: 30px;\" id=\"' + childId + '\"><thead><tr>';",
      "    for(var key in d[d.length-1][0]){",
      "      html += '<th>' + key + '</th>';",
      "    }",
      "    html += '</tr></thead><tfoot><tr>'",
      "    for(var key in d[d.length-1][0]){",
      "      html += '<th></th>';",
      "    }",
      "    return html + '</tr></tfoot></table>';",
      "  } else {",
      "    return '';",
      "  }",
      "};",
      "",
      "// row callback to style the rows of the child tables",
      "var rowCallback = function(row, dat, displayNum, index){",
      "  if($(row).hasClass('odd')){",
      "    $(row).css('background-color', 'white');",
      "    $(row).hover(function(){",
      "      $(this).css('background-color', 'lightgreen');",
      "    }, function() {",
      "      $(this).css('background-color', 'white');",
      "    });",
      "  } else {",
      "    $(row).css('background-color', 'white');",
      "    $(row).hover(function(){",
      "      $(this).css('background-color', 'lightblue');",
      "    }, function() {",
      "      $(this).css('background-color', 'white');",
      "    });",
      "  }",
      "};",
      "",
      "// header callback to style the header of the child tables",
      "var headerCallback = function(thead, data, start, end, display){",
      "  $('th', thead).css({",
      "    'color': 'black',",
      "    'background-color': 'white'",
      "  });",
      "};",
      "",
      "// make the datatable",
      "var format_datatable = function(d, childId, rowIdx){",
      "  // footer callback to display the totals",
      "  // and update the parent row",
      "  var footerCallback = function(tfoot, data, start, end, display){",
      "    $('th', tfoot).css('background-color', '#F5F2F2');",
      "    var api = this.api();",
      "// update the Override CPM when the Override CPP is changed",
      "    var col_override_cpp = api.column(7).data();",
      "    var col_population = api.column(9).data();",
      "    if(ok){",
      "      for(var i = 0; i < col_override_cpp.length; i++){",
      "        api.cell(i,8).data(((parseFloat(col_override_cpp[i])*100)/(parseFloat(col_population[i])/1000)).toFixed(2));",
      "      }",
      "    }",
      "// update the Override CPP when the Override CPM is changed",
      "    var col_override_cpm = api.column(8).data();",
      "    for(var i = 0; i < col_override_cpm.length; i++){",
      "      api.cell(i,7).data(((parseFloat(col_override_cpm[i])*parseFloat(col_population[i])/1000)/100).toFixed(0));",
      "    }",
      "// Update the spot mixes",
      "    var col_mix_percentage = api.column(2).data();",
      "    var col_mix60_mix30 = api.column(10).data();",
      "    var col_mix30_mix15 = api.column(11).data();",
      "    for(var i = 0; i < col_mix_percentage.length; i++){",
      "      api.cell(i,3).data((parseFloat(col_mix_percentage[i])*parseFloat(col_mix60_mix30[i])).toFixed(1));",
      "      api.cell(i,4).data((parseFloat(col_mix_percentage[i])*parseFloat(col_mix30_mix15[i])).toFixed(1));",
      "    }",
      "    var child_col_CPM = api.column(6).data();",
      "    for(var i = 0; i < child_col_CPM.length; i++){",
      "      api.cell(i,6).data(parseFloat(child_col_CPM[i]).toFixed(2));",
      "    }",
      # "    parseFloat(api.column(5)).toFixed(2)",
      "// Make the footer sums",
      "    api.columns().eq(0).each(function(index){",
      "      if(index == 0) return $(api.column(index).footer()).html('Mix Total');",
      "      var coldata = api.column(index).data();",
      "      var total = coldata",
      "          .reduce(function(a, b){return parseInt(a) + parseInt(b)}, 0);",
      "      if(index == 3 || index == 4 ||index == 5 || index == 6 || index == 7 || index == 8) {",
      "        $(api.column(index).footer()).html('');",
      "      } else {",
      "        $(api.column(index).footer()).html(total);",
      "      }",
      "      if(total == 100) {",
      "        $(api.column(index).footer()).css({'color': 'green'});",
      "      } else {",
      "        $(api.column(index).footer()).css({'color': 'red'});",
      "      }",
      "    })",
      "  // update the parent row",
      "    var col_share = api.column(2).data();",
      "    var col_CPP = api.column(5).data();",
      "    var col_CPM = api.column(6).data();",
      "    var col_Historical_CPP = api.column(7).data();",
      "    var col_Historical_CPM = api.column(8).data();",
      "    var CPP = 0, CPM = 0, Historical_CPP = 0, Historical_CPM = 0;",
      "    for(var i = 0; i < col_share.length; i++){",
      "      CPP += (parseInt(col_share[i])*parseInt(col_CPP[i]).toFixed(0));",
      "      CPM += (parseInt(col_share[i])*parseInt(col_CPM[i]).toFixed(2));",
      "      Historical_CPP += (parseInt(col_share[i])*parseInt(col_Historical_CPP[i]).toFixed(0));",
      "      Historical_CPM += (parseInt(col_share[i])*parseInt(col_Historical_CPM[i]).toFixed(2));",
      "    }",
      "    table.cell(rowIdx, j0+3).data((CPP/100).toFixed(2));",
      "    table.cell(rowIdx, j0+4).data((CPM/100).toFixed(2));",
      "    table.cell(rowIdx, j0+5).data((Historical_CPP/100).toFixed(2));",
      "    table.cell(rowIdx, j0+6).data((Historical_CPM/100).toFixed(2));",
      "  }",
      "  var n = d.length - 1;",
      "  var id = 'table#' + childId;",
      "  var columns = Object.keys(d[n][0]).map(function(x){",
      "    return {data: x, title: x};",
      "  });",
      "  if (Object.keys(d[n][0]).indexOf('_details') === -1) {",
      "    var subtable = $(id).DataTable({",
      "                 'data': d[n],",
      "                 'columns': columns,",
      "                 'autoWidth': true,",
      "                 'deferRender': true,",
      "                 'info': false,",
      "                 'lengthChange': false,",
      "                 'ordering': d[n].length > 1,",
      "                 'order': [],",
      "                 'paging': true,",
      "                 'scrollX': false,",
      "                 'scrollY': false,",
      "                 'searching': false,",
      "                 'sortClasses': false,",
      "                 'pageLength': 50,",
      "                 'rowCallback': rowCallback,",
      "                 'headerCallback': headerCallback,",
      "                 'footerCallback': footerCallback,",
      "                 'columnDefs': [",
      "                  {targets: [0, 9, 10, 11], visible: false},",
      "                  {targets: '_all', className: 'dt-center'}",
      "                 ]",
      "               });",
      "  } else {",
      "    var subtable = $(id).DataTable({",
      "            'data': d[n],",
      "            'columns': columns,",
      "            'autoWidth': true,",
      "            'deferRender': true,",
      "            'info': false,",
      "            'lengthChange': false,",
      "            'ordering': d[n].length > 1,",
      "            'order': [],",
      "            'paging': true,",
      "            'scrollX': false,",
      "            'scrollY': false,",
      "            'searching': false,",
      "            'sortClasses': false,",
      "            'pageLength': 50,",
      "            'rowCallback': rowCallback,",
      "            'headerCallback': headerCallback,",
      "            'footerCallback': footerCallback,",
      "            'columnDefs': [",
      "              {targets: [0, 9, 10, 11], visible: false},",
      "              {targets: -1, visible: false},",
      "              {targets: 0, orderable: false, className: 'details-control'},",
      "              {targets: '_all', className: 'dt-center'}",
      "             ]",
      "          }).column(0).nodes().to$().css({cursor: 'pointer'});",
      "  }",
      "  subtable.MakeCellsEditable({",
      "    onUpdate: onUpdate,",
      "    inputCss: 'my-input-class',",
      "    columns: [2, 7, 8],",
      "    confirmationButton: {",
      "      confirmCss: 'my-confirm-class',",
      "      cancelCss: 'my-cancel-class'",
      "    }",
      "  });",
      "};",
      "",
      "// display the child table on click",
      "// array to store the id's of the already created child tables",
      "var children = [];",
      "table.on('click', 'td.details-control', function(){",
      "  var tbl = $(this).closest('table'),",
      "      tblId = tbl.attr('id'),",
      "      td = $(this),",
      "      row = $(tbl).DataTable().row(td.closest('tr')),",
      "      rowIdx = row.index();",
      "  if(row.child.isShown()){",
      "    row.child.hide();",
      "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\"/>');",
      "  } else {",
      "    var childId = tblId + '-child-' + rowIdx;",
      "// this child table has not been created yet",
      "    if(children.indexOf(childId) === -1){",
      "      children.push(childId);",
      "      row.child(format(row.data(), childId)).show();",
      "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');",
      "      format_datatable(row.data(), childId, rowIdx);",
      "    }else{",
      "      row.child(true);",
      "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');",
      "    }",
      "  }",
      "});"
    )
    # Download button
    downloadButtonJS <-  c(
      "function(xlsx) {",
      "  var table = $('#daypartTable').find('table').DataTable();",
      "  // Letters for Excel columns.",
      "  var LETTERS = [",
      "    'A','B','C','D','E','F','G','H','I','J','K','L','M',",
      "    'N','O','P','Q','R','S','T','U','V','W','X','Y','Z'",
      "  ];",
      "  // Get sheet.",
      "  var sheet = xlsx.xl.worksheets['sheet1.xml'];",
      "  // Get a clone of the sheet data.        ",
      "  var sheetData = $('sheetData', sheet).clone();",
      "  // Clear the current sheet data for appending rows.",
      "  $('sheetData', sheet).empty();",
      "  // Row count in Excel sheet.",
      "  var rowCount = 1;",
      "  // Iterate each row in the sheet data.",
      "  $(sheetData).children().each(function (index) {",
      "    // Used for DT row() API to get child data.",
      "    var rowIndex = index - 2;", #
      "    // Don't process row if its the header row.",
      sprintf("    if (index > 1 && index < %d) {", nrow(Dat)+2), #
      "      // Get row",
      "      var row = $(this.outerHTML);",
      "      // Set the Excel row attr to the current Excel row count.",
      "      row.attr('r', rowCount);",
      "      // Iterate each cell in the row to change the row number.",
      "      row.children().each(function (index) {",
      "        var cell = $(this);",
      "        // Set each cell's row value.",
      "        var rc = cell.attr('r');",
      "        rc = rc.replace(/\\d+$/, \"\") + rowCount;",
      "        cell.attr('r', rc);",
      "      });",
      "      // Get the row HTML and append to sheetData.",
      "      row = row[0].outerHTML;",
      "      $('sheetData', sheet).append(row);",
      "      rowCount++;",
      "      // Get the child data - could be any data attached to the row.",
      "      // Basically this grabd all the rows of data",
      sprintf("      var childData = table.row(':eq(' + rowIndex + ')').data()[%d];", ncol(Dat)-1),
      "      if (childData.length > 0) {",
      "        var colNames = Object.keys(childData[0]).slice(1,9);",
      "        // Prepare Excel formatted row",
      "        headerRow = '<row r=\"' + rowCount +",
      "          '\"><c t=\"inlineStr\" r=\"A' + rowCount +",
      "          '\"><is><t></t></is></c>';",
      "        for(var i = 0; i < colNames.length; i++){",
      "          headerRow = headerRow +",
      "            '<c t=\"inlineStr\" r=\"' + LETTERS[i+1] + rowCount +",
      "            '\" s=\"7\"><is><t>' + colNames[i] +", 
      "            '</t></is></c>';",
      "        }",
      "        headerRow = headerRow + '</row>';",
      "        // Append header row to sheetData.",
      "        $('sheetData', sheet).append(headerRow);",
      "        rowCount++; // Inc excelt row counter.",
      "      }",
      "      // The child data is an array of rows",
      "      for (let c = 0; c < childData.length; c++) {",
      "        // Get row data.",
      "        var child = childData[c];",
      "        // Prepare Excel formatted row",
      "        var childRow = '<row r=\"' + rowCount +",
      "          '\"><c t=\"inlineStr\" r=\"A' + rowCount +",
      "          '\"><is><t></t></is></c>';",
      "        for(let i = 0; i < colNames.length; i++){",
      "          childRow = childRow +",
      "            '<c t=\"inlineStr\" r=\"' + LETTERS[i+1] + rowCount +",
      "            '\" s=\"5\"><is><t>' + child[colNames[i]] +", 
      "            '</t></is></c>';",
      "        }",
      "        childRow = childRow + '</row>';",
      "        // Append row to sheetData.",
      "        $('sheetData', sheet).append(childRow);",
      "        rowCount++; // Inc excel row counter.",
      "      }",
      "      // Just append the header row and increment the excel row counter.",
      "    } else {",
      "      $('sheetData', sheet).append(this.outerHTML);",
      "      rowCount++;",
      "    }",
      "  });",
      "}"
    )
    # Table
    table <- DT::datatable(
      Dat,
      callback = callback_js,
      rownames = rowNames,
      escape = -colIdx-1,
      style = "bootstrap4",
      extensions = 'Buttons',
      options = list(
        dom = "Bt",
        columnDefs = list(
          list(width = '30px', targets = 0),
          list(width = '330px', targets = 1),
          list(visible = FALSE, targets = ncol(Dat)-1+colIdx),
          list(orderable = FALSE, className = 'details-control', targets = colIdx),
          list(className = "dt-center", targets = "_all")
        ),
        buttons = list(
          list(
            extend = "excel",
            className = 'btn btn-primary glyphicon glyphicon-download-alt',
            text = " Export",
            exportOptions = list(
              orthogonal = "export",
              columns = 0:(ncol(Dat)-2)
            ),
            title = excelTitle,
            orientation = "landscape",
            customize = JS(downloadButtonJS)
          )
        ),
        lengthMenu = list(c(-1, 10, 20),
                          c("All", 10, 20))
      )
    )
    # Call the html tools deps (js & css files in this directory)
    cell_edit_dep <- htmltools::htmlDependency(
      "CellEdit", "1.0.19", 
      src = 'www/',
      script = "dataTables.cellEdit.js",
      stylesheet = "dataTables.cellEdit.css"
    )
    table$dependencies <- c(table$dependencies, list(cell_edit_dep))
    
    table %>% formatStyle(
      c(MARKET[2], 'Population', SQAD_CPP_DOLLAR, SQAD_CPM_DOLLAR, OVERRIDE_CPP_DOLLAR, OVERRIDE_CPM_DOLLAR),
      target = 'row',
      backgroundColor = "#F5F2F2"
    )
  }, server = FALSE)
  
  ### This is where I am trying to save the edits to the table and
  ### use those new values for the below table 
  output$market_costings_gross_net_table <- renderTable({
    # Get the data from reative function
    market_costings <- market_level_view()
    reactive_market_costings <- reactiveValues(data = market_costings)
    
    proxy <- dataTableProxy("daypartTable")
    
    observeEvent(input$tableRefresh, {
      DT::replaceData(proxy, reactive_market_costings$data)
    })
  })

回答1:


First round

To get the data in Shiny:

  • add this line at the end of the footerCallback function:

    "    Shiny.setInputValue('data:nestedData', table.data().toArray());",
    
  • before the Shiny app, add:

  library(jsonlite)

  registerInputHandler(
    "nestedData", 
    function(data, ...){
      fromJSON(toJSON(data))
    },
    force = TRUE
  )
  • then the data is available in input[["data"]]:
  observe({
    print(input[["data"]])
  })

If you need more help, please edit your post to make your code reproducible. I don't have your latest version of this code.


Second round

As said in my comment, it's better to use toFixed in a render option. I also set the type of editable cells to number, so that the values of the edited cells are not converted to strings, and moreover there are spinner arrows in the editing boxes.

library(DT)

df_children <-
  structure(
    list(
      Market = c(
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY"
      ),
      Daypart = c(
        "Daytime",
        "Early Fringe",
        "Early Morning",
        "Early News",
        "Late Fringe",
        "Late News",
        "Prime Access",
        "Prime Time",
        "tv_2",
        "tv_3",
        "tv_cross_screen",
        "Daytime",
        "Early Fringe",
        "Early Morning",
        "Early News",
        "Late Fringe",
        "Late News",
        "Prime Access",
        "Prime Time",
        "tv_2",
        "tv_3",
        "tv_cross_screen"
      ),
      `Mix (%)` = c(15,
                    10, 15, 10, 5, 5, 10, 10, 0, 0, 0, 15, 10, 15, 10, 5, 5, 10,
                    10, 0, 0, 0),
      `Spot:30 (%)` = c(15, 10, 15, 10, 5, 5, 10, 10,
                        0, 0, 0, 15, 10, 15, 10, 5, 5, 10, 10, 0, 0, 0),
      `Spot:15 (%)` = c(0,
                        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
      `Gross CPP ($)` = c(
        18,
        18,
        16,
        23,
        24,
        40,
        26,
        44,
        0,
        0,
        0,
        77,
        71,
        61,
        78,
        109,
        145,
        93,
        213,
        0,
        0,
        0
      ),
      `Gross CPM ($)` = c(
        1.57,
        1.57,
        1.39,
        2,
        2.09,
        3.49,
        2.27,
        3.83,
        23,
        21,
        13,
        6.71,
        6.19,
        5.32,
        6.8,
        9.5,
        12.63,
        8.1,
        18.56,
        23,
        21,
        13
      ),
      `Historical Composite CPP ($)` = c(0,
                                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
      `Historical Composite CPM ($)` = c(0, 0, 0, 0, 0, 0, 0, 0,
                                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
      population = c(
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L
      ),
      slider_60s = c(
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4
      ),
      slider_30s = c(
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6
      )
    ),
    .Names = c(
      "Market",
      "Daypart",
      "Mix (%)",
      "Spot:30 (%)",
      "Spot:15 (%)",
      "Gross CPP ($)",
      "Gross CPM ($)",
      "Historical Composite CPP ($)",
      "Historical Composite CPM ($)",
      "population",
      "slider_60s",
      "slider_30s"
    ),
    class = "data.frame",
    row.names = c(NA,-22L)
  )

df_parent <-
  structure(
    list(
      Market = c("ABILENE-SWEETWATER", "ALBANY-SCHENECTADY-TROY, NY"),
      `Gross CPP` = c(1.94, 7.89),
      `Gross CPM` = c(1.02, 0.82),
      `Historical Composite Gross CPP (if applicable)` = c(0, 0),
      `Historical Composite Gross CPM (if applicable)` = c(0, 0)
    ),
    .Names = c(
      "Market",
      "Gross CPP",
      "Gross CPM",
      "Historical Composite Gross CPP (if applicable)",
      "Historical Composite Gross CPM (if applicable)"
    ),
    row.names = c(NA,-2L),
    class = "data.frame"
  )

# function to make the required dataframe
NestedData <- function(dat, children){
  stopifnot(length(children) == nrow(dat))
  g <- function(d){
    if(is.data.frame(d)){
      purrr::transpose(d)
    }else{
      purrr::transpose(NestedData(d[[1]], children = d$children))
    }
  }
  subdats <- lapply(children, g)
  oplus <- sapply(subdats, function(x) if(length(x)) "&oplus;" else "")
  cbind(" " = oplus, dat, "_details" = I(subdats), stringsAsFactors = FALSE)
}

# make the required dataframe
# one must have: length(children) == nrow(dat)
Dat <- NestedData(
  dat = df_parent,
  children = split(df_children, df_children$Market)
)

## whether to show row names (set TRUE or FALSE)
rowNames <- FALSE
colIdx <- as.integer(rowNames)

## make the callback
parentRows <- which(Dat[,1] != "")

callback_js = JS(
  "var ok = true;",
  "function onUpdate(updatedCell, updatedRow, oldValue) {",
  "  var column = updatedCell.index().column;",
  "  if(column === 8){",
  "    ok = false;",
  "  }else if(column === 7){",
  "    ok = true;",
  "  }",
  "}",
  "function render0(data, type, row) {", # @Timothy, new
  "  if(type === 'display') {",
  "    return parseFloat(data).toFixed(0);",
  "  } else {",
  "    return data;",
  "  }",
  "}",
  "function render2(data, type, row) {", # @Timothy, new
  "  if(type === 'display') {",
  "    return parseFloat(data).toFixed(2);",
  "  } else {",
  "    return data;",
  "  }",
  "}",
  sprintf("var parentRows = [%s];", toString(parentRows-1)),
  sprintf("var j0 = %d;", colIdx),
  "var nrows = table.rows().count();",
  "for(var i=0; i < nrows; ++i){",
  "  if(parentRows.indexOf(i) > -1){",
  "    table.cell(i,j0).nodes().to$().css({cursor: 'pointer'});",
  "  }else{",
  "    table.cell(i,j0).nodes().to$().removeClass('details-control');",
  "  }",
  "}",
  "",
  "// make the table header of the nested table",
  "var format = function(d, childId){",
  "  if(d != null){",
  "    var html = ",
  "      '<table class=\"display compact hover\" ' + ",
  "      'style=\"padding-left: 30px;\" id=\"' + childId + '\"><thead><tr>';",
  "    for(var key in d[d.length-1][0]){",
  "      html += '<th>' + key + '</th>';",
  "    }",
  "    html += '</tr></thead><tfoot><tr>'",
  "    for(var key in d[d.length-1][0]){",
  "      html += '<th></th>';",
  "    }",
  "    return html + '</tr></tfoot></table>';",
  "  } else {",
  "    return '';",
  "  }",
  "};",
  "",
  "// row callback to style the rows of the child tables",
  "var rowCallback = function(row, dat, displayNum, index){",
  "  if($(row).hasClass('odd')){",
  "    $(row).css('background-color', 'white');",
  "    $(row).hover(function(){",
  "      $(this).css('background-color', 'lightgreen');",
  "    }, function() {",
  "      $(this).css('background-color', 'white');",
  "    });",
  "  } else {",
  "    $(row).css('background-color', 'white');",
  "    $(row).hover(function(){",
  "      $(this).css('background-color', 'lightblue');",
  "    }, function() {",
  "      $(this).css('background-color', 'white');",
  "    });",
  "  }",
  "};",
  "",
  "// header callback to style the header of the child tables",
  "var headerCallback = function(thead, data, start, end, display){",
  "  $('th', thead).css({",
  "    'color': 'black',",
  "    'background-color': 'white'",
  "  });",
  "};",
  "",
  "// make the datatable",
  "var format_datatable = function(d, childId, rowIdx){",
  "  // footer callback to display the totals",
  "  // and update the parent row",
  "  var footerCallback = function(tfoot, data, start, end, display){", # @Timothy, I removed all the 'toFixed'
  "    $('th', tfoot).css('background-color', '#F5F2F2');",
  "    var api = this.api();",
  "// update the Override CPM when the Override CPP is changed",
  "    var col_override_cpp = api.column(7).data();",
  "    var col_population = api.column(9).data();",
  "    if(ok){",
  "      for(var i = 0; i < col_override_cpp.length; i++){",
  "        api.cell(i,8).data(((parseFloat(col_override_cpp[i])*100)/(parseFloat(col_population[i])/1000)));",
  "      }",
  "    }",
  "// update the Override CPP when the Override CPM is changed",
  "    var col_override_cpm = api.column(8).data();",
  "    for(var i = 0; i < col_override_cpm.length; i++){",
  "      api.cell(i,7).data(((parseFloat(col_override_cpm[i])*parseFloat(col_population[i])/1000)/100));",
  "    }",
  "// Update the spot mixes",
  "    var col_mix_percentage = api.column(2).data();",
  "    var col_mix60_mix30 = api.column(10).data();",
  "    var col_mix30_mix15 = api.column(11).data();",
  "    for(var i = 0; i < col_mix_percentage.length; i++){",
  "      api.cell(i,3).data((parseFloat(col_mix_percentage[i])*parseFloat(col_mix60_mix30[i])));",
  "      api.cell(i,4).data((parseFloat(col_mix_percentage[i])*parseFloat(col_mix30_mix15[i])));",
  "    }",
  "    var child_col_CPM = api.column(6).data();",
  "    for(var i = 0; i < child_col_CPM.length; i++){",
  "      api.cell(i,6).data(parseFloat(child_col_CPM[i]));",
  "    }",
  "// Make the footer sums",
  "    api.columns().eq(0).each(function(index){",
  "      if(index == 0) return $(api.column(index).footer()).html('Mix Total');",
  "      var coldata = api.column(index).data();",
  "      var total = coldata",
  "          .reduce(function(a, b){return parseFloat(a) + parseFloat(b);}, 0);",
  "      if(index == 3 || index == 4 ||index == 5 || index == 6 || index == 7 || index == 8) {",
  "        $(api.column(index).footer()).html('');",
  "      } else {",
  "        $(api.column(index).footer()).html(total);",
  "      }",
  "      if(total == 100) {",
  "        $(api.column(index).footer()).css({'color': 'green'});",
  "      } else {",
  "        $(api.column(index).footer()).css({'color': 'red'});",
  "      }",
  "    })",
  "  // update the parent row", # @Timothy, I replaced everywhere parseInt with parseFloat
  "    var col_share = api.column(2).data();",
  "    var col_CPP = api.column(5).data();",
  "    var col_CPM = api.column(6).data();",
  "    var col_Historical_CPP = api.column(7).data();",
  "    var col_Historical_CPM = api.column(8).data();",
  "    var CPP = 0, CPM = 0, Historical_CPP = 0, Historical_CPM = 0;",
  "    for(var i = 0; i < col_share.length; i++){",
  "      CPP += (parseFloat(col_share[i])*parseFloat(col_CPP[i]));",
  "      CPM += (parseFloat(col_share[i])*parseFloat(col_CPM[i]));",
  "      Historical_CPP += (parseFloat(col_share[i])*parseFloat(col_Historical_CPP[i]));",
  "      Historical_CPM += (parseFloat(col_share[i])*parseFloat(col_Historical_CPM[i]));",
  "    }",
  "    table.cell(rowIdx, j0+2).data(CPP/100);", # @Timothy, there were errors here (it's j0 + 2/3/4/5)
  "    table.cell(rowIdx, j0+3).data(CPM/100);",
  "    table.cell(rowIdx, j0+4).data(Historical_CPP/100);",
  "    table.cell(rowIdx, j0+5).data(Historical_CPM/100);",
  "    Shiny.setInputValue('data:nestedData', table.data().toArray());",
  "  }",
  "  var n = d.length - 1;",
  "  var id = 'table#' + childId;",
  "  var columns = Object.keys(d[n][0]).map(function(x){",
  "    return {data: x, title: x};",
  "  });",
  "  var subtable = $(id).DataTable({",
  "                 'data': d[n],",
  "                 'columns': columns,",
  "                 'autoWidth': true,",
  "                 'deferRender': true,",
  "                 'info': false,",
  "                 'lengthChange': false,",
  "                 'ordering': d[n].length > 1,",
  "                 'order': [],",
  "                 'paging': true,",
  "                 'scrollX': false,",
  "                 'scrollY': false,",
  "                 'searching': false,",
  "                 'sortClasses': false,",
  "                 'pageLength': 50,",
  "                 'rowCallback': rowCallback,",
  "                 'headerCallback': headerCallback,",
  "                 'footerCallback': footerCallback,",
  "                 'columnDefs': [",
  "                  {targets: [2, 3, 4], render: render0},", # @Timothy, new
  "                  {targets: [5, 6, 7, 8], render: render2},", # @Timothy, new
  "                  {targets: [0, 9, 10, 11], visible: false},",
  "                  {targets: '_all', className: 'dt-center'}",
  "                 ]",
  "               });",
  "  subtable.MakeCellsEditable({",
  "    onUpdate: onUpdate,",
  "    inputCss: 'my-input-class',",
  "    columns: [2, 7, 8],", 
  "    inputTypes: [", # @Timothy, new 
  "      {column: 2, type: 'number'},",
  "      {column: 7, type: 'number'},",
  "      {column: 8, type: 'number'}",
  "    ],",
  "    confirmationButton: {",
  "      confirmCss: 'my-confirm-class',",
  "      cancelCss: 'my-cancel-class'",
  "    }",
  "  });",
  "};",
  "",
  "// display the child table on click",
  "// array to store the id's of the already created child tables",
  "var children = [];",
  "table.on('click', 'td.details-control', function(){",
  "  var tbl = $(this).closest('table'),",
  "      tblId = tbl.attr('id'),",
  "      td = $(this),",
  "      row = $(tbl).DataTable().row(td.closest('tr')),",
  "      rowIdx = row.index();",
  "  if(row.child.isShown()){",
  "    row.child.hide();",
  "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\"/>');",
  "  } else {",
  "    var childId = tblId + '-child-' + rowIdx;",
  "// this child table has not been created yet",
  "    if(children.indexOf(childId) === -1){",
  "      children.push(childId);",
  "      row.child(format(row.data(), childId)).show();",
  "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');",
  "      format_datatable(row.data(), childId, rowIdx);",
  "    }else{",
  "      row.child(true);",
  "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');",
  "    }",
  "  }",
  "});"
)


render_js <- JS( # @Timothy, new
  "function(data, type, row) {",
  "  if(type === 'display') {",
  "    return '$' + data.toFixed(2);",
  "  } else {",
  "    return data;",
  "  }",
  "}"
)


## the datatable
dtable <- datatable(
  Dat, callback = callback_js, rownames = rowNames, escape = -colIdx-1, 
  extensions = "Buttons", 
  options = list(
    dom = "Bfrtip",
    columnDefs = list(
      list(render = render_js, targets = colIdx + 1 + 1:4), # @Timothy, new
      list(visible = FALSE, targets = ncol(Dat)-1+colIdx),
      list(orderable = FALSE, className = 'details-control', targets = colIdx),
      list(className = "dt-center", targets = "_all")
    )
  )
)

path <- "~/Work/R/DT" # folder containing the files dataTables.cellEdit.js
                      # and dataTables.cellEdit.css
dep <- htmltools::htmlDependency(
  "CellEdit", "1.0.19", path, 
  script = "dataTables.cellEdit.js", stylesheet = "dataTables.cellEdit.css")
dtable$dependencies <- c(dtable$dependencies, list(dep))






library(shiny)
library(jsonlite)

registerInputHandler(
  "nestedData", 
  function(data, ...){
    fromJSON(toJSON(data))
  },
  force = TRUE
)

ui <- fluidPage(
  br(),
  DTOutput("dtable")
)

server <- function(input, output){
  
  output[["dtable"]] <- renderDT(dtable)
  
  observe({
    print(input[["data"]])
  })
  
}

shinyApp(ui, server)


来源:https://stackoverflow.com/questions/64159670/replacing-initial-dataframe-from-an-editable-datatable-and-using-that-new-data-f

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!