glin / reactable

Interactive data tables for R
https://glin.github.io/reactable
Other
611 stars 79 forks source link

Server Side Rendering #22

Open Sbirch556 opened 4 years ago

Sbirch556 commented 4 years ago

Possible to implement some type of server side rendering similar to data.table R package does? Below example takes some time to render.

library(shiny)
library(reactable)

ui <- fluidPage(

  titlePanel("Reactable"),
  reactableOutput("table")

  )

server <- function(input, output, session) {

  data <- reactive({
    f <- data.frame(Sepal.Length=rep(iris$Sepal.Length,100),
                    Sepal.Width=rep(iris$Sepal.Width,100),
                    Petal.Length=rep(iris$Petal.Length,100),
                    Petal.Width=rep(iris$Petal.Width,100),
                    Species=rep(iris$Species,100),
                    stringsAsFactors = FALSE)
    return(f)
  })

  output$table <- renderReactable({

    reactable(data(),
              filterable = TRUE,
              sortable = TRUE,
              resizable = TRUE,
              onClick = "expand",
              highlight = TRUE,
              paginationType = "jump",
              details = colDef(
              name = "More",
              details = function(index){
              tabsetPanel(
                tabPanel("Data",data()[index,])
              )
            })
          )
    })
  }

shinyApp(ui, server)
glin commented 4 years ago

Yes, I'd like to implement server-side rendering eventually, but it'll probably be a while before I get there. For now, I'd definitely recommend using DT for large tables in Shiny apps.

For this example, it looks like most of the time comes from rendering the row details. So as a workaround, I think it'll be faster to use a JavaScript render function if possible, or avoid Shiny/htmltools tags. HTML tag rendering seems to be pretty slow at the moment (which I also noticed in https://github.com/glin/reactable/issues/17).

GitHunter0 commented 3 years ago

The server-side rendering would be indeed a fantastic addition to reactable

timelyportfolio commented 2 years ago

This is a good example with react-table 7. I might have a need for server-side rendering, and if so then will add code and discussion.

gofford commented 2 years ago

Did you make any progress with this @timelyportfolio ? Server-side rendering is sorely missed in reactable, and really limits scalability.

GitHunter0 commented 2 years ago

Hey @glin , have you seen this awesome react-base-table project ? It looks promising. There is also glide-data-grid, the project which Streamlit based on its new dataframe display.

daeyoonlee commented 1 year ago

One vote for the server side rendering feature. That's why some tables still use DT. glide-data-grid This looks really good too.

avraam-inside commented 1 year ago

Hello, plus topic. Now a lot of pain comes from rewriting the project on a WEB datatable, which is VERY raw and unpleasant in web layout (problems are just at every step), unlike reactable.

But server-side optimization solved it. I really want to see server-side in reactable...

P.S.: By the way, the datatable has well-implemented modifications to the painting of each cell - https://rstudio.github.io/DT/functions.html. They work pretty fast, there is enough functionality in general. I would like something similar for reactable, because looping through each cell in R is quite slow.

glin commented 1 year ago

Quick update: I'm working on this now, thanks to support from Posit giving me some dedicated time to work on this.

There's currently experimental support for server-side data in the latest development version. If you want to try it out, you can install the latest dev version from GitHub, then install the V8 package (required for server-side data but not a hard dependency):

devtools::install_github("glin/reactable")

install.packages("V8")

And then set reactable(server = TRUE) to enable the experimental server-side support in Shiny:

library(shiny)
library(reactable)

tbl <- reactable(
  mtcars,
  server = TRUE
)

ui <- fluidPage(
  reactableOutput("tbl")
)

server <- function(input, output) {
  output$tbl <- renderReactable({
    tbl
  })
}

shinyApp(ui, server)

Most features are implemented, but there's still some remaining work to do, including:


Going into more detail: the default server-side backend is running JavaScript under the hood (via V8), pretty much the exact same code that runs in your browser for client-side tables. It's similar to the recent Static Rendering feature, except the table is server-rendered every time, not just for the initial page.

