替换 editable DataTable 中的初始数据框并在另一个 table 中使用该新数据框
Replacing initial Dataframe from an editable DataTable and using that new data frame in another table
我的 Shiny 应用程序中有一个嵌套的 DataTable。 child table 中的一些列被用户编辑table。这里的目标是让用户编辑值,然后数据框将被这些新值替换。然后我想将替换的数据框用于另一个 table。我需要在另一个 table.
中使用编辑后的值进行计算
工作流程:
create nested data table -> user edits values -> initial data is replaced by new data -> use the new data for another table
我尝试使用 reactiveValues()
、dataTableProxy()
、replaceData()
和 observeEvert()
来遵循该工作流程,但没有成功。
# Parent table
structure(list(Market = c("ABILENE-SWEETWATER", "ALBANY-SCHENECTADY-TROY, NY"
), `Gross CPP` = c(".94", ".89"), `Gross CPM` = c(".02",
"[=12=].82"), `Historical Composite Gross CPP (if applicable)` = c("[=12=]",
"[=12=]"), `Historical Composite Gross CPM (if applicable)` = c("[=12=]",
"[=12=]")), .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)
})
})
第一轮
要在 Shiny 中获取数据:
在footerCallback
函数末尾添加这一行:
" Shiny.setInputValue('data:nestedData', table.data().toArray());",
在 Shiny 应用程序之前,添加:
library(jsonlite)
registerInputHandler(
"nestedData",
function(data, ...){
fromJSON(toJSON(data))
},
force = TRUE
)
- 然后数据在
input[["data"]]
: 中可用
observe({
print(input[["data"]])
})
如果您需要更多帮助,请编辑您的 post 以使您的代码可重现。我没有此代码的最新版本。
第二轮
正如我在评论中所说,最好在 render
选项中使用 toFixed
。我还将可编辑单元格的类型设置为number
,这样编辑单元格的值就不会转换为字符串,而且编辑框里有微调箭头。
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)) "⊕" 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)
我的 Shiny 应用程序中有一个嵌套的 DataTable。 child table 中的一些列被用户编辑table。这里的目标是让用户编辑值,然后数据框将被这些新值替换。然后我想将替换的数据框用于另一个 table。我需要在另一个 table.
中使用编辑后的值进行计算工作流程:
create nested data table -> user edits values -> initial data is replaced by new data -> use the new data for another table
我尝试使用 reactiveValues()
、dataTableProxy()
、replaceData()
和 observeEvert()
来遵循该工作流程,但没有成功。
# Parent table
structure(list(Market = c("ABILENE-SWEETWATER", "ALBANY-SCHENECTADY-TROY, NY"
), `Gross CPP` = c(".94", ".89"), `Gross CPM` = c(".02",
"[=12=].82"), `Historical Composite Gross CPP (if applicable)` = c("[=12=]",
"[=12=]"), `Historical Composite Gross CPM (if applicable)` = c("[=12=]",
"[=12=]")), .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)
})
})
第一轮
要在 Shiny 中获取数据:
在
footerCallback
函数末尾添加这一行:" Shiny.setInputValue('data:nestedData', table.data().toArray());",
在 Shiny 应用程序之前,添加:
library(jsonlite)
registerInputHandler(
"nestedData",
function(data, ...){
fromJSON(toJSON(data))
},
force = TRUE
)
- 然后数据在
input[["data"]]
: 中可用
observe({
print(input[["data"]])
})
如果您需要更多帮助,请编辑您的 post 以使您的代码可重现。我没有此代码的最新版本。
第二轮
正如我在评论中所说,最好在 render
选项中使用 toFixed
。我还将可编辑单元格的类型设置为number
,这样编辑单元格的值就不会转换为字符串,而且编辑框里有微调箭头。
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)) "⊕" 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)