rstudio / gt

Easily generate information-rich, publication-quality tables from R
https://gt.rstudio.com
Other
1.99k stars 201 forks source link

shiny get selected row from GT #354

Open jkr216 opened 5 years ago

jkr216 commented 5 years ago

It would be great if GT could be used in Shiny as way for reactive inputs. That is, a user clicks on a row in a GT and passes data from that row as a reactive input. Datatable has this capability. When user clicks a row in a datatable, we can access the selected row with: input$tableId_rows_selected see here: https://stackoverflow.com/questions/28274584/get-selected-row-from-datatable-in-shiny-app

Thanks very much!

rich-iannone commented 5 years ago

Thanks Jonathan, I'm pretty excited about this feature—all Shiny-related things for gt tend to excite me—but I think it will take some time before I could get to this.

AlbertRapp commented 1 year ago

Since v.0.9.0 you can create interactive tables with {gt}. Under the hood this uses {reactable}. Hence, it's pretty easy to piggyback off the underlying {reactable} engine. This requires to modify the internal render_as_ihtml() function so that it does not set selection = NULL when it calls reactable::reactable().

Here's a proposal. First, modify the render_as_ihtml(data, id) function:

``` my_render_as_ithml <- function (data, id, selection = NULL) { data <- gt:::build_data(data = data, context = "html") data <- gt:::add_css_styles(data = data) source_notes <- gt:::dt_source_notes_get(data = data) footnotes <- gt:::dt_footnotes_get(data = data) has_footer_section <- !is.null(source_notes) || nrow(footnotes) > 1 has_header_section <- gt:::dt_heading_has_title(data = data) locale <- gt:::dt_locale_get_value(data = data) if (is.null(locale)) { lang <- "en" } else { lang <- gsub("(.)?_.*", "\\1", locale) } data_tbl <- gt:::dt_data_get(data = data) data_tbl_vars <- gt:::dt_boxhead_get_vars_default(data = data) data_tbl <- data_tbl[, data_tbl_vars, drop = FALSE] if (ncol(data_tbl) < 1) { cli::cli_abort(c("When displaying an interactive gt table, there must be at least one visible column.", `*` = "Check that the input data table has at least one column,", `*` = "Failing that, look at whether all columns have been inadvertently hidden.")) } column_names <- gt:::dt_boxhead_get_vars_default(data = data) column_labels <- gt:::dt_boxhead_get_vars_labels_default(data = data) column_alignments <- gt:::dt_boxhead_get_vars_align_default(data = data) boxh <- gt:::dt_boxhead_get(data = data) column_widths <- dplyr::filter(boxh, type %in% c("default", "stub")) column_widths <- dplyr::pull(dplyr::arrange(column_widths, dplyr::desc(type)), column_width) column_widths <- unlist(column_widths) if (!is.null(column_widths)) { column_widths <- vapply(column_widths, FUN.VALUE = integer(1), USE.NAMES = FALSE, FUN = function(x) { if (grepl("px", x)) { x <- as.integer(gsub("px", "", x)) } else { x <- NA_integer_ } x }) } opt_val <- gt:::dt_options_get_value use_pagination <- opt_val(data = data, option = "ihtml_use_pagination") use_pagination_info <- opt_val(data = data, option = "ihtml_use_pagination_info") use_search <- opt_val(data = data, option = "ihtml_use_search") use_sorting <- opt_val(data = data, option = "ihtml_use_sorting") use_filters <- opt_val(data = data, option = "ihtml_use_filters") use_resizers <- opt_val(data = data, option = "ihtml_use_resizers") use_highlight <- opt_val(data = data, option = "ihtml_use_highlight") use_compact_mode <- opt_val(data = data, option = "ihtml_use_compact_mode") use_text_wrapping <- opt_val(data = data, option = "ihtml_use_text_wrapping") use_page_size_select <- opt_val(data = data, option = "ihtml_use_page_size_select") page_size_default <- opt_val(data = data, option = "ihtml_page_size_default") page_size_values <- opt_val(data = data, option = "ihtml_page_size_values") pagination_type <- opt_val(data = data, option = "ihtml_pagination_type") use_row_striping <- opt_val(data = data, option = "row_striping_include_table_body") row_striping_color <- opt_val(data = data, option = "row_striping_background_color") table_width <- opt_val(data = data, option = "table_width") table_background_color <- opt_val(data = data, option = "table_background_color") table_font_names <- opt_val(data = data, option = "table_font_names") table_font_color <- opt_val(data = data, option = "table_font_color") column_labels_border_top_style <- opt_val(data = data, option = "column_labels_border_top_style") column_labels_border_top_width <- opt_val(data = data, option = "column_labels_border_top_width") column_labels_border_top_color <- opt_val(data = data, option = "column_labels_border_top_color") column_labels_border_bottom_style <- opt_val(data = data, option = "column_labels_border_bottom_style") column_labels_border_bottom_width <- opt_val(data = data, option = "column_labels_border_bottom_width") column_labels_border_bottom_color <- opt_val(data = data, option = "column_labels_border_bottom_color") emoji_symbol_fonts <- c("Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji") table_font_names <- base::setdiff(table_font_names, emoji_symbol_fonts) font_family_str <- gt:::as_css_font_family_attr(font_vec = table_font_names, value_only = TRUE) if (table_width == "auto") table_width <- NULL col_defs <- lapply(seq_along(column_names), FUN = function(x) { formatted_cells <- extract_cells(data = data, columns = column_names[x]) reactable::colDef(cell = function(value, index) formatted_cells[index], name = column_labels[x], align = column_alignments[x], headerStyle = list(`font-weight` = "normal"), width = if (is.null(column_widths) || is.na(column_widths[x])) NULL else column_widths[x], html = TRUE) }) names(col_defs) <- column_names styles_tbl <- gt:::dt_styles_get(data = data) body_styles_tbl <- dplyr::filter(styles_tbl, locname %in% c("data", "stub")) body_styles_tbl <- dplyr::arrange(body_styles_tbl, colnum, rownum) body_styles_tbl <- dplyr::select(body_styles_tbl, colname, rownum, html_style) body_style_rules <- vapply(seq_len(nrow(body_styles_tbl)), FUN.VALUE = character(1), USE.NAMES = FALSE, FUN = function(x) { colname <- body_styles_tbl[x, ][["colname"]] rownum <- body_styles_tbl[x, ][["rownum"]] html_style <- body_styles_tbl[x, ][["html_style"]] html_style <- unlist(strsplit(html_style, "; ")) html_style <- gsub("(-)\\s*(.)", "\\U\\2", html_style, perl = TRUE) html_style <- gsub("(:)\\s*(.*)", ": '\\2'", html_style, perl = TRUE) html_style <- paste(html_style, collapse = ", ") html_style <- gsub(";'$", "'", html_style) paste0("if (colInfo.id === '", colname, "' & rowIndex === ", rownum, ") {\n", " return { ", html_style, " }\n", "}\n\n") }) body_style_rules <- paste(body_style_rules, collapse = "") body_style_js_str <- paste0("function(rowInfo, colInfo) {\n", "const rowIndex = rowInfo.index + 1\n", body_style_rules, "}", collapse = "") default_col_def <- reactable::colDef(style = reactable::JS(body_style_js_str)) if (has_header_section) { tbl_heading <- gt:::dt_heading_get(data = data) heading_component <- htmltools::div(style = htmltools::css(`font-family` = font_family_str, `border-top-style` = "solid", `border-top-width` = "2px", `border-top-color` = "#D3D3D3", `padding-bottom` = if (use_search) "8px" else NULL), htmltools::div(class = "gt_heading gt_title gt_font_normal", style = htmltools::css(`text-size` = "bigger"), htmltools::HTML(tbl_heading$title)), htmltools::div(class = paste("gt_heading", "gt_subtitle", if (use_search) "gt_bottom_border" else NULL), htmltools::HTML(tbl_heading$subtitle))) } else { heading_component <- NULL } if (has_footer_section) { if (!is.null(source_notes)) { source_notes_component <- create_source_notes_component_h(data = data) } else { source_notes_component <- NULL } if (!is.null(footnotes)) { footnotes_component <- create_footnotes_component_h(data = data) } else { footnotes_component <- NULL } footer_component <- htmltools::div(style = htmltools::css(`font-family` = font_family_str, `border-top-style` = "solid", `border-top-width` = "2px", `border-top-color` = "#D3D3D3", `border-bottom-style` = "solid", `border-bottom-width` = "2px", `border-bottom-color` = "#D3D3D3", `padding-top` = "6px", `padding-bottom` = "6px", `padding-left` = "10px", `padding-right` = "10px"), htmltools::div(source_notes_component), htmltools::div(footnotes_component)) } else { footer_component <- NULL } tbl_theme <- reactable::reactableTheme(color = table_font_color, backgroundColor = table_background_color, borderColor = NULL, borderWidth = NULL, stripedColor = row_striping_color, highlightColor = NULL, cellPadding = NULL, style = list(fontFamily = font_family_str), tableStyle = NULL, headerStyle = list(borderTopStyle = column_labels_border_top_style, borderTopWidth = column_labels_border_top_width, borderTopColor = column_labels_border_top_color, borderBottomStyle = column_labels_border_bottom_style, borderBottomWidth = column_labels_border_bottom_width, borderBottomColor = column_labels_border_bottom_color), groupHeaderStyle = NULL, tableBodyStyle = NULL, rowGroupStyle = NULL, rowStyle = NULL, rowStripedStyle = NULL, rowHighlightStyle = NULL, rowSelectedStyle = NULL, cellStyle = NULL, footerStyle = NULL, inputStyle = NULL, filterInputStyle = NULL, searchInputStyle = NULL, selectStyle = NULL, paginationStyle = NULL, pageButtonStyle = NULL, pageButtonHoverStyle = NULL, pageButtonActiveStyle = NULL, pageButtonCurrentStyle = NULL) x <- reactable::reactable(data = data_tbl, columns = col_defs, columnGroups = NULL, rownames = NULL, groupBy = NULL, sortable = use_sorting, resizable = use_resizers, filterable = use_filters, searchable = use_search, searchMethod = NULL, defaultColDef = default_col_def, defaultColGroup = NULL, defaultSortOrder = "asc", defaultSorted = NULL, pagination = use_pagination, defaultPageSize = page_size_default, showPageSizeOptions = use_page_size_select, pageSizeOptions = page_size_values, paginationType = pagination_type, showPagination = TRUE, showPageInfo = use_pagination_info, minRows = 1, paginateSubRows = FALSE, details = NULL, defaultExpanded = FALSE, selection = selection, selectionId = NULL, defaultSelected = NULL, onClick = NULL, highlight = use_highlight, outlined = FALSE, bordered = FALSE, borderless = FALSE, striped = use_row_striping, compact = use_compact_mode, wrap = use_text_wrapping, showSortIcon = TRUE, showSortable = TRUE, class = NULL, style = NULL, rowClass = NULL, rowStyle = NULL, fullWidth = TRUE, width = table_width, height = "auto", theme = tbl_theme, language = NULL, elementId = id, static = FALSE) if (!is.null(heading_component)) { x <- htmlwidgets::prependContent(x, heading_component) } if (!is.null(footer_component)) { x <- htmlwidgets::appendContent(x, footer_component) } x } ```

