jsplyr is a JavaScript backend for
dplyr. Instead of manipulating
data on the Shiny server, it pushes the work to the browser, where it
runs on JSON data client-side. This keeps data wrangling fast and
responsive even for large data.frames, while letting you write the
familiar dplyr verbs you already know.
jsplyris still in early stages of development. To check whichdplyrverbs are supported check the reference section.
Install the released version from CRAN:
install.packages("jsplyr")Or install the development version from GitHub:
# install.packages("pak")
pak::pak("r-world-devs/jsplyr")First, in UI you need to include_jsplyr() (sources Javascript
code).
Second, in server part you need to call copy_to() to register your
JSON data for further manipulation with jsplyr. This works similar to
dbplyr::copy_to() where you pass database connection as an input. The
difference is that you do not create a specific connection as in
dbplyr, you just make use of shiny session (which represents a
connection with the web browser).
jsplyr takes into account two cases:
- Your JSON data is already defined in the browser with
JavaScript.
var mtcars = [{"mpg":21,"cyl":6,"disp":160,"hp":110,"drat":3.9,"wt":2.62,"qsec":16.46,"vs":0,"am":1,"gear":4,"carb":4,"_row":"Mazda RX4"},{"mpg":21,"cyl":6,"disp":160,"hp":110,"drat":3.9,"wt":2.875,"qsec":17.02,"vs":0,"am":1,"gear":4,"carb":4,"_row":"Mazda RX4 Wag"},{"mpg":22.8,"cyl":4,"disp":108,"hp":93,"drat":3.85,"wt":2.32,"qsec":18.61,"vs":1,"am":1,"gear":4,"carb":1,"_row":"Datsun 710"},{"mpg":21.4,"cyl":6,"disp":258,"hp":110,"drat":3.08,"wt":3.215,"qsec":19.44,"vs":1,"am":0,"gear":3,"carb":1,"_row":"Hornet 4 Drive"},{"mpg":18.7,"cyl":8,"disp":360,"hp":175,"drat":3.15,"wt":3.44,"qsec":17.02,"vs":0,"am":0,"gear":3,"carb":2,"_row":"Hornet Sportabout"},{"mpg":18.1,"cyl":6,"disp":225,"hp":105,"drat":2.76,"wt":3.46,"qsec":20.22,"vs":1,"am":0,"gear":3,"carb":1,"_row":"Valiant"},{"mpg":14.3,"cyl":8,"disp":360,"hp":245,"drat":3.21,"wt":3.57,"qsec":15.84,"vs":0,"am":0,"gear":3,"carb":4,"_row":"Duster 360"},{"mpg":24.4,"cyl":4,"disp":146.7,"hp":62,"drat":3.69,"wt":3.19,"qsec":20,"vs":1,"am":0,"gear":4,"carb":2,"_row":"Merc 240D"},{"mpg":22.8,"cyl":4,"disp":140.8,"hp":95,"drat":3.92,"wt":3.15,"qsec":22.9,"vs":1,"am":0,"gear":4,"carb":2,"_row":"Merc 230"},{"mpg":19.2,"cyl":6,"disp":167.6,"hp":123,"drat":3.92,"wt":3.44,"qsec":18.3,"vs":1,"am":0,"gear":4,"carb":4,"_row":"Merc 280"},{"mpg":17.8,"cyl":6,"disp":167.6,"hp":123,"drat":3.92,"wt":3.44,"qsec":18.9,"vs":1,"am":0,"gear":4,"carb":4,"_row":"Merc 280C"},{"mpg":16.4,"cyl":8,"disp":275.8,"hp":180,"drat":3.07,"wt":4.07,"qsec":17.4,"vs":0,"am":0,"gear":3,"carb":3,"_row":"Merc 450SE"},{"mpg":17.3,"cyl":8,"disp":275.8,"hp":180,"drat":3.07,"wt":3.73,"qsec":17.6,"vs":0,"am":0,"gear":3,"carb":3,"_row":"Merc 450SL"},{"mpg":15.2,"cyl":8,"disp":275.8,"hp":180,"drat":3.07,"wt":3.78,"qsec":18,"vs":0,"am":0,"gear":3,"carb":3,"_row":"Merc 450SLC"},{"mpg":10.4,"cyl":8,"disp":472,"hp":205,"drat":2.93,"wt":5.25,"qsec":17.98,"vs":0,"am":0,"gear":3,"carb":4,"_row":"Cadillac Fleetwood"},{"mpg":10.4,"cyl":8,"disp":460,"hp":215,"drat":3,"wt":5.424,"qsec":17.82,"vs":0,"am":0,"gear":3,"carb":4,"_row":"Lincoln Continental"},{"mpg":14.7,"cyl":8,"disp":440,"hp":230,"drat":3.23,"wt":5.345,"qsec":17.42,"vs":0,"am":0,"gear":3,"carb":4,"_row":"Chrysler Imperial"},{"mpg":32.4,"cyl":4,"disp":78.7,"hp":66,"drat":4.08,"wt":2.2,"qsec":19.47,"vs":1,"am":1,"gear":4,"carb":1,"_row":"Fiat 128"},{"mpg":30.4,"cyl":4,"disp":75.7,"hp":52,"drat":4.93,"wt":1.615,"qsec":18.52,"vs":1,"am":1,"gear":4,"carb":2,"_row":"Honda Civic"},{"mpg":33.9,"cyl":4,"disp":71.1,"hp":65,"drat":4.22,"wt":1.835,"qsec":19.9,"vs":1,"am":1,"gear":4,"carb":1,"_row":"Toyota Corolla"},{"mpg":21.5,"cyl":4,"disp":120.1,"hp":97,"drat":3.7,"wt":2.465,"qsec":20.01,"vs":1,"am":0,"gear":3,"carb":1,"_row":"Toyota Corona"},{"mpg":15.5,"cyl":8,"disp":318,"hp":150,"drat":2.76,"wt":3.52,"qsec":16.87,"vs":0,"am":0,"gear":3,"carb":2,"_row":"Dodge Challenger"},{"mpg":15.2,"cyl":8,"disp":304,"hp":150,"drat":3.15,"wt":3.435,"qsec":17.3,"vs":0,"am":0,"gear":3,"carb":2,"_row":"AMC Javelin"},{"mpg":13.3,"cyl":8,"disp":350,"hp":245,"drat":3.73,"wt":3.84,"qsec":15.41,"vs":0,"am":0,"gear":3,"carb":4,"_row":"Camaro Z28"},{"mpg":19.2,"cyl":8,"disp":400,"hp":175,"drat":3.08,"wt":3.845,"qsec":17.05,"vs":0,"am":0,"gear":3,"carb":2,"_row":"Pontiac Firebird"},{"mpg":27.3,"cyl":4,"disp":79,"hp":66,"drat":4.08,"wt":1.935,"qsec":18.9,"vs":1,"am":1,"gear":4,"carb":1,"_row":"Fiat X1-9"},{"mpg":26,"cyl":4,"disp":120.3,"hp":91,"drat":4.43,"wt":2.14,"qsec":16.7,"vs":0,"am":1,"gear":5,"carb":2,"_row":"Porsche 914-2"},{"mpg":30.4,"cyl":4,"disp":95.1,"hp":113,"drat":3.77,"wt":1.513,"qsec":16.9,"vs":1,"am":1,"gear":5,"carb":2,"_row":"Lotus Europa"},{"mpg":15.8,"cyl":8,"disp":351,"hp":264,"drat":4.22,"wt":3.17,"qsec":14.5,"vs":0,"am":1,"gear":5,"carb":4,"_row":"Ford Pantera L"},{"mpg":19.7,"cyl":6,"disp":145,"hp":175,"drat":3.62,"wt":2.77,"qsec":15.5,"vs":0,"am":1,"gear":5,"carb":6,"_row":"Ferrari Dino"},{"mpg":15,"cyl":8,"disp":301,"hp":335,"drat":3.54,"wt":3.57,"qsec":14.6,"vs":0,"am":1,"gear":5,"carb":8,"_row":"Maserati Bora"},{"mpg":21.4,"cyl":4,"disp":121,"hp":109,"drat":4.11,"wt":2.78,"qsec":18.6,"vs":1,"am":1,"gear":4,"carb":2,"_row":"Volvo 142E"}]t
In this case you simply pass the name used in JavaScript to
copy_to().
lazy_mtcars <- shiny::reactive({
dplyr::copy_to(dest = session, df = "mtcars")
})- Your data is a
data.frameloaded in a server.
In that case you pass the data.frame object to the function in order
to send it to the browser.
lazy_mtcars <- shiny::reactive({
dplyr::copy_to(dest = session, df = mtcars)
})Keep in mind that jsplyr works in a reactive context.
Once you have copied your JSON, you can manipulate it with the supported
verbs. jsplyr, similar to dbplyr, creates a lazy representation of
your data. Calling these verbs simply registers the next computation
steps (like queries in dbplyr) without triggering any computation in
the browser.
lazy_mtcars_query <- shiny::reactive({
lazy_mtcars() |>
dplyr::filter(mpg >= input$filter_mpg) |>
dplyr::select(input$select_columns) |>
dplyr::distinct()
})To retrieve the data from the browser back to the server you call
collect(). This triggers the computation in the browser and returns
the result. (There is also a compute() step under the hood that runs
the registered steps; you do not need to call it yourself — collect()
runs it for you.)
output$mtcars_tb <- shiny::renderDT({
lazy_mtcars_query() |>
dplyr::collect()
})You will find an example application in the inst/example_apps folder.
Putting the pieces together, here is a minimal Shiny app that copies a
data.frame to the browser, filters and selects it lazily based on user
input, and renders the collected result. All the data manipulation
happens in the browser.
library(shiny)
library(jsplyr)
ui <- fluidPage(
include_jsplyr(),
titlePanel("jsplyr example"),
sidebarLayout(
sidebarPanel(
numericInput("min_mpg", "Minimum mpg", value = 20),
selectInput(
"columns",
"Columns",
choices = names(mtcars),
selected = c("mpg", "cyl", "hp"),
multiple = TRUE
)
),
mainPanel(
DT::DTOutput("table")
)
)
)
server <- function(input, output, session) {
lazy_mtcars <- reactive({
dplyr::copy_to(dest = session, df = mtcars)
})
output$table <- DT::renderDT({
lazy_mtcars() |>
dplyr::filter(mpg >= input$min_mpg) |>
dplyr::select(input$columns) |>
dplyr::collect()
})
}
shinyApp(ui, server)collect() returns a promise,
because the result is fetched asynchronously from the browser. Reactive
outputs such as renderDT() resolve promises for you. In other reactive
contexts — reactive(), eventReactive(), observeEvent() and
observe() — you must handle the promise yourself (with
promises::then() or the re-exported %...>% pipe). See
vignette("collect-with-promises") for the details.