Running JavaScript on the server not only allows for major code reuse (i.e. easier maintenance, features always work the same in client-side and server-side mode), but also big performance gains compared to an R implementation based on data.frames. Data manipulation in JavaScript/V8 is just so fast by default, especially for sorting and filtering. And the JavaScript library already has a bunch of memoization/caching built-in via React, so we get all of that for free. (For example, paginating a sorted/filtered table won't recalculate the sorting/filtering).

There are still some drawbacks with this approach though, like very large datasets may run up against V8's 4 GB heap limit and crash, or take a noticeable few seconds to initialize. But I think it'll work well for most use cases, with custom swappable backends covering everything else.

If you want to test the performance, here's the 100k rows example converted to a Shiny app. Running locally on my system, most operations take <200 ms at most (sorting <200ms, searching <50ms, pagination <10ms). You can change rows to 1 million and the timing may still be reasonable (roughly the 100k rows timing multiplied by 10 for me). I think you could go up to 2 million rows before hitting the V8 heap limit, but at that point, it probably makes sense to switch to a backend that doesn't read all data into memory.

library(shiny)
library(reactable)

rows <- 100000
dates <- seq.Date(as.Date("2018-01-01"), as.Date("2018-12-01"), "day")
data <- data.frame(
  index = seq_len(rows),
  date = sample(dates, rows, replace = TRUE),
  city = sample(names(precip), rows, replace = TRUE),
  state = sample(rownames(USArrests), rows, replace = TRUE),
  temp = round(runif(rows, 0, 100), 1),
  stringsAsFactors = FALSE
)

tbl <- reactable(
  data,
  filterable = TRUE,
  searchable = TRUE,
  minRows = 10,
  highlight = TRUE,
  columns = list(
    state = colDef(
      html = TRUE,
      cell = JS("function(cell) {
        return '<a href=\"https://wikipedia.org/wiki/' + cell.value + '\">' + cell.value + '</a>'
      }")
    )
  ),
  details = colDef(
    html = TRUE,
    details = JS("function(rowInfo) {
      return 'Details for row: ' + rowInfo.index +
        '<pre>' + JSON.stringify(rowInfo.row, null, 2) + '</pre>'
    }")
  ),
  showPageSizeOptions = TRUE,
  server = TRUE
)

ui <- fluidPage(
  reactableOutput("tbl")
)

server <- function(input, output) {
  output$tbl <- renderReactable({
    tbl
  })
}

shinyApp(ui, server)
GitHunter0 commented 1 year ago

This is fantastic news, we appreciate a lot.

Will it allow infinite vertical scroll instead of pagination (which is the other feature I most miss)?

BilboBaagins commented 1 year ago

Hey Greg - thanks so much for creating/maintaining the reactable package, I use it so much.

I came across this post as similar to OP Sbirch556, I have a reactable in an R Shiny dashboard with an R function for custom rendering expandable row details. I actually copied the R function from the CRAN Packages demo and have added a dynamically generated button for each record to export to PNG using the shinyscreenshot package - so it's kind of heavy on R functions and HTML.

Initially, I was rendering a data source that only ever had a couple hundred records, max. However, I have since added a larger data source that can range from a few hundred to c.50k records, depending on the user's input (which forms an SQL query to fetch data for the reactable).

I currently have logic prior to creating the reactable with fetched data to determine if(nrow(data) > 1000) then use a JavaScript render function for the expandable row details, otherwise stick with the R function. It would be ideal if I could keep the R function with formatted styling and actionButton when nrow(data) <= 10000) or higher.

Your latest dev seems promising that perhaps I can use the R render function on server-side.

As a test, I downloaded the dev version of reactable and the V8 package.

devtools::install_github("glin/reactable")

install.packages("V8")

I then replaced the JavaScript render function in the example you gave above with the OP's R render function. Note: I also reduced the number of row to ten thousand.

library(shiny)
library(reactable)

#rows <- 100000
rows <- 10000
dates <- seq.Date(as.Date("2018-01-01"), as.Date("2018-12-01"), "day")
data <- data.frame(
  index = seq_len(rows),
  date = sample(dates, rows, replace = TRUE),
  city = sample(names(precip), rows, replace = TRUE),
  state = sample(rownames(USArrests), rows, replace = TRUE),
  temp = round(runif(rows, 0, 100), 1),
  stringsAsFactors = FALSE
)

then <- Sys.time()

tbl <- reactable(
  data,
  filterable = TRUE,
  searchable = TRUE,
  minRows = 10,
  highlight = TRUE,
  columns = list(
    state = colDef(
      html = TRUE,
      cell = JS("function(cell) {
        return '<a href=\"https://wikipedia.org/wiki/' + cell.value + '\">' + cell.value + '</a>'
      }")
    )
  ),
  details = function(index){

    # Print index to console
    print(paste0("index: ", index))

    tabsetPanel(
      tabPanel("Data", data[index,])
    )

  },
  showPageSizeOptions = TRUE,
  server = TRUE
)