This gives us a function my_render_as_ihtml(data, id, selection) that is really just the original function where selection = NULL was replaced with selection = selection.

Since the output of this function is a {reactable} table, we can treat it as such in a dummy Shiny app.

library(shiny)
library(gt)

ui <- fluidPage(
  reactable::reactableOutput("table"),
  verbatimTextOutput('selected')
)

server <- function(input, output, session) {
  output$table <- reactable::renderReactable({
    gt::towny |> 
      dplyr::select(name, land_area_km2) |> 
      dplyr::slice(1:20) |> 
      gt::gt() |> 
      gt::tab_options(
        table.width = '500px', 
        container.width = '500px'
      )  |> 
      my_render_as_ithml(selection = 'multiple', id = 'bla')
  })

  output$selected <- renderText({
    selected <- reactable::getReactableState("table", "selected")
    req(selected)
    paste0('Selected rows: ', paste0(selected, collapse = ', '))
  })

}

shinyApp(ui, server)

image

EkremBayar commented 1 year ago

Hello @jkr216 , @rich-iannone and @AlbertRapp

I develop shiny apps and need a lot to use row selection on the table. DT and reactable packages allow us to do this. I use When I use DT package with simple data sets, there is no problem but, if your data sets are large and you will change table styles, it causes some format issues. Probably, you can handle the issues by using javascript, I think.

