Перемещайте строки из одного ОД в другие с помощью кнопок действий в R Shiny

ОБНОВЛЕНИЕ

Я пытаюсь создать приложение, используя shiny и DT, аналогично принятому ответу Шри здесь. Я бы хотел, чтобы к нему были следующие дополнения:

  1. Расширьте решение от Shree, чтобы элементы из DT слева (источник) можно было перемещать более чем в одну таблицу справа и обратно и быть расширяемыми, чтобы я мог решить, сколько таблиц я хочу разместить справа . То есть разные предметы из таблицы слева могут помещаться в другую таблицу справа.
  2. Кроме того, чтобы рядом с каждой таблицей справа были кнопки с двойными стрелками, чтобы можно было добавлять или удалять все элементы в таблице, щелкнув кнопки с двойной стрелкой, а не только кнопки с одной стрелкой для перемещения только выбранных переменных, как здесь, но при этом вы можете решить, отображать их или нет.
  3. Таблицы справа должны быть видны, даже если они пусты.

Может ли кто-нибудь помочь с этим?


person panman    schedule 07.07.2020    source источник
comment
В общем, было бы хорошо, если бы вы нам показали то, что пробовали до сих пор. Также облегчает получение того, чего вы хотите достичь. Для 1) я не уверен, хотите ли вы показать один и тот же вывод для всех n таблиц справа или вы даже хотите иметь возможность выбирать подмножество из n таблиц и только добавлять к ним строки. для 2) и 3) я добавил ответ.   -  person Tonio Liebrand    schedule 13.07.2020
comment
@Tonio Liebrand: Извините, вы правы, я не понял для №1. Я имел в виду, что разные элементы из таблицы слева переходят в разные таблицы справа. Я отредактировал вопрос.   -  person panman    schedule 13.07.2020


Ответы (3)


Как уже упоминалось, блестящие модули - это элегантный способ решить эту проблему. Вы должны передать несколько reactives для получения строк, и вы должны вернуть несколько reactives для отправки строк / сообщить основной таблице, что она должна удалить только что отправленные строки.

Полностью рабочий пример выглядит следующим образом:

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)

Объяснение

Модуль содержит пользовательский интерфейс и сервер, и благодаря методам размещения имен имена должны быть уникальными только в пределах одного модуля (и каждый модуль позже должен иметь также уникальное имя). Модуль может взаимодействовать с основным приложением через reactives, которые либо передаются в callModule (обратите внимание, что я все еще использую старые функции, поскольку я еще не обновил свою блестящую библиотеку), либо которые возвращаются из функции сервера.

В основном приложении у нас есть кнопка, которая динамически вставляет пользовательский интерфейс и вызывает callModule для активации логики. observers также генерируются в том же вызове, чтобы заставить работать логику сервера.

person thothal    schedule 16.07.2020
comment
Большое тебе спасибо! Решение весьма впечатляющее. Еще один вопрос / просьба. Можно ли это обобщить, чтобы количество таблиц справа передавалось в качестве аргумента функции модуля, чтобы модуль можно было повторно использовать несколько раз (например, на разных вкладках в приложении) вместо кнопки «Добавить таблицу». - person panman; 16.07.2020
comment
Что ж, модуль можно вызывать откуда угодно. Вам не нужно добавлять его динамически. В любом пользовательском интерфейсе вы также можете просто вызвать функцию пользовательского интерфейса напрямую. например fluidPage(receiver_ui("my_fixed_ui", "odd")) также будет работать. Поэтому, если вы хотите иметь несколько таблиц, просто используйте своего рода цикл, например fluidPage(lapply(1:4, function(i) receiver_ui(paste0("row",i), "odd")) - person thothal; 17.07.2020
comment
Спасибо. Я попробовал предложения в вашем комментарии, но у меня появляются пустые синие поля с кнопками вверху страницы. Извините, я новичок в Shiny и (особенно) модули для меня волшебство. Вы можете изменить решение? - person panman; 17.07.2020
comment
Конечно, вам также необходимо определить функцию сервера. Вы прочитали статью по ссылке? - person thothal; 20.07.2020

Чтобы обобщить произвольное количество таблиц, я бы использовал модуль. Модуль будет содержать графический интерфейс и логику для одного DT. У него будут аргументы для входного DT (таблицы, из которой принимаются строки) и выходного DT (таблицы, в которую отправляются строки). Любой из них или оба могут быть NULL. В графическом интерфейсе пользователя будет отображаться DT и будут виджеты для запуска различных команд отправки строк. Дополнительные сведения о модулях см. здесь.

Что касается вашей неспособности удалить строки из исходной таблицы: я не слишком хорошо знаком с DT, но считаю, что вам нужно использовать прокси: as на этой странице говорится, что после того, как таблица была отрисована в приложении Shiny, вы можете использовать прокси-объект, возвращенный из dataTableProxy(), для управления ею. В настоящее время поддерживаются следующие методы: selectRows(), selectColumns(), selectCells(), selectPage() и addRow() ..

person Limey    schedule 08.07.2020

Чтобы получить кнопки с двойными стрелками, вы можете использовать:

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, чтобы гарантировать, что datatable отображается, даже если он пуст. Это не очень элегантно, поскольку в нем есть пустая строка. Для этого могут быть лучшие способы. Например. если вы добавите строку и снова отмените выбор, чтобы таблица была пустой, отображается 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)

Что касается требований к нескольким таблицам, см. Мой комментарий.

person Tonio Liebrand    schedule 13.07.2020
comment
Спасибо за объяснение и решение. Что касается требования к нескольким таблицам, я обновил вопрос. Надеюсь, теперь стало понятнее - перемещайте разные предметы слева на разные столы справа. - person panman; 14.07.2020
comment
Тонио? Есть новости о нескольких столах справа? - person panman; 16.07.2020
comment
я работал над этим, но не хватило времени. Добавление модулей и наличие динамической памяти для каждой из этих таблиц - это больше работы, но я думаю, что у вас уже есть хороший ответ, верно? - person Tonio Liebrand; 17.07.2020