now <- Sys.time()
elapsed <- now - then
print(paste0("elapsed:", elapsed)) #"elapsed:39.0062990188599"

ui <- fluidPage(
  reactableOutput("tbl")
)

server <- function(input, output) {
  output$tbl <- renderReactable({
    tbl
  })
}

shinyApp(ui, server)

The time to load the shiny app and render the reactable is still really long. I note that it takes about 40 seconds to churn through the R function itself and create the reactable. It then takes another 30-odd seconds to render the table on the shiny app. This is all run locally on my machine.

My two questions are:

  1. Is my interpretation correct - can you reduce the time to generate and render the reactable with an R function for expandable rows? If so, have I missed something or is this functionality not available yet?

  2. Have you come across any examples of a JavaScript render function version of your CRAN Packages demo? My lack of JS really letting me down here... I need to up my game with it!

Thanks again for a brilliant package!

glin commented 1 year ago

@GitHunter0 It won't, and virtualized scrolling is a completely separate feature from server-side data support. But if virtualized scrolling is implemented in the feature, then it would theoretically "just work "with server-side data. You can follow this existing issue for table virtualization: https://github.com/glin/reactable/issues/203

This is fantastic news, we appreciate a lot.

Will it allow infinite vertical scroll instead of pagination (which is the other feature I most miss)?

glin commented 1 year ago

@BilboBaagins The current implementation does nothing to help that use case yet, and all R render functions are still run for the entire table up front. However, it is on the list of remaining things to do, and I'm still thinking about how it would work. There are some cases where you might want to precalculate all custom rendered content up front (because doing it on demand would be slow), and there are some cases where you would want to do it per page, and I'm not really sure how to accommodate both in a nice way.

For huge tables, I would recommend going JavaScript render functions all the way if you can though, as those would be both lazy-run and efficient if all it does it generate HTML from table data.

And unfortunately no, I haven't seen any JavaScript versions of the CRAN Packages demo, and I don't think it would be very easy because of the embedded R htmlwidget in there. The Popular Movies example does have some examples of generating complicated HTML from JavaScript that may be useful though.

Drwhit commented 6 months ago

I am seeing that when I set server=TRUE, it is taking 3 seconds longer to render my table. I get this warning when I load my app package:

Warning message: replacing previous import ‘V8::JS’ by ‘reactable::JS’ when loading ‘brokerAI’

Would that prevent server-side rendering?

gadepallivs commented 3 months ago

@glin Thank you for the package. My data has 4 K rows and will increase over time. My reactable is slowing down using shiny::icon, tags ( a, div, and span ) on multiple columns that add color, or an icon , or hyperlink, and bar chart. When I try the examples, from the suggested custom javascript to speed up link, the JS example code render a blank page, but the R render works. Do I need to install any packages to JS to work ?

> sessionInfo()
R version 4.3.2 (2023-10-31)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Sonoma 14.3

Matrix products: default
BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/New_York
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] plotly_4.10.4          tippy_0.1.0            reactable.extras_0.2.0
 [4] reactable_0.4.4        lubridate_1.9.3        forcats_1.0.0         
 [7] stringr_1.5.1          readr_2.1.5            tidyr_1.3.1           
[10] tibble_3.2.1           tidyverse_2.0.0        thematic_0.1.5        
[13] bsicons_0.1.2          htmltools_0.5.7        shinyWidgets_0.8.2    
[16] DT_0.32                dplyr_1.1.4            ggplot2_3.5.0         
[19] purrr_1.0.2            bslib_0.6.1            shinyjqui_0.4.1       
[22] bs4Dash_2.3.3          shiny_1.8.0           

loaded via a namespace (and not attached):
 [1] gtable_0.3.4          xfun_0.42             shinyjs_2.1.0         htmlwidgets_1.6.4    
 [5] processx_3.8.3        lattice_0.22-5        REDCapR_1.1.0         callr_3.7.5          
 [9] tzdb_0.4.0            crosstalk_1.2.1       ps_1.7.6              vctrs_0.6.5          