I tried reactable package to make nice tables. However, the rendering of the table is too slow. You have to use DT package for speed. I'd like to try gt package and see its speed but, it has no row selection attribute.

@AlbertRapp I couldn't run render_as_ihtml function. Could you share your working code succesfully?

@jkr216 and @rich-iannone I wonder your experiences, which package do you suggest to make nice format and fast rendering with row selection attribute.

This is my reactable code. Unfortunately, it is too slow :(

output$table <- renderReactable({

  if(is.null(rvList$DFFiltered)){
    df <- rvList$DFRaw
  }else{
    df <- rvList$DFFiltered
  }
  if(is.data.frame(df)){
    # https://stackoverflow.com/questions/71250726/conditional-formattingbackground-cell-multiple-columns-using-reactable-lib
    reactablecolumns <-
      df %>% rename_all(funs(str_to_title(gsub("[[:punct:]]", " ", .)))) %>%
      colnames() %>%
      set_names() %>%
      keep(~ .x %in% (str_to_title(gsub('[[:punct:] ]+',' ', make_clean_names(bormetriclist %>% pull(Attributes) %>% unique()))))) %>%
      map(~ {
        colDef(
          vAlign = "center",
          style = function(value) {
            colfunc <- colorRampPalette(c("red", "yellow", "#00A300"))
            colfunc <- colfunc(20)
            ds_color <- ifelse(value == 1, colfunc[1], NA)
            ds_color <- ifelse(value == 1.5, colfunc[2], ds_color)
            ds_color <- ifelse(value == 2, colfunc[3], ds_color)
            ds_color <- ifelse(value == 2.5, colfunc[4], ds_color)
            ds_color <- ifelse(value == 3, colfunc[5], ds_color)
            ds_color <- ifelse(value == 3.5, colfunc[6], ds_color)
            ds_color <- ifelse(value == 4, colfunc[7], ds_color)
            ds_color <- ifelse(value == 4.5, colfunc[8], ds_color)
            ds_color <- ifelse(value == 5, colfunc[9], ds_color)
            ds_color <- ifelse(value == 5.5, colfunc[10], ds_color)
            ds_color <- ifelse(value == 6, colfunc[12], ds_color)
            ds_color <- ifelse(value == 6.5, colfunc[13], ds_color)
            ds_color <- ifelse(value == 7, colfunc[14], ds_color)
            ds_color <- ifelse(value == 7.5, colfunc[15], ds_color)
            ds_color <- ifelse(value == 8, colfunc[16], ds_color)
            ds_color <- ifelse(value == 8.5, colfunc[17], ds_color)
            ds_color <- ifelse(value == 9, colfunc[18], ds_color)
            ds_color <- ifelse(value == 9.5, colfunc[19], ds_color)
            ds_color <- ifelse(value == 10, colfunc[20], ds_color)
            list(background = ds_color, fontWeight = "bold", color = "white", fontSize = 20)
          }
        )
      })

    reactablecolumns["Value"] = list("Value" = colDef(
      format = colFormat(currency = "EUR", separators = TRUE, digits = 0, locales = "de-DE"),
      minWidth = 110))
    reactablecolumns["Height"] = list(Height = colDef(format = colFormat(suffix = " cm")))
    reactablecolumns["Weight"] = list(Weight = colDef(format = colFormat(suffix = " kg")))
    reactablecolumns["Photo"] = list(Photo = colDef(minWidth = 60, html = TRUE,sticky = "left"))

    # Uzun yazılı karakter değişkenleri için: Tippy
    render.reactable.cell.with.tippy <- function(text, tooltip){
      div(
        # text-decoration: underline;
        #text-decoration-style: dotted;
        #text-decoration-color: #FF6B00;
        style = "
                cursor: info;
                caret-color: red;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;",
        tippy(text = text, tooltip = tooltip, theme = "light")
      )
    }
    # https://stackoverflow.com/questions/64591293/r-reactable-how-to-truncate-cell-content-and-display-upon-hovering
    reactablecolumns["Player"] = list(Player = colDef(
      cell =  function(value, index, name){render.reactable.cell.with.tippy(text = value, tooltip = value)},
      minWidth = 100,vAlign = "center", html = TRUE, sticky = "left",style = list(borderRight = "2px solid black")))
    reactablecolumns["Name"] = list("Name" = colDef(
      cell =  function(value, index, name){render.reactable.cell.with.tippy(text = value, tooltip = value)},
      minWidth = 100,vAlign = "center"))

    reactablecolumns["SId"] = list("Sb Id" = colDef(show = FALSE))
    reactablecolumns["Tmd"] = list("Tm Id" = colDef(show = FALSE))

    for(i in c("CName", "Name", "Agent",  "Team", "Nat", "TNat", "MRM")){
      reactablecolumns[i] = list(i = colDef(
        cell =  function(value, index, name){render.reactable.cell.with.tippy(text = value, tooltip = value)},
        minWidth = 100,vAlign = "center"))
    }

    # Reactable
    reactable(
      df %>%
        rename_all(funs(str_to_title(gsub("[[:punct:]]", " ", .)))),

      height = 750,
      defaultPageSize = 100, pagination = T,pageSizeOptions = c(50, 100, 150, 200), showPageInfo = TRUE,
      showPageSizeOptions = T, showPagination = T, paginationType = "numbers",
      bordered = TRUE, striped = TRUE, highlight = TRUE, compact = TRUE,
      sortable = TRUE, showSortable = TRUE, fullWidth = FALSE,  style = "z-index: 0; width:100%; font-size:78%;",
      selection = "single", onClick = "select",

      theme = reactableTheme(
        rowSelectedStyle = list(backgroundColor = "#C6E0B4", boxShadow = "inset 2px 0 0 0 #ffa62d")
      ),

      defaultColDef = colDef(
        headerVAlign = "center",vAlign = "center",
        header = function(value) gsub("_", " ", value, fixed = TRUE),
        #cell = function(value) format(value, nsmall = 1),
        align = "center",
        minWidth = 100,
        headerStyle = list(background = "#002749", color = "white")
      ),

      columns = reactablecolumns
    )
  }

})
rwaaijman commented 5 months ago

just wondering if this feature is already implemented? Or will it be implemented in the future (when using gt in shiny)

prappopo commented 4 days ago

Hello @AlbertRapp.

When I run your shiny App, R does not seem to know where to look for rstudio/gt internal functions, and so gives back an error. Could you tell me how to fix this?

I followed your recipe by copying the source code for render_as_ihtml from https://rdrr.io/github/rstudio/gt/src/R/render_as_i_html.R I modified the function() call to function(data, id,mysel), and modified the line "selection=NULL" to "selection=mysel", and saved the file as my_render_as_ihtml. Then I used this slightly modified version of your shiny code, which: --sources the my_render_as_ihtml file, and --has "my_render_as_ihtml(mysel = 'multiple', id = 'bla') as the last line of the output$table argument. (also corrected minor typo: ithml -->ihtml)

When I run the App, I get: Warning: Error in build_data: could not find function "build_data", which refers to the first line of the my_render_as_ihtml function:

data <- build_data(data = data, context = "html")

Similarly, if I replace my_render_as_ihtml(mysel = 'multiple', id = 'bla') with render_as_ihtml(id = 'bla') I get: Error in render_as_ihtml: could not find function "render_as_ihtml"

The code is not recognizing gt (internal) functions, although the gt library is successfully loaded ( "gt" %in% tolower(library()$results[,1]) returns TRUE).

Thanks for any suggestions

#

fname="my_render_as_ihtml" fpath=paste0("data","/",fname,".R") source(fpath)

library(shiny) library(gt)

ui <- fluidPage( reactable::reactableOutput("table"), verbatimTextOutput('selected') )

server <- function(input, output, session) { output$table <- reactable::renderReactable({ gt::towny |> dplyr::select(name, land_area_km2) |> dplyr::slice(1:20) |> gt::gt() |> gt::tab_options( table.width = '500px', container.width = '500px' ) |> my_render_as_ihtml(mysel = 'multiple', id = 'bla')

render_as_ihtml(id = 'bla')

})

output$selected <- renderText({ selected <- reactable::getReactableState("table", "selected") req(selected) paste0('Selected rows: ', paste0(selected, collapse = ', ')) })

}

shinyApp(ui, server)