Can't hide columns AND set rownames = FALSE in Shiny DT - r

I created a data table in Shiny that uses DT to style values based on the values in a set of hidden columns. The table shows whether Units of a company have hit their goals for Calls and Emails.
The problem is that when I hide the columns (using columnDefs = list(list(targets = c(4, 5), visible = FALSE))), I can no longer use rownames = FALSE under the datatable() call: the table displays with no data. Does anyone know how I can get both these options to work together?
I've used the following articles:
https://rstudio.github.io/DT/010-style.html
How do I suppress row names when using DT::renderDataTable in R shiny?
library(shiny)
library(tidyverse)
library(DT)
x <- tibble(
Unit = c("Sales", "Marketing", "HR"),
Calls = c(100, 150, 120),
Emails = c(200, 220, 230),
Calls_goal = c(1, 0, 0),
Emails_goal = c(0, 1, 1)
)
ui <- fluidPage(
mainPanel(
DT::dataTableOutput("table")
)
)
server <- function(input, output) {
output$table <- DT::renderDataTable({
# Can't use both visible = FALSE and rownames = FALSE
datatable(x,
options = list(
columnDefs = list(list(targets = c(4, 5), visible = FALSE)) # THIS
),
rownames = TRUE) %>% # OR THIS
formatStyle(
columns = c('Calls', 'Emails'),
valueColumns = c('Calls_goal', 'Emails_goal'),
color = styleEqual(c(1, 0), c("red", "black"))
)
})
}
shinyApp(ui = ui, server = server)

As rownames are also a column, when you set them to false, yo need to reindex the columns you want to hide. So, in your particular case, column 5 no longer exist. Now it is number 4, and the 4th is the 3rd, so your code should look like:
server <- function(input, output) {
output$table <- DT::renderDataTable({
# Can't use both visible = FALSE and rownames = FALSE
datatable(x, rownames=F,
options = list(
columnDefs = list(list(targets = c(3, 4), visible = FALSE) # THIS
)
)) %>% # OR THIS
formatStyle(
columns = c('Calls', 'Emails'),
valueColumns = c('Calls_goal', 'Emails_goal'),
color = styleEqual(c(1, 0), c("red", "black"))
)
})
}

Related

Sorting Column DataTable for Shiny App by abs

Was hoping someone can help sort a column by absolute value in a Shiny app in the datatable() function? Tried multiple methods (dplyr, arrange, etc) but for some reason it's not clicking with me. It's a three column datatable, trying to sort column 2/val2 by the absolute value.
table_stage <- reactive ({
tbl <- datatable(tabledat(),
rownames = FALSE,
options = list(
columnDefs = list(list(className = "dt-center", targets = 2)),
order = list(list(2, "asc"))
)) %>%
formatRound("val", 2) %>%
formatRound("val2", 2)
return(tbl)
})
This is definitely wrong, did not work at all.
table_stage <- reactive ({
tbl <- datatable(tabledat(),
rownames = FALSE,
options = list(
columnDefs = list(list(className = "dt-center", targets = 2)),
order = list(list((arrange(abs(2)), "desc"))
)) %>%
formatRound("val", 2) %>%
formatRound("val2", 2)
return(tbl)
})
The code runs without problems when using dataTableOutput in a UI context and renderDataTable in the Server function.
This script works with order by mpg (the first column in the example table) and if mtcars is a data frame, it works too.
library(data.table)
library(shiny)
library(dplyr)
library(DT)
if (interactive()) {
ui <- fluidPage(
dataTableOutput("table")
)
#optional test data
tabledat <- data.table::as.data.table(mtcars)
server <- function(input, output) {
output$table <-
renderDataTable({
tabledat %>%
datatable(
rownames = FALSE,
options = list(
columnDefs = list(list(className = "dt-center", targets = 2)),
order = list(list(1, "asc"))
)
)
},
)
}
shinyApp(ui, server)
}
You need to use the render option:
library(DT)
js <- "
function(data, type, row, meta) {
if(type === 'sort') {
data = Math.abs(data);
}
return data;
}
"
mydata <- as.data.frame(
matrix(runif(40, -10000, 10000), nrow = 10, ncol = 4)
)
datatable(
mydata,
options = list(
"columnDefs" = list(
list(
"targets" = 1,
"render" = JS(js)
)
)
)
)

How to copy multiple row and column headers in a rendered table when using DT table copy function?

A similar question was posted but never answered: r shiny problem with datatable to copy a table with table head (colspan)
When running the below reproducible code, I'd like the DT "copy" button to include ALL table column and row headers, when there are multiple headers. So far DT copy only copies one header.
I have the code to do this using an action button/observeEvent() outside of DT (not shown in below code), but if possible I'd instead like to use DT's native copy clipboard function (like in the code below) because of other benefits it offers including but not limited to simplicity.
The images at the bottom better explain.
Maybe it's not possible. But maybe it is!
Reproducible code:
library(DT)
library(shiny)
library(dplyr)
library(htmltools)
library(data.table)
data <-
data.frame(
ID = c(1,1,1,2,2,2,3,3,3),
Period = c(1, 2, 3, 1, 2, 3, 1, 2, 3),
Values = c(5, 10, 15, 0, 2, 4, 3, 6, 9),
State = c("X0","X1","X2","X0","X2","X0", "X2","X1","X9")
)
numTransit <- function(x, from=1, to=3){
setDT(x)
unique_state <- unique(x$State)
all_states <- setDT(expand.grid(list(from_state = unique_state, to_state = unique_state)))
dcast(x[, .(from_state = State[from],
to_state = State[to]),
by = ID]
[,.N, c("from_state", "to_state")]
[all_states,on = c("from_state", "to_state")],
to_state ~ from_state, value.var = "N"
)
}
ui <- fluidPage(
tags$head(tags$style(".datatables .display {margin-left: 0;}")),
h4(strong("Transition table inputs:")),
numericInput("transFrom", "From period:", 1, min = 1, max = 3),
numericInput("transTo", "To period:", 2, min = 1, max = 3),
h4(strong("Output transition table:")),
DTOutput("resultsDT"),
)
server <- function(input, output, session) {
results <-
reactive({
results <- numTransit(data, input$transFrom, input$transTo) %>%
replace(is.na(.), 0) %>%
bind_rows(summarise_all(., ~(if(is.numeric(.)) sum(.) else "Sum")))
results <- cbind(results, Sum = rowSums(results[,-1]))
})
output$data <- renderTable(data)
output$resultsDT <- renderDT(server=FALSE, {
datatable(
data = results(),
rownames = FALSE,
extensions = c("Buttons", "Select"), # for Copy button
selection = 'none', # for Copy button
filter = 'none',
container = tags$table(
class = 'display',
tags$thead(
tags$tr(
tags$th(rowspan = 2,sprintf('To state where end period = %s',input$transTo),style="border-right: solid 1px;"),
tags$th(colspan = 10,sprintf('From state where initial period = %s', input$transFrom))),
tags$tr(mapply(tags$th, colnames(results())[-1],
style = sprintf("border-right: solid %spx;", rep(0, ncol(results()) - 1L)),
SIMPLIFY = FALSE))
)
),
options = list(scrollX = F,
buttons = list(list(extend = "copy",text = 'Copy',exportOptions = list(modifier = list(selected = TRUE)))), # for Copy button
dom = 'Bft', # added 'B' for Copy button
lengthChange = T,
pagingType = "numbers",
autoWidth = T,
info = FALSE,
searching = FALSE)
) %>%formatStyle(c(1), `border-right` = "solid 1px")
})
}
shinyApp(ui, server)
Additional example:
Below is another, simpler example of trying to copy/paste all headers using DT, starting with the example used in post How to copy tableOutput to clipboard? (however adding the "sketch" container to datatable for a second column header to illustrate the copy/paste issue I'm trying to address):
library(shiny)
library(dplyr)
library(DT)
library(htmltools)
df <- mtcars
one <- function(.data, var, na = TRUE) {
return({
.data %>%
group_by(.data[[var]]) %>%
filter(!is.na(.data[[var]])) %>%
tally() %>%
mutate(`%` = 100*n/sum(n))
})
}
# ADDED SKETCH TO ORIGINAL EXAMPLE:
sketch = htmltools::withTags(table(
class = 'display',
thead(tr(th(colspan = 3, 'Table')),
tr(lapply(c('Variable','n','%'),th))
)
))
ui <- fluidPage(
selectInput("var", label = "Select Variable", choices = c("", names(df))),
DTOutput("valu", width = "15%")
)
server <- function(input, output) {
output$valu <- renderDT({
if(input$var != '') {
data <- df %>% one(input$var, na = input$check)
DT::datatable(data,
class = 'cell-border stripe',
rownames = FALSE,
extensions = c("Buttons", "Select"),
selection = 'none',
container = sketch, # ADDED SKETCH CONTAINER TO ORIGINAL EXAMPLE
options =
list(
select = TRUE,
dom = "Bt",
buttons = list(
list(
extend = "copy",
text = 'Copy'))
)) %>% formatStyle(
0,
target = "row",
fontWeight = styleEqual(1, "bold")
)
}
}, server = FALSE)
output$value <- renderTable({
if(input$var != '') {
data <- df %>% one(input$var, na = input$check)
return(data)
}
}, spacing = "xs", bordered = TRUE)
}
shinyApp(ui, server)
Hmm... for Copy I don't know yet. But you can export such a table to Excel and then copy from Excel. I agree this is not highly convenient, but I don't know another way. This requires some JS libraries:
tags$script(src = "xlsx.core.min.js"), # https://github.com/SheetJS/sheetjs/blob/master/dist/xlsx.core.min.js
tags$script(src = "FileSaver.min.js"), # https://raw.githubusercontent.com/eligrey/FileSaver.js/master/dist/FileSaver.min.js
tags$script(src = "tableexport.min.js"), # https://github.com/clarketm/TableExport/tree/master/dist
tags$link(rel = "stylesheet", href = "tableexport.min.css")
library(shiny)
library(DT)
library(shinyjs)
js_export <-
"
var $table = $('#DTtable').find('table');
var instance = $table.tableExport({
formats: ['xlsx'],
exportButtons: false,
filename: 'myTable',
sheetname: 'Sheet1'
});
var exportData0 = instance.getExportData();
var exportData = exportData0[Object.keys(exportData0)[0]]['xlsx'];
instance.export2file(exportData.data, exportData.mimeType, exportData.filename,
exportData.fileExtension, exportData.merges,
exportData.RTL, exportData.sheetname);
"
ui <- fluidPage(
useShinyjs(),
tags$head(
# put these files in the www subfolder
tags$script(src = "xlsx.core.min.js"),
tags$script(src = "FileSaver.min.js"),
tags$script(src = "tableexport.min.js")
),
DTOutput("DTtable"),
actionButton("export", "Export table", class = "btn-primary")
)
sketch <- htmltools::withTags(table(
class = "display",
thead(
tr(
th(rowspan = 2, "Species"),
th(colspan = 2, "Sepal"),
th(colspan = 2, "Petal")
),
tr(
lapply(rep(c("Length", "Width"), 2), th)
)
)
))
server <- function(input, output, session){
output[["DTtable"]] <- renderDT({
datatable(
head(iris, 6),
container = sketch, rownames = FALSE
) %>%
formatPercentage("Sepal.Length") %>%
formatCurrency("Sepal.Width")
})
observeEvent(input[["export"]], {
runjs(js_export)
})
}
shinyApp(ui, server)
Note that it also takes the formatting into account, but I'm wondering why there are some dates :-/

How to save edits to a R Shiny datatable cell but not save any filters from a sliderInput?

I'm trying to build an editable datatable in R Shiny that saves changes to the cells to the original file.
I also want users to be able to filter the dataset, but I do not want any filters to affect the original file. For instance, if you change the Sepal.Length of a cell to be 400, I want to see that saved to the original file; however, if you filter the file, I want to still see all the observations in the original file.
I have parts of it figured out, but I cannot get all the parts to come together. Here's a below example using the iris data.
library(shiny)
library(DT)
library(dplyr)
#Saving iris to your desktop, so you can confirm if file is being edited
data("iris")
write_csv(iris, "~/desktop/iris.test") #my file path; you may need to edit
dt_output = function(title, id) {
fluidRow(column(
12, h1(paste0('Table ', sub('.*?([0-9]+)$', '\\1', id), ': ', title)),
hr(), DTOutput(id)
))
}
render_dt = function(data, editable = 'cell', server = TRUE, ...) {
renderDT(data, selection = 'none', server = server, editable = editable, ...)
}
shinyApp(
ui = fluidPage(
title = 'Double-click to edit table cells',
sliderInput("slide",
"Slider",
min = min(iris$Sepal.Length),
max = max(iris$Sepal.Length),
value = mean(iris$Sepal.Length)),
dt_output('edit rows but disable certain columns (editable = list(target = "row", disable = list(columns = c(2, 4, 5))))', 'x10')
),
server = function(input, output, session) {
d1 = read_csv("~/desktop/iris.test") #my file path; you may need to edit for reproducibility
d1$Date = Sys.time() + seq_len(nrow(d1))
d2 = reactive({
d1 %>%
dplyr::filter(Sepal.Length > input$slide)
})
options(DT.options = list(pageLength = 5))
output$x10 = render_dt(d2(), list(target = 'cell', disable = list(columns = c(2, 4, 5))))
# edit rows but disable columns 2, 4, 5
observeEvent(input$x10_cell_edit, {
d3 <<- editData(d2(), input$x10_cell_edit, 'x10')
write_csv(d3, "~/desktop/iris.test") #my file path; you may need to edit for reproducibility
})
}
)
You may use reactiveValues to save your original dataset and change the values in it.
Create a row number column to make it easier to change values on the correct row.
library(shiny)
library(DT)
library(tidyverse)
write_csv(iris, "iris.test.csv")
dt_output = function(title, id) {
fluidRow(column(
12, h1(paste0('Table ', sub('.*?([0-9]+)$', '\\1', id), ': ', title)),
hr(), DTOutput(id)
))
}
render_dt = function(data, editable = 'cell', server = TRUE, ...) {
renderDT(data, selection = 'none', server = server, editable = editable, ...)
}
shinyApp(
ui = fluidPage(
title = 'Double-click to edit table cells',
sliderInput("slide",
"Slider",
min = min(iris$Sepal.Length),
max = max(iris$Sepal.Length),
value = mean(iris$Sepal.Length)),
dt_output('edit rows but disable certain columns (editable = list(target = "row", disable = list(columns = c(2, 4, 5))))', 'x10')
),
server = function(input, output, session) {
d1 = read_csv("iris.test.csv") %>%
mutate(row = row_number()) %>% as.data.frame()
d1$Date = Sys.time() + seq_len(nrow(d1))
rv <- reactiveValues(data = d1)
d2 = reactive({
d1 %>%
dplyr::filter(Sepal.Length > input$slide)
})
options(DT.options = list(pageLength = 5))
output$x10 = render_dt(d2(), list(target = 'cell', disable = list(columns = c(2, 4, 5))))
# edit rows but disable columns 2, 4, 5
observeEvent(input$x10_cell_edit, {
tmp <- d2()
row <- tmp$row[input$x10_cell_edit$row]
rv$data[row, input$x10_cell_edit$col] <- input$x10_cell_edit$value
write_csv(rv$data, "iris.test.csv")
})
}
)

Hide a column in shiny datatable but keep it searchable

The DT package in Shiny produces a table with a searchbar that searches over every column in the table. I have a column of metadata which I do not want to display in the table, but I still want the rows to come up if I search with the search bar.
For example, the app below contains a column titled searchCol . This column is just letters. I want to hide this column in the actual table, and I want to be able to search for the letter b , using the DT search bar, and have the second row show up.
Is there a way to hide the column but have it still work with the search bar?
library(shiny)
library(DT)
ui <- fluidPage(
DTOutput('tbl1'),
)
server <- function(input, output, session) {
output$tbl1 <- DT::renderDT(server = TRUE, {
datatable(
cbind(data.frame(replicate(3,sample(0:1,26,rep=TRUE))), data.frame(searchCol = letters)),
escape = FALSE,
rownames = FALSE,
filter = list(position = "top", clear = FALSE, plain = TRUE),
selection = "single",
options = list(
autoWidth = TRUE,
pageLength = 50,
lengthMenu = c(50, 100, 1000),
dom = 'Blfrtip',
buttons = c('copy', 'excel')
)
)
})
}
shinyApp(ui, server)
I've adapted the answer from here to the format you need to use in DT::datatable. You can use columnDefs to define the render options for the different columns, targets defines which column you mean. Please note that the JS library datatables starts counting columns at 0.
library(shiny)
library(DT)
ui <- fluidPage(
DTOutput('tbl1'),
)
server <- function(input, output, session) {
output$tbl1 <- DT::renderDT(server = TRUE, {
datatable(
cbind(data.frame(replicate(3,sample(0:1,26,rep=TRUE))), data.frame(searchCol = letters)),
escape = FALSE,
rownames = FALSE,
filter = list(position = "top", clear = FALSE, plain = TRUE),
selection = "single",
options = list(
autoWidth = TRUE,
pageLength = 50,
lengthMenu = c(50, 100, 1000),
dom = 'Blfrtip',
buttons = c('copy', 'excel'),
columnDefs = list(
list(
targets = 3,
searchable = TRUE,
visible = FALSE
)
)
)
)
})
}
shinyApp(ui, server)

Shiny: Using selections from timevis to highlight rows in data table

I am building a shiny app with a timeline and a data table. What I would like to have happen is when the user clicks on an item in the timeline, the corresponding data in the table is highlighted.
I have come up with a solution for this, but it seems very hacky and R is giving me warning messages. Basically what I have done is created a flag in the data table that is 1 if that item is selected and 0 if it's not, then I format the row based on that flag. When I create the "selected" field, I get a warning because initially nothing is selected and mutate doesn't like the fact that input$timeline_selected is NULL. Also for some reason when I try to add the rownames = FALSE argument to datatable all the data in the table is filtered out (not sure what is happening there).
Anyway, I'm wondering if there is a better way to do this perhaps with HTML or CSS. I've tried looking, but I can't figure out how to do it.
Eventually I would also like to know how to highlight the rows in the data table if the user hovers over the item in the timeline rather than selects it.
library(shiny)
library(DT)
library(dplyr)
dataBasic <- data.frame(
id = 1:4,
content = c("Item one", "Item two" ,"Ranged item", "Item four"),
start = c("2016-01-10", "2016-01-11", "2016-01-20", "2016-02-14"),
end = c(NA, NA, "2016-02-04", NA)
)
ui <- fluidPage(
column(wellPanel(timevisOutput("timeline")
), width = 7
),
column(wellPanel(dataTableOutput(outputId = "table")
), width = 5)
)
server <- function(input, output){
# Create timeline
output$timeline <- renderTimevis({
config <- list(
orientation = "top",
multiselect = TRUE
)
timevis(dataBasic, options = config)
})
output$table <- DT::renderDataTable({
input$timeline_data %>%
mutate(selected = if_else(id %in% input$timeline_selected, 1, 0)) %>%
datatable(options = list(pageLength = 10,
columnDefs = list(list(targets = 5, visible = FALSE))
)
) %>%
formatStyle("selected", target = "row", backgroundColor = styleEqual(c(0, 1), c("transparent", "#0092FF"))
)
})
}
shinyApp(ui = ui, server = server)
Using Your Code
Your method certainly works -- it's similar to this answer. You could prevent some of the error messages by using if...else and a validation statment:
output$table <- DT::renderDataTable({
validate(need(!is.null(input$timeline_data), ""))
if(is.null(input$timeline_selected)) {
input$timeline_data %>%
datatable(
rownames = FALSE,
options = list(pageLength = 10))
} else {
input$timeline_data %>%
mutate(selected = if_else(id %in% input$timeline_selected, 1, 0)) %>%
datatable(rownames = FALSE,
options = list(pageLength = 10,
columnDefs = list(list(targets = 4, visible = FALSE))
)
) %>%
formatStyle("selected", target = "row", backgroundColor = styleEqual(c(0, 1), c("transparent", "#0092FF"))
)
}
})
I believe your issue with adding rownames = FALSE is because columnDefs uses JS indexing instead of R indexing. R indexes start at 1, whereas JS indexes start at 0.
When rownames = TRUE, your table has column indexes 0-5, where rownames is column 0 and selected is the column 5. So columnDefs works. However, when rownames = FALSE, you only have column indexes 0-4, so targets = 5 is outside the index range of your table. If you change your code to targets = 4, then you will again be specifying the selected column in columnDefs.
Other Options
Here's two other options using JS:
Generate the table on the server-side, as based on this answer. This may be a better option for large data objects.
Generate the table on the client-side as based on this answer. With a smaller object, this seems to update more smoothly.
An example app with both tables is below.
Example Code
library(shiny)
library(DT)
library(dplyr)
library(timevis)
dataBasic <- data.frame(
id = 1:4,
content = c("Item one", "Item two" ,"Ranged item", "Item four"),
start = c("2016-01-10", "2016-01-11", "2016-01-20", "2016-02-14"),
end = c(NA, NA, "2016-02-04", NA)
)
ui <- fluidPage(
column(wellPanel(timevisOutput("timeline")
), width = 7
),
column(
wellPanel(
h3("Client-Side Table"),
DT::dataTableOutput("client_table"),
h3("Server-Side Table"),
DT::dataTableOutput("server_table")
), width = 5)
)
server <- function(input, output, session){
# Create timeline
output$timeline <- renderTimevis({
config <- list(
orientation = "top",
multiselect = TRUE
)
timevis(dataBasic, options = config)
})
## client-side ##
# based on: https://stackoverflow.com/a/42165876/8099834
output$client_table <- DT::renderDataTable({
# if timeline has been selected, add JS drawcallback to datatable
# otherwise, just return the datatable
if(!is.null(input$timeline_selected)) {
# subtract one: JS starts index at 0, but R starts index at 1
index <- as.numeric(input$timeline_selected) - 1
js <- paste0("function(row, data) {
$(this
.api()
.row(", index, ")
.node())
.css({'background-color': 'lightblue'});}")
datatable(dataBasic,
rownames = FALSE,
options = list(pageLength = 10,
drawCallback=JS(js)))
} else {
datatable(dataBasic,
rownames = FALSE,
options = list(pageLength = 10))
}
}, server = FALSE)
## server-side ##
# based on: https://stackoverflow.com/a/49176615/8099834
output$server_table <- DT::renderDataTable({
# create the datatable
dt <- datatable(dataBasic,
rownames = FALSE,
options = list(pageLength = 10))
# if timeline has been selected, add row background colors with formatstyle
if(!is.null(input$timeline_selected)) {
index <- as.numeric(input$timeline_selected)
background <- JS(paste0("value == '",
index,
"' ? 'lightblue' : value != 'else' ? 'white' : ''"))
dt <- dt %>%
formatStyle(
'id',
target = 'row',
backgroundColor = background)
}
# return the datatable
dt
})
}
shinyApp(ui = ui, server = server)

Resources