Skip to content

Commit 703d51a

Browse files
committed
Update test examples and document virtualized windowed fetching
- Make DuckDB test Rmd chunks self-contained (inline data generation) - Add Parquet + virtual scrolling test example - Update virtual scrolling test to use backend API - Document pre-render jank with virtual + pagination=FALSE - Add virtualized windowed fetching as future Phase 5
1 parent b8fca1a commit 703d51a

5 files changed

Lines changed: 80 additions & 24 deletions

File tree

design/duckdb-wasm-engine/duckdb-wasm-engine-test.Rmd

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ reactable(
131131
### Large dataset: 1M rows (virtualized)
132132

133133
```{r}
134+
n <- 1000000
135+
big_data <- data.frame(
136+
id = seq_len(n),
137+
value = rnorm(n),
138+
category = sample(LETTERS, n, replace = TRUE),
139+
score = round(runif(n, 0, 100), 2)
140+
)
141+
134142
reactable(
135143
big_data,
136144
backend = backendDuckDB(),
@@ -547,6 +555,15 @@ reactable(
547555
### Large multi-level grouping: 100k rows grouped by category and region
548556

549557
```{r}
558+
n <- 100000
559+
large_data <- data.frame(
560+
id = seq_len(n),
561+
category = sample(LETTERS, n, replace = TRUE),
562+
region = sample(c("East", "West", "North", "South"), n, replace = TRUE),
563+
value = round(rnorm(n, mean = 100, sd = 50), 2),
564+
score = round(runif(n, 0, 100), 2)
565+
)
566+
550567
reactable(
551568
large_data,
552569
backend = backendDuckDB(),
@@ -890,6 +907,15 @@ reactable(
890907
Force embedded Arrow IPC even for large data.
891908

892909
```{r}
910+
n <- 1000000
911+
parquet_data <- data.frame(
912+
id = seq_len(n),
913+
value = rnorm(n),
914+
category = sample(LETTERS, n, replace = TRUE),
915+
score = round(runif(n, 0, 100), 2),
916+
label = paste0("item-", seq_len(n))
917+
)
918+
893919
reactable(
894920
parquet_data,
895921
backend = backendDuckDB(format = "arrow"),
@@ -899,6 +925,33 @@ reactable(
899925
)
900926
```
901927

928+
### Parquet with virtual scrolling
929+
930+
Unpaginated virtual scrolling with Parquet sidecar. DuckDB sends all rows in a single
931+
query (pageSize = null), and virtual scrolling handles rendering.
932+
933+
```{r}
934+
n <- 1000000
935+
parquet_data <- data.frame(
936+
id = seq_len(n),
937+
value = rnorm(n),
938+
category = sample(LETTERS, n, replace = TRUE),
939+
score = round(runif(n, 0, 100), 2),
940+
label = paste0("item-", seq_len(n))
941+
)
942+
943+
reactable(
944+
parquet_data,
945+
backend = backendDuckDB(format = "parquet"),
946+
pagination = FALSE,
947+
virtual = TRUE,
948+
height = 500,
949+
sortable = TRUE,
950+
filterable = TRUE,
951+
searchable = TRUE
952+
)
953+
```
954+
902955
### Parquet in Shiny (client mode)
903956

904957
Test that Parquet sidecar files work in a Shiny app. In Shiny, `backendDuckDB()` defaults

design/duckdb-wasm-engine/duckdb-wasm-engine.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,10 @@ and memory than it saves on queries.
10991099
- **Debounce tuning:** The POC debounces search input at 300ms. For large datasets, increasing the debounce or
11001100
requiring a minimum query length (3+ chars) would reduce perceived lag.
11011101

1102+
### Future: virtualized windowed fetching
1103+
1104+
With `virtual = TRUE, pagination = FALSE`, DuckDB currently fetches all rows at once (`pageSize: null` omits LIMIT/OFFSET). For Parquet, this means downloading the entire file over HTTP before the table renders. A future enhancement would use scroll-position-driven queries to fetch only a sliding window of rows around the viewport, leveraging Parquet HTTP range requests for efficient partial reads. See Phase 5 in `design/server-side-data/server-side-data.md` for the full plan.
1105+
11021106
## End-to-end benchmark: DuckDB vs default backend
11031107

11041108
Measured in Chrome (Windows), serving rendered R Markdown documents over HTTP. Both documents use the same dataset

design/server-side-data/server-side-data.md

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -133,37 +133,22 @@ For multi-level grouping, nested data frames contain their own `.subRows`.
133133
### 1. Bug Fixes and Polish
134134

135135
#### 1.1 Documentation Typo
136-
**File:** `man/reactable-server.Rd` line 52-53
137136

138-
Current text incorrectly says:
139-
```
140-
- `reactableServerData()` should return a `resolvedData()` object.
141-
- `reactableServerData()` should not return any value.
142-
```
137+
~~**File:** `man/reactable-server.Rd` line 52-53~~
143138

144-
Should be:
145-
```
146-
- `reactableServerData()` should return a `resolvedData()` object.
147-
- `reactableServerInit()` should not return any value.
148-
```
139+
**Done.** The Rd already correctly says `reactableServerInit()` should not return any value.
149140

150141
#### 1.2 df Backend groupBy Bug
151-
**File:** `R/server-df.R`
152142

153-
When `Reactable.toggleGroupBy()` is called via JavaScript API, the df backend returns grouped rows without the `__state` property needed for proper row identification. The V8 backend handles this correctly.
143+
~~**File:** `R/server-df.R`~~
154144

155-
**Fix:** In `dfGroupBy()`, add state information to grouped rows:
156-
```r
157-
df[["__state"]] <- listSafeDataFrame(
158-
id = sapply(df[[groupedColumnId]], function(x) sprintf("%s:%s", groupedColumnId, x)),
159-
grouped = rep(TRUE, nrow(df))
160-
)
161-
```
145+
**Done.** `dfGroupBy()` now adds `__state` with `id`, `grouped`, and `subRowCount` to grouped rows.
162146

163147
#### 1.3 Pagination Display with Empty Results
164-
**File:** `srcjs/Reactable.js`
165148

166-
When server-side search returns zero results, pagination shows "1-10 of 0 rows" instead of "0-0 of 0 rows".
149+
~~**File:** `srcjs/Reactable.js`~~
150+
151+
**Done.** `Pagination.js` uses `Math.min(page * pageSize + 1, rowCount)` to correctly show "0-0 of 0 rows" when `rowCount = 0`.
167152

168153
#### 1.4 Stop Sending Unused State
169154
**File:** `srcjs/Reactable.js`
@@ -197,6 +182,8 @@ When server-side search returns zero results, pagination shows "1-10 of 0 rows"
197182

198183
**Future simplification: consider removing pre-rendered first page.** The R-side pre-rendering of the first page (to avoid a blank flash while WASM loads) adds significant JS complexity: `canSkipInitialDuckDBQuery`, `duckdbQueryCount`, `stateMatchesPrerender` comparison against `defaultSorted`, the groupBy special case (pre-rendered data is flat so we must query immediately), and race conditions when users interact before DuckDB is ready. Without pre-rendering, the query effect fires unconditionally after init and the entire skip optimization disappears. The tradeoff is showing a loading/empty state during WASM init instead of instant first-page display.
199184

185+
Pre-rendering is also problematic with **virtual scrolling + `pagination = FALSE`**: the pre-rendered `defaultPageSize` rows (e.g., 10) display immediately, then several seconds later the full dataset loads from DuckDB and the table jumps to show all rows. This creates a jarring partial-load effect. Deferring table readiness until all client-side data is fetched (showing a loading indicator instead of the partial pre-render) would give a smoother experience for this combination.
186+
200187
Another issue: **floating point precision mismatch** between the two data paths. The pre-rendered page goes through `jsonlite::toJSON(digits = NA)` which uses C's `%.15g` format (15 significant digits), while DuckDB query results come through Arrow's `row.toJSON()` which uses JavaScript's `Number.toString()` (up to 17 significant digits for exact float64 round-trip). Since 15 significant digits isn't always enough to recover the exact float64 value, numbers with many decimal places can visibly change when the user first interacts and DuckDB takes over from the pre-rendered data. This is unsolvable without either (a) increasing jsonlite's digits to 17 for exact round-trip, (b) rounding DuckDB results to 15 significant digits to match jsonlite, or (c) removing pre-rendering so there's only one data path.
201188

202189
**Option B: Full server-side implementation (future)**
@@ -419,6 +406,16 @@ reactableServerData.duckdb_backend <- function(
419406
- Document current limitation first
420407
- Full implementation if user demand warrants
421408

409+
5. **Phase 5: Virtualized windowed fetching** (future)
410+
- Enable `virtual = TRUE, pagination = FALSE` with DuckDB/Parquet without loading all rows at once
411+
- Watch `virtualizer.range` (debounced) to detect when visible rows change
412+
- Fire DuckDB queries with `LIMIT bufferSize OFFSET scrollPosition` for a sliding window (~500 rows centered on viewport)
413+
- Maintain a sparse data array of length `totalRowCount` with placeholder objects for unfetched rows
414+
- Show loading skeleton/shimmer for placeholder rows while data is in-flight
415+
- Invalidate entire buffer on sort/filter/search and re-fetch from current scroll position
416+
- Key benefit for Parquet: HTTP range requests mean DuckDB reads only the byte ranges needed, not the full file
417+
- This is bidirectional infinite scroll -- the main complexity is buffer management and debouncing queries during fast scrolling
418+
422419
## Verification
423420

424421
### Manual Testing

design/virtual-scrolling/virtual-scrolling-test.Rmd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ server <- function(input, output) {
228228
229229
reactable(
230230
data,
231-
server = TRUE,
231+
backend = backendV8(),
232232
virtual = TRUE,
233233
height = 500,
234234
defaultPageSize = 1000,
@@ -276,7 +276,7 @@ Hypothetical API:
276276
reactable(
277277
# Column schema only, no data
278278
data.frame(id = integer(), value = numeric(), category = character()),
279-
server = TRUE,
279+
backend = backendV8(),
280280
virtual = TRUE,
281281
pagination = FALSE, # No pagination - seamless scrolling
282282
height = 500,

pkgdown/_pkgdown.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ reference:
9090
contents:
9191
- reactable-server
9292
- resolvedData
93+
- backendDf
9394
- backendDuckDB
95+
- backendV8
9496

9597
# Exclude duplicate no-static examples from search index
9698
search:

0 commit comments

Comments
 (0)