[13] tools_4.3.2           generics_0.1.3        curl_5.2.1            fansi_1.0.6          
[17] pkgconfig_2.0.3       checkmate_2.3.1       data.table_1.15.2     lifecycle_1.0.4      
[21] compiler_4.3.2        textshaping_0.3.7     munsell_0.5.0         fontawesome_0.5.2    
[25] httpuv_1.6.14         sass_0.4.8            lazyeval_0.2.2        yaml_2.3.8           
[29] crayon_1.5.2          later_1.3.2           pillar_1.9.0          jquerylib_0.1.4      
[33] ellipsis_0.3.2        cachem_1.0.8          mime_0.12             tidyselect_1.2.0     
[37] digest_0.6.34         stringi_1.8.3         rprojroot_2.0.4       fastmap_1.1.1        
[41] grid_4.3.2            here_1.0.1            colorspace_2.1-0      cli_3.6.2            
[45] magrittr_2.0.3        utf8_1.2.4            reactR_0.5.0          withr_3.0.0          
[49] backports_1.4.1       scales_1.3.0          promises_1.2.1        timechange_0.3.0     
[53] httr_1.4.7            config_0.3.2          ragg_1.2.7            hms_1.1.3            
[57] memoise_2.0.1         knitr_1.45            shinycssloaders_1.0.0 viridisLite_0.4.2    
[61] rlang_1.1.3           Rcpp_1.0.12           xtable_1.8-4          glue_1.7.0           
[65] renv_1.0.5            rstudioapi_0.15.0     jsonlite_1.8.8        R6_2.5.1             
[69] fs_1.6.3              systemfonts_1.0.5  
jzadra commented 3 months ago

+1 for server side! Though I'm going to give the dev a try.

jwijffels commented 2 months ago

I've been testing server-side rendering to see if I can implement my own S3 object based on some changes to https://github.com/glin/reactable/blob/main/R/server-df.R. I noticed however that when using the groupby feature, namely with the Reactable.toggleGroupBy the groupby column is empty, unless I use the v8 backend instead of the df backend. I'm a bit lost in the javascript at https://github.com/glin/reactable/blob/main/srcjs/useGroupBy.js where this is happening to avoid that it does make the group-by column empty in order to make a pull request. What exactly is an option to show the group text.

image

library(reactable)
library(shiny)
library(V8)

ui <- fluidPage(
    reactableOutput("table")
)
server <- function(input, output, session) {
    output$table <- renderReactable({
        reactable(iris, 
                  columns = list(
                      Species = colDef(header = function(name) {
                          tagList(
                               name,
                               tags$button(
                                 onclick = sprintf("event.stopPropagation(); Reactable.toggleGroupBy('%s', 'Species')", "table"),
                                 style = "margin-left: 0.5rem;",
                                 "Group by"
                               )
                             )
                           },
                          grouped = JS("function(cellInfo, state) {
                            return cellInfo.value
                          }"),
                          aggregated = JS("function(cellInfo) {
                            return cellInfo.value
                          }")),
                      Sepal.Length = colDef(aggregate = "mean"),
                      Sepal.Width = colDef(aggregate = "mean"),
                      Petal.Length = colDef(aggregate = "mean"),
                      Petal.Width = colDef(aggregate = "mean")
                  ),
                  server = "df")
    })
}

shinyApp(ui, server)

edit, when inspecting a bit the v8 backend and the structure it generates, and comparing what the df backend has, it looks to me that the df backend needs the following in order to make it work for one level of groupby to be put here: https://github.com/glin/reactable/blob/main/R/server-df.R#L142

df[["__state"]] <- listSafeDataFrame(id = sapply(df[[groupedColumnId]], FUN = function(x) sprintf("%s:%s", groupedColumnId, x)), grouped = sapply(df[[groupedColumnId]], FUN = function(x) TRUE))

jwijffels commented 2 months ago

Not sure if I'm hijacking this thread but as this is all related to server side-rendering which is currently experimental. Let me add that these stopping conditions also need to be removed https://github.com/glin/reactable/blob/main/R/columns.R#L164-L168 in order to allow custom server-side aggregations. In my local tests, I'm now changing your code at https://github.com/glin/reactable/blob/main/R/server-df.R to be able use a custom frequency function instead of the default.

aggregateFuncs <- list(
  "sum" = function(x) sum(x, na.rm = TRUE),
  ...
  "unique" = function(x) paste(na.exclude(unique(x)), collapse = ", "),
  "frequency" = function(x) {
    counts <- as.list(table(x))
    countStrs <- vapply(seq_along(counts), function(i) {
      value <- names(counts)[i]
      count <- counts[[i]]
      sprintf("%s (%s)", value, count)
    }, character(1))
    paste(countStrs, collapse = ", ")
  }
)
aggregateFuncs$frequency <- function(x) {
  round(100 * sum(x %in% "YES") / sum(!is.na(x)), digits = 0)
}