使用 R Shiny 中的操作按钮将行从一个 DT 移动到其他 DT
Move rows from one DT to other DTs using action buttons in R Shiny
更新
我正在尝试使用 shiny
和 DT
、 制作应用程序。你,我想对它进行以下补充:
- 扩展 Shree 的解决方案,以便左侧
DT
中的项目(源)可以移动到右侧和后面的多个 table 并且是可扩展的,这样我可以决定在右边放多少个 table。也就是说,来自左侧 table 的不同项目可以放在右侧的不同 table 中。
- 此外,在右侧的每个 table 旁边都有双箭头按钮,这样 table 中的所有项目都可以通过单击双箭头按钮来添加或删除,而不是仅用于移动所选变量的单箭头按钮,like here,但仍然能够决定是否显示它们。
- 右侧的表格即使为空也可见。
有人可以帮忙吗?
要推广到任意数量的 table,我会使用一个模块。该模块将包含单个 DT
的 GUI 和逻辑。它将具有“输入 DT”(从中接收行的 table)和“输出 DT”(向其发送行的 table)的参数。一个或两个都可以是 NULL
。 GUI 将显示 DT
并有一个小部件来启动各种“发送行”命令。有关模块的更多详细信息,请参阅 here。
至于您无法从源中删除行 table:我对 DT
不是很熟悉,但我相信您需要使用代理:因为 this page说“在 Shiny 应用程序中呈现 table 后,您可以使用从 dataTableProxy()
返回的代理对象来操作它。目前支持的方法是 selectRows()
、selectColumns()
, selectCells()
、selectPage()
和 addRow()
.".
要获得双箭头按钮,您可以使用:
actionButton("add_all", label = NULL, icon("angle-double-right"),
lib = "font-awesome")
请注意 ?icon
链接到 fontawesome 页面,该页面提供双箭头图标:https://fontawesome.com/icons?d=gallery&q=double%20arrow&m=free。
要删除所有项目,您只需切换到默认状态即可:
observeEvent(input$remove_all, {
mem$selected <- select_init
mem$pool <- pool_init
})
默认状态定义为:
pool_init <- data.frame(data = LETTERS[1:10])
select_init <- data.frame(data = "")
要添加所有行,您基本上只需切换状态即可:
mem$selected <- pool_init
mem$pool <- select_init
请注意,我使用(几乎)空的 data.frame 来确保显示数据 table,即使它是空的。这不是很优雅,因为它里面有一个空字符串。可能有更好的方法。例如。如果您添加一行并再次取消选择它,那么 table 为空,它会显示 No data available in table
。这实际上看起来更好。
完整的可重现示例:
library(shiny)
library(DT)
ui <- fluidPage(
br(),
splitLayout(cellWidths = c("40%", "10%", "40%", "10%"),
DTOutput("pool"),
list(
br(),br(),br(),br(),br(),br(),br(),
actionButton("add", label = NULL, icon("arrow-right")),
br(),br(),
actionButton("remove", label = NULL, icon("arrow-left"))
),
DTOutput("selected"),
list(
br(),br(),br(),br(),br(),br(),br(),
actionButton("add_all", label = NULL, icon("angle-double-right"),
lib = "font-awesome"),
br(),br(),
actionButton("remove_all", label = NULL, icon("angle-double-left"),
lib = "font-awesome")
)
)
)
pool_init <- data.frame(data = LETTERS[1:10])
select_init <- data.frame(data = "")
server <- function(input, output, session) {
mem <- reactiveValues(
pool = pool_init, selected = select_init
)
observeEvent(input$add, {
req(input$pool_rows_selected)
mem$selected <- rbind(isolate(mem$selected), mem$pool[input$pool_rows_selected, , drop = F])
mem$selected <- mem$selected[sapply(mem$selected, nchar) > 0, , drop = FALSE]
mem$pool <- isolate(mem$pool[-input$pool_rows_selected, , drop = F])
})
observeEvent(input$remove, {
req(input$selected_rows_selected)
mem$pool <- rbind(isolate(mem$pool), mem$selected[input$selected_rows_selected, , drop = F])
mem$pool <- mem$pool[sapply(mem$pool, nchar) > 0, , drop = FALSE]
mem$selected <- isolate(mem$selected[-input$selected_rows_selected, , drop = F])
})
observeEvent(input$add_all, {
mem$selected <- pool_init
mem$pool <- data.frame(data = "")
})
observeEvent(input$remove_all, {
mem$selected <- select_init
mem$pool <- pool_init
})
output$pool <- renderDT({
mem$pool
})
output$selected <- renderDT({
mem$selected
})
}
shinyApp(ui, server)
关于多个table的要求,请看我的评论。
如前所述,shiny modules 是解决此问题的一种优雅方法。你必须传递一些 reactives
来接收行,你必须 return 一些 reactives
来发送行/告诉主要 table 它应该删除它刚刚发送的行.
一个完整的示例如下所示:
library(shiny)
library(DT)
receiver_ui <- function(id, class) {
ns <- NS(id)
fluidRow(
column(width = 1,
actionButton(ns("add"),
label = NULL,
icon("angle-right")),
actionButton(ns("add_all"),
label = NULL,
icon("angle-double-right")),
actionButton(ns("remove"),
label = NULL,
icon("angle-left")),
actionButton(ns("remove_all"),
label = NULL,
icon("angle-double-left"))),
column(width = 11,
dataTableOutput(ns("sink_table"))),
class = class
)
}
receiver_server <- function(input, output, session, selected_rows, full_page, blueprint) {
## data_exch contains 2 data.frames:
## send: the data.frame which should be sent back to the source
## receive: the data which should be added to this display
data_exch <- reactiveValues(send = blueprint,
receive = blueprint)
## trigger_delete is used to signal the source to delete the rows whihc just were sent
trigger_delete <- reactiveValues(trigger = NULL, all = FALSE)
## render the table and remove .original_order, which is used to keep always the same order
output$sink_table <- renderDataTable({
dat <- data_exch$receive
dat$.original_order <- NULL
dat
})
## helper function to move selected rows from this display back
## to the source via data_exch
shift_rows <- function(selector) {
data_exch$send <- data_exch$receive[selector, , drop = FALSE]
data_exch$receive <- data_exch$receive[-selector, , drop = FALSE]
}
## helper function to add the relevant rows
add_rows <- function(all) {
rel_rows <- if(all) req(full_page()) else req(selected_rows())
data_exch$receive <- rbind(data_exch$receive, rel_rows)
data_exch$receive <- data_exch$receive[order(data_exch$receive$.original_order), ]
## trigger delete, such that the rows are deleted from the source
old_value <- trigger_delete$trigger
trigger_delete$trigger <- ifelse(is.null(old_value), 0, old_value) + 1
trigger_delete$all <- all
}
observeEvent(input$add, {
add_rows(FALSE)
})
observeEvent(input$add_all, {
add_rows(TRUE)
})
observeEvent(input$remove, {
shift_rows(req(input$sink_table_rows_selected))
})
observeEvent(input$remove_all, {
shift_rows(req(input$sink_table_rows_current))
})
## return the send reactive to signal the main app which rows to add back
## and the delete trigger to remove rows
list(send = reactive(data_exch$send),
delete = trigger_delete)
}
ui <- fluidPage(
tags$head(tags$style(HTML(".odd {background: #DDEBF7;}",
".even {background: #BDD7EE;}",
".btn-default {min-width:38.25px;}",
".row {padding-top: 15px;}"))),
fluidRow(
actionButton("add", "Add Table")
),
fluidRow(
column(width = 6, dataTableOutput("source_table")),
column(width = 6, div(id = "container")),
)
)
server <- function(input, output, session) {
orig_data <- mtcars
orig_data$.original_order <- seq(1, NROW(orig_data), 1)
my_data <- reactiveVal(orig_data)
handlers <- reactiveVal(list())
selected_rows <- reactive({
my_data()[req(input$source_table_rows_selected), , drop = FALSE]
})
all_rows <- reactive({
my_data()[req(input$source_table_rows_current), , drop = FALSE]
})
observeEvent(input$add, {
old_handles <- handlers()
n <- length(old_handles) + 1
uid <- paste0("row", n)
insertUI("#container", ui = receiver_ui(uid, ifelse(n %% 2, "odd", "even")))
new_handle <- callModule(
receiver_server,
uid,
selected_rows = selected_rows,
full_page = all_rows,
## select 0 rows data.frame to get the structure
blueprint = orig_data[0, ])
observeEvent(new_handle$delete$trigger, {
if (new_handle$delete$all) {
selection <- req(input$source_table_rows_current)
} else {
selection <- req(input$source_table_rows_selected)
}
my_data(my_data()[-selection, , drop = FALSE])
})
observe({
req(NROW(new_handle$send()) > 0)
dat <- rbind(isolate(my_data()), new_handle$send())
my_data(dat[order(dat$.original_order), ])
})
handlers(c(old_handles, setNames(list(new_handle), uid)))
})
output$source_table <- renderDataTable({
dat <- my_data()
dat$.original_order <- NULL
dat
})
}
shinyApp(ui, server)
说明
一个模块包含 UI 和服务器,由于命名空间技术,名称只需要在一个模块内是唯一的(并且每个模块以后也必须有一个唯一的名称)。该模块可以通过 reactives
与主应用程序通信,这些应用程序要么传递给 callModule
(请注意,我仍在使用旧功能,因为我尚未更新我的 shiny 库),或者 return来自服务器函数。
在主应用程序中,我们有一个按钮,它动态插入 UI 并调用 callModule
来激活逻辑。 observers
也在同一调用中生成以使服务器逻辑工作。
更新
我正在尝试使用 shiny
和 DT
、
- 扩展 Shree 的解决方案,以便左侧
DT
中的项目(源)可以移动到右侧和后面的多个 table 并且是可扩展的,这样我可以决定在右边放多少个 table。也就是说,来自左侧 table 的不同项目可以放在右侧的不同 table 中。 - 此外,在右侧的每个 table 旁边都有双箭头按钮,这样 table 中的所有项目都可以通过单击双箭头按钮来添加或删除,而不是仅用于移动所选变量的单箭头按钮,like here,但仍然能够决定是否显示它们。
- 右侧的表格即使为空也可见。
有人可以帮忙吗?
要推广到任意数量的 table,我会使用一个模块。该模块将包含单个 DT
的 GUI 和逻辑。它将具有“输入 DT”(从中接收行的 table)和“输出 DT”(向其发送行的 table)的参数。一个或两个都可以是 NULL
。 GUI 将显示 DT
并有一个小部件来启动各种“发送行”命令。有关模块的更多详细信息,请参阅 here。
至于您无法从源中删除行 table:我对 DT
不是很熟悉,但我相信您需要使用代理:因为 this page说“在 Shiny 应用程序中呈现 table 后,您可以使用从 dataTableProxy()
返回的代理对象来操作它。目前支持的方法是 selectRows()
、selectColumns()
, selectCells()
、selectPage()
和 addRow()
.".
要获得双箭头按钮,您可以使用:
actionButton("add_all", label = NULL, icon("angle-double-right"),
lib = "font-awesome")
请注意 ?icon
链接到 fontawesome 页面,该页面提供双箭头图标:https://fontawesome.com/icons?d=gallery&q=double%20arrow&m=free。
要删除所有项目,您只需切换到默认状态即可:
observeEvent(input$remove_all, {
mem$selected <- select_init
mem$pool <- pool_init
})
默认状态定义为:
pool_init <- data.frame(data = LETTERS[1:10])
select_init <- data.frame(data = "")
要添加所有行,您基本上只需切换状态即可:
mem$selected <- pool_init
mem$pool <- select_init
请注意,我使用(几乎)空的 data.frame 来确保显示数据 table,即使它是空的。这不是很优雅,因为它里面有一个空字符串。可能有更好的方法。例如。如果您添加一行并再次取消选择它,那么 table 为空,它会显示 No data available in table
。这实际上看起来更好。
完整的可重现示例:
library(shiny)
library(DT)
ui <- fluidPage(
br(),
splitLayout(cellWidths = c("40%", "10%", "40%", "10%"),
DTOutput("pool"),
list(
br(),br(),br(),br(),br(),br(),br(),
actionButton("add", label = NULL, icon("arrow-right")),
br(),br(),
actionButton("remove", label = NULL, icon("arrow-left"))
),
DTOutput("selected"),
list(
br(),br(),br(),br(),br(),br(),br(),
actionButton("add_all", label = NULL, icon("angle-double-right"),
lib = "font-awesome"),
br(),br(),
actionButton("remove_all", label = NULL, icon("angle-double-left"),
lib = "font-awesome")
)
)
)
pool_init <- data.frame(data = LETTERS[1:10])
select_init <- data.frame(data = "")
server <- function(input, output, session) {
mem <- reactiveValues(
pool = pool_init, selected = select_init
)
observeEvent(input$add, {
req(input$pool_rows_selected)
mem$selected <- rbind(isolate(mem$selected), mem$pool[input$pool_rows_selected, , drop = F])
mem$selected <- mem$selected[sapply(mem$selected, nchar) > 0, , drop = FALSE]
mem$pool <- isolate(mem$pool[-input$pool_rows_selected, , drop = F])
})
observeEvent(input$remove, {
req(input$selected_rows_selected)
mem$pool <- rbind(isolate(mem$pool), mem$selected[input$selected_rows_selected, , drop = F])
mem$pool <- mem$pool[sapply(mem$pool, nchar) > 0, , drop = FALSE]
mem$selected <- isolate(mem$selected[-input$selected_rows_selected, , drop = F])
})
observeEvent(input$add_all, {
mem$selected <- pool_init
mem$pool <- data.frame(data = "")
})
observeEvent(input$remove_all, {
mem$selected <- select_init
mem$pool <- pool_init
})
output$pool <- renderDT({
mem$pool
})
output$selected <- renderDT({
mem$selected
})
}
shinyApp(ui, server)
关于多个table的要求,请看我的评论。
如前所述,shiny modules 是解决此问题的一种优雅方法。你必须传递一些 reactives
来接收行,你必须 return 一些 reactives
来发送行/告诉主要 table 它应该删除它刚刚发送的行.
一个完整的示例如下所示:
library(shiny)
library(DT)
receiver_ui <- function(id, class) {
ns <- NS(id)
fluidRow(
column(width = 1,
actionButton(ns("add"),
label = NULL,
icon("angle-right")),
actionButton(ns("add_all"),
label = NULL,
icon("angle-double-right")),
actionButton(ns("remove"),
label = NULL,
icon("angle-left")),
actionButton(ns("remove_all"),
label = NULL,
icon("angle-double-left"))),
column(width = 11,
dataTableOutput(ns("sink_table"))),
class = class
)
}
receiver_server <- function(input, output, session, selected_rows, full_page, blueprint) {
## data_exch contains 2 data.frames:
## send: the data.frame which should be sent back to the source
## receive: the data which should be added to this display
data_exch <- reactiveValues(send = blueprint,
receive = blueprint)
## trigger_delete is used to signal the source to delete the rows whihc just were sent
trigger_delete <- reactiveValues(trigger = NULL, all = FALSE)
## render the table and remove .original_order, which is used to keep always the same order
output$sink_table <- renderDataTable({
dat <- data_exch$receive
dat$.original_order <- NULL
dat
})
## helper function to move selected rows from this display back
## to the source via data_exch
shift_rows <- function(selector) {
data_exch$send <- data_exch$receive[selector, , drop = FALSE]
data_exch$receive <- data_exch$receive[-selector, , drop = FALSE]
}
## helper function to add the relevant rows
add_rows <- function(all) {
rel_rows <- if(all) req(full_page()) else req(selected_rows())
data_exch$receive <- rbind(data_exch$receive, rel_rows)
data_exch$receive <- data_exch$receive[order(data_exch$receive$.original_order), ]
## trigger delete, such that the rows are deleted from the source
old_value <- trigger_delete$trigger
trigger_delete$trigger <- ifelse(is.null(old_value), 0, old_value) + 1
trigger_delete$all <- all
}
observeEvent(input$add, {
add_rows(FALSE)
})
observeEvent(input$add_all, {
add_rows(TRUE)
})
observeEvent(input$remove, {
shift_rows(req(input$sink_table_rows_selected))
})
observeEvent(input$remove_all, {
shift_rows(req(input$sink_table_rows_current))
})
## return the send reactive to signal the main app which rows to add back
## and the delete trigger to remove rows
list(send = reactive(data_exch$send),
delete = trigger_delete)
}
ui <- fluidPage(
tags$head(tags$style(HTML(".odd {background: #DDEBF7;}",
".even {background: #BDD7EE;}",
".btn-default {min-width:38.25px;}",
".row {padding-top: 15px;}"))),
fluidRow(
actionButton("add", "Add Table")
),
fluidRow(
column(width = 6, dataTableOutput("source_table")),
column(width = 6, div(id = "container")),
)
)
server <- function(input, output, session) {
orig_data <- mtcars
orig_data$.original_order <- seq(1, NROW(orig_data), 1)
my_data <- reactiveVal(orig_data)
handlers <- reactiveVal(list())
selected_rows <- reactive({
my_data()[req(input$source_table_rows_selected), , drop = FALSE]
})
all_rows <- reactive({
my_data()[req(input$source_table_rows_current), , drop = FALSE]
})
observeEvent(input$add, {
old_handles <- handlers()
n <- length(old_handles) + 1
uid <- paste0("row", n)
insertUI("#container", ui = receiver_ui(uid, ifelse(n %% 2, "odd", "even")))
new_handle <- callModule(
receiver_server,
uid,
selected_rows = selected_rows,
full_page = all_rows,
## select 0 rows data.frame to get the structure
blueprint = orig_data[0, ])
observeEvent(new_handle$delete$trigger, {
if (new_handle$delete$all) {
selection <- req(input$source_table_rows_current)
} else {
selection <- req(input$source_table_rows_selected)
}
my_data(my_data()[-selection, , drop = FALSE])
})
observe({
req(NROW(new_handle$send()) > 0)
dat <- rbind(isolate(my_data()), new_handle$send())
my_data(dat[order(dat$.original_order), ])
})
handlers(c(old_handles, setNames(list(new_handle), uid)))
})
output$source_table <- renderDataTable({
dat <- my_data()
dat$.original_order <- NULL
dat
})
}
shinyApp(ui, server)
说明
一个模块包含 UI 和服务器,由于命名空间技术,名称只需要在一个模块内是唯一的(并且每个模块以后也必须有一个唯一的名称)。该模块可以通过 reactives
与主应用程序通信,这些应用程序要么传递给 callModule
(请注意,我仍在使用旧功能,因为我尚未更新我的 shiny 库),或者 return来自服务器函数。
在主应用程序中,我们有一个按钮,它动态插入 UI 并调用 callModule
来激活逻辑。 observers
也在同一调用中生成以使服务器逻辑工作。