Repository navigation
Allow non-display of subjects from placebo arm - #175
LittleBeannie wants to merge 1 commit into
Conversation
prepare_ae_forestly, tests and new contents to…A checklist in the control bar (lt's LT.ui.chip + popover + checklist, in the el._lt.chips group) chooses which treatment arm(s) show in each expanded AE listing; the detail callback filters its records by the checked set and el._lt.resetDetail re-renders open details. The picker offers the arms present in the listing (not aggregate forest columns). Requires lt (>= 0.4.37). Close #174 Close #175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Heads-up before this goes further: in the lt migration (#169) this turns out to be much simpler on the JS side, with no new R argument.
The drill-down listing in #169 now carries an interactive treatment-group picker — a control-bar chip opening a checklist of the arms. The reader toggles which group(s) appear in the listing (e.g. uncheck Placebo) live, entirely client-side: unchecking an arm filters that arm's subjects out of the open listing and re-renders it, with nothing recomputed in R. That addresses #174 directly, for any arm, without threading a placebo-exclusion flag through prepare_ae_forestly() / ae_forestly().
So I think we can close this in favor of #169 rather than carrying the R-side exclusion.
The only advantage of the R-side exclusion is that the HTML file size will be smaller (depending on the proportion of placebo rows in the data), but since the size is significantly significantly smaller now with #169, I'm no longer worried about it.
A checklist in the control bar (lt's LT.ui.chip + popover + checklist, in the el._lt.chips group) chooses which treatment arm(s) show in each expanded AE listing; the detail callback filters its records by the checked set and el._lt.resetDetail re-renders open details. The picker offers the arms present in the listing (not aggregate forest columns). Requires lt (>= 0.4.37). Close #174 Close #175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Let's close my PR175 for now to avoid making the function getting complicated! |
* Migrate main AE forest table from reactable+plotly to lt (#168 follow-up) The main interactive AE forest table, its inline proportion dot plot, and its risk-difference error bar are now a single `lt` table (yihui/lt) made interactive with `lt_interactive()`, replacing the reactable core, the vendored Plotly figures, and the crosstalk slider/dropdown. The drill-down listings keep rendering lazily on expand (as in #168). R: - R/lt_table.R: new format_lt_forestly() builds the structural lt table (per-arm n/(%) columns under treatment-group spanners, lt_dotplot proportion figure, lt_errorbar risk-difference figure, column widths, hidden helper columns parameter/hide_prop/hide_n/CI bounds, and the show/hide menu spec). - R/format_ae_forestly.R: drop the reactable/plotly column specs; expose figure metadata (ranges, colors, headers, per-arm groupings, widths) for lt instead. - R/ae_forestly.R: layer lt_interactive() + native row-detail drill-down + the forestly controls on the structural table. Argument names unchanged (including the historically misspelled `dowload_button`). - R/html_dependency.R: drop plotly/crosstalk/search-filter deps; add the forestly-widgets JS+CSS dependency. - R/utils.R: carry %||%, get_label, assign_label (previously in reactable2.R). - Delete R/reactable2.R, R/search_filter.R, R/sparkline_point_js.R and the inst/js plotly/crosstalk/sparkline assets. Controls (inst/js/forestly-widgets.js, inst/css/forestly-widgets.css): - Dependency-free parameter dropdown, dual-thumb incidence slider, and CSV download that drive the table through lt's el._lt.filter() controller, filtering on the hidden helper columns. Deps: drop crosstalk, reactable, reactR; require lt (>= 0.4.21). Tests/docs: rewrite test-ae_forestly.R and test-format_ae_forestly.R for the lt output; add test-lt_table.R; delete test-sparkline_point_js.R and its snapshot; refresh design-pattern.Rmd and CLAUDE.md; add NEWS entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Unstagger the dot plot, truncate long cells, and fix the shifted plot axes - Draw the per-arm proportion dots on a single track instead of staggering them onto separate rows; the forest-plot vignettes don't jitter dots vertically. - Keep each AE table cell on one line, truncating overflow with an ellipsis rather than wrapping; hovering a cell reveals its full text. Figure cells (an SVG that must not be clipped) and the drill-down detail are exempt. - Fix the proportion dot plot and risk-difference error-bar axes being drawn one column to the right of their plots when the SOC column is offered as a hide/show toggle (display_soc_toggle = TRUE). The axes live in the table's plot-footer row, and lt's column-visibility menu did not hide a toggled-off column there, leaving a stray cell that shifted the axes. Fixed in lt and now requires lt (>= 0.4.22). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Rewire AE filters to lt typed filters; require lt (>= 0.4.23) Replace forestly's hand-built parameter dropdown and incidence slider with lt's declarative typed filters, declared through lt_interactive(filter = ...) in ae_forestly() and bound to the table's hidden helper columns (parameter, hide_prop/hide_n). lt renders them automatically (a funnel under a visible column's header, a chip in the control bar for a hidden column), so the corresponding bespoke HTML controls and their CSS/JS drop out. The CSV download remains the only forestly-owned widget, reading the table's current view via lt's el._lt contract. Bump the lt requirement to 0.4.23 (the typed-filter API) and refresh NEWS and CLAUDE.md to describe the new filter wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * I don't see a reason why not checking vignettes * Use lt's built-in CSV download; drop forestly's widget JavaScript lt now provides a CSV download button directly through lt_interactive(download = ...), so ae_forestly() no longer needs its own download control. Wire dowload_button to lt's download (named "ae-forest.csv"), and remove the bespoke button div, its JavaScript (inst/js/forestly-widgets.js), and its CSS. forestly now ships no widget JavaScript of its own; the forestly-widgets dependency carries only the table's cell-truncation styles. Bump the lt requirement to 0.4.28 (where download landed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Lead the AE table with the inline figures and tidy their rendering Reorder the main table so the proportion dot plot and risk-difference error bar come right after the term/SOC columns, ahead of the per-arm n/(%) and numeric difference columns, matching the forest-plot convention of showing the figures first. Stagger the dot plot's per-arm dots onto separate tracks (as the original forest plot did) so near-equal proportions stay distinguishable, and drop the error bar's color/legend when only a single comparison is drawn, since the 'vs. <ref>' spanner already names it. Extend the one-line/ellipsis cell styling to header cells too, so a long group label over a narrow n/(%) column no longer wraps into a tall header row, while figure (svg) cells stay exempt. * Guard figure-range computation against all-NA data When a filtered dataset yields no computable risk differences (or proportions) -- e.g. a comparison arm with no analysis subjects -- the sliced matrix passed to range() is all-NA. With na.rm = TRUE, range() then warns 'no non-missing arguments to min/max; returning Inf/-Inf' and returns a degenerate c(Inf, -Inf) range that breaks the dot-plot and error-bar axis scaling. Replace the four range() calls with a safe_range() helper that drops non-finite values and falls back to c(0, 0) when none remain, so the figures still render without warnings. * Guard per-row max against all-NA rows in hide_prop/hide_n apply(..., max, na.rm = TRUE) over a row with no non-missing values warns and returns -Inf. Use a safe_max() helper that returns 0 for such a row, so the hidden helper columns the slider filters on stay finite. * Factor the two all-NA guards into one agg_finite() helper safe_max() and safe_range() differed only by the aggregator and the all-NA fallback, so collapse them into agg_finite(x, f, empty). Also hoist the per-arm prop/n slices (prop_grp/n_grp) that were sliced in several places. * Set copyright year to 2026 on the new lt_table.R * Rewrite the 0.1.7 NEWS around the lt migration Consolidate the reactable/Plotly-to-lt migration into a single entry (the main table and drill-down listings are no longer separate items), drop the reactable-internal optimizations the migration itself removed, require lt (>= 0.5), and move the dowload_button spelling note out to an issue (#173). * Rename misspelled dowload_button to download_button Close #173. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add a treatment-group picker to the drill-down listing A checklist in the control bar (lt's LT.ui.chip + popover + checklist, in the el._lt.chips group) chooses which treatment arm(s) show in each expanded AE listing; the detail callback filters its records by the checked set and el._lt.resetDetail re-renders open details. The picker offers the arms present in the listing (not aggregate forest columns). Requires lt (>= 0.4.37). Close #174 Close #175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Shrink the embedded table JSON via xfun::tojson options Set xfun.tojson.dict = 0.5 and xfun.tojson.pretty = FALSE once near the top of ae_forestly() (scoped, restored on exit) so both JSON payloads the widget embeds -- the drill-down listing store and the main table lt serializes in format() -- are dictionary-encoded and whitespace-free. Repetitive columns (repeated labels, formatted numbers with few levels, the per-row parameter string, the duplicated figure-input columns) now ship as compact codes plus uniques, sharply shrinking a large forest table's HTML. The listing store no longer needs the dict/pretty args inline. Requires the xfun that defaults these args from the options. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update tests for compact tojson output ae_forestly() now sets xfun.tojson.pretty = FALSE, so the serialized lt spec and the drill-down group list ship without the pretty-print spaces these assertions matched. Match the compact form ("type":"select", ["Placebo","Low Dose","High Dose"], etc.). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix the treatment-group chip opening only on its funnel button The drill-down treatment-group picker built its popover with `LT.ui.popover(doc, label, build)` and then wrapped the returned funnel in `LT.ui.chip()`. Without lt's `host` argument the popover's click target is only the bare funnel button, so clicking the chip's label text ("Treatment group") did nothing -- the popover never opened. Build the chip first and pass `chip.el` as the popover's `host`, then drop the funnel wrap into the chip, matching how lt's own typed-filter chips wire themselves. The whole chip is now the click target. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Drop the per-row drill-down index for a client-built lookup The drill-down listing shipped, per main-table row, a precomputed array of record indices (run- or delta-encoded). On a large table that index dwarfed everything else -- ~72MB of a 139MB dummy output -- and relied on the records being contiguous in the store to compress at all. Ship no per-row pointers. Add two dict-encoded key columns (`param`, `soc`) to the shared record store and have the detail callback build a `parameter + "\r" + upper(term or SOC) -> record indices` map once on first expand, then resolve each row by its own `parameter`/`name`. This removes the whole index (down to a few MB of keys), drops the map/run/delta encoding in R, and no longer depends on store order. Also drop a redundant as.factor() on the key columns: xfun::tojson() dict-encodes any atomic vector and coerces factors to character anyway. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Let lt derive the incidence slider's bounds and step The slider range was computed in R: the max rounded up to the nearest 10 and forced to at least 100 for the proportion filter, with a hardcoded step of 1. For percentages well under 100 (the usual case) this stretched the track to 0-100 and, with step 1, snapped decimal incidences to the endpoints -- dragging the slider past 1 emptied the table. Drop the R-side range logic and pass only `type = "range"` plus the label; lt now derives nice, data-bracketing min/max and a sensible step from the bound column itself. A user-supplied `filter_range` still overrides min/max. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Set table width and container class via lt instead of a wrapper div ae_forestly() wrapped the lt table in a fixed-width div that also carried the forestly-ae class (for cell styles) and an overflow-x scroll container. All three are lt's own job now: lt_width() sizes the table, lt_class() puts the forestly-ae class on lt's container, and lt's `.lt-wrap` already scrolls horizontally. The wrapper div is dropped; the table is emitted straight into the tagList. Requires lt (>= 0.4.40) for lt_class(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Drop the width argument; size the table responsively in CSS ae_forestly(width=) hard-set the table container's width in pixels (default 1400). Replace it with a CSS rule on the `forestly-ae` container: the table takes its natural width, is centered, and is capped just under the viewport (100vw minus a fixed slack for the scrollbar/gutter) so a wider table breaks out of a narrow parent yet still scrolls inside its own container instead of adding a page-level horizontal scrollbar. With this the table fits any page on its own, so an explicit width is rarely needed; the argument is removed rather than kept as a seldom-used override. It was added this development cycle and never shipped to CRAN, so no deprecation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Exempt the control-bar row from clip-on-hover The one-line-per-cell rule (clip with ellipsis, reveal full text on hover) was also matching the lt control-bar row, so the filter chips and their popovers were clipped and only appeared while the row was hovered. Scope the rule to skip the control-bar row (tr.lti-head) so the chips stay visible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Speed up ae_forestly: drop dead sort, defer drill-down store Remove the sort of the AE listing (toupper + order + reindex over ~950k rows). The client-side detail callback builds its own param+term/SOC -> record-index map, so records need no particular order and carry no per-row index; the sort was leftover from an earlier contiguous-run index and cost ~16s on large inputs for no effect. Build the embedded drill-down record store lazily. The payload is dict-encoded JS (not plain JSON), so it can't ride as a deferred <script type="application/json">. Wrapping it in a getStore() closure lets V8 only pre-parse it at load; the tens-of-MB of array literals compile and allocate on first expand instead of when the spec's IIFE runs, keeping them off the initial-paint path. The control-bar chip still registers eagerly on mount. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Vectorize per-row max in format_ae_forestly The hide_prop/hide_n helper columns were built with a per-row apply() calling agg_finite() once per AE term -- a hotspot on large tables. Replace with a column-wise pmax() (non-finite cells -> -Inf so they are skipped; an all-non-finite row resets to the empty value), which is numerically identical and avoids the per-row R call overhead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Dedupe drill-down store across parameters (~36% smaller HTML) (#176) * Dedupe drill-down store across parameters (~36% smaller HTML) The drill-down store embedded the stacked per-parameter AE listing verbatim. Because the parameters overlap -- an AE meeting several criteria (e.g. "any", "serious" and "drug-related") is one physical record repeated once per matching parameter -- the listing is ~3x its distinct rows (953k vs 300k on an 8-param, 300k-event example). Every column was serialized at the full stacked length, so both the xfun::tojson dictionaries and the per-row code arrays carried the 3x overhead. Store each distinct record once (`records`, plus its match-key `soc`) and add `members`, mapping each parameter to the indices of the records in it. The client builds the same `parameter + "\r" + upper(term or SOC) -> record indices` lookup, now iterating `members` and keying each record by its own event and SOC, so a term row and a SOC row still both resolve. The listing rendered for every (parameter, term/SOC) row is unchanged. On the 300k-event example the store drops 46.8 MB -> 20.9 MB and the whole HTML 71.0 MB -> 45.1 MB. The store is already built lazily (#190), so this is purely a file-size / parse win, not a change to first paint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address review: drop as.factor, use match(), trim comment - `ref`: `match(rec_key, rec_key[u_first])` instead of `as.integer(factor(...))` -- same first-appearance 0-based index without building a factor. - Drop `as.factor()` on the record columns and `soc`: xfun::tojson(dict=) now dictionary-encodes plain character vectors identically, so the coercion is no longer needed. - Consolidate the 3-paragraph store comment into one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Ship each proportion/diff once, not twice, in the lt main table (#177) `format_lt_forestly()` fed `lt_dotplot()` / `lt_errorbar()` private copies of every value and CI column (`prop_fig_1..ng`, `diff_fig_1..nd`, `diff_lo_*`, `diff_up_*`) so the originals could stay visible as numbers -- the plots "use up" (hide) the columns they draw. But a plot draws its SVG only into the first value column's cell and reads the rest by reference, hidden or not. So each figure needs just one private anchor copy to draw into; the other proportions and comparison diffs read from the visible numeric columns (`hide = FALSE`) and the error bar's CI bounds from the already-shipped hidden `lower_*`/`upper_*`. This drops `prop_fig_2..ng`, `diff_lo_*` and `diff_up_*` from the payload entirely. On the 595,918-row profiling table the serialized main table shrinks 24.2 MB -> 20.7 MB (-14.7%); the figures are identical (same values feed the same dots and error bars). Saves (ng - 1) + 2*nd columns. Column names are passed to lt as plain character vectors (the error-bar value/lower/upper triple as a length-3 vector), not as.formula(). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * Draw forest figures into dedicated lt columns via into= lt 0.4.42 adds into= to lt_dotplot()/lt_errorbar(): the plot draws into a runtime-synthesized column instead of clobbering the first value column, so callers no longer need a throwaway anchor copy. Drop the prop_fig_1 / diff_fig_1 copy columns (and the vestigial prop_fig/diff_fig NA placeholders) and point each figure at its own into= column, positioned after SOC with lt_move(). Two value arrays no longer travel in the JSON payload (lt ships only the new column name; the runtime materializes it). Data columns 19 -> 17. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Ship the drill-down soc array only in SOC-display mode A main-table row's name is an AE term by default but a SOC when components="soc". The detail lookup keys records by term and, in SOC mode, also by SOC so a SOC row resolves. Ship the per-record soc array and the SOC key only then; the common term-only table omits both to keep the payload small. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Scope the AE table via lt_wrap() instead of lt_class() lt renamed lt_class() to the more general lt_wrap(); tag the table container the same way. Require lt (>= 0.4.43). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Re-add a width argument to ae_forestly() ae_forestly(width=) fixes the table container width (a number is pixels, or any CSS value) via lt_wrap(); NULL (default) keeps the natural width. Either way the width is capped at the viewport, so a wide table scrolls inside the table rather than adding a whole-page scrollbar. Restores the public width argument dropped in the reactable->lt migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Restore utils.R copyright year and drop a stale issue ref utils.R was created in #167 (2026); an earlier commit on this branch wrongly reset it to 2023. Also remove a fabricated #190 reference from a test comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * implicit NULL is fine * Enable filters on all columns; relabel the drill-down arm picker Pass an unnamed `TRUE` default to lt_interactive(filter=) so every column gets a filter box, then specialize: AE Criteria stays a single-select dropdown, SOC Name a dropdown, and the numeric n/(%)/ difference columns get range sliders (range_cols from format_lt_forestly()). Relabel the drill-down treatment-group picker to "Arms in drill-down tables", which says what it scopes. Requires lt (>= 0.4.44) for the empty-column filter skip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Exempt the per-column filter row from clip-on-hover The clip-on-hover rule (one line per cell, full text on hover) exempted only the control-bar row (.lti-head), not the per-column filter row (.lti-filters). A funnel popover under a column header lives in that row, so it got overflow:hidden while unhovered and vanished when the mouse left the cell. Exempt .lti-filters too, as .lti-head already is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Normalize group colors to hex for client-side SVG figures R color names like "grey60"/"grey40" and palette indices are not valid CSS colors, so the browser-rendered dot plot / error bar silently fell back to black. Convert the `color` vector via col2rgb()/rgb() before it reaches lt, so any R color spec renders correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Drop the free-text box on the AE-criteria filter The AE-criteria dropdown is single-choice: a custom filter expression would never apply, so pass `box = FALSE` (lt >= 0.4.45) to render the dropdown without the expression box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Resolve each drill-down detail on demand, not the whole listing at once Expanding a row no longer forces the entire embedded listing to resolve. Two changes to the detail callback and its payload: - Per-parameter key map: the term/SOC -> record-index map is now built for only the expanded row's parameter (its `members[param]` slice) on that parameter's first expand, instead of scanning every record of every parameter up front. - Lazy column decode: each record column ships as data -- dictionary `{u, c}` when repetitive, else plain `{p}` -- instead of xfun's `[codes].map(i => u[i])` form that rebuilds the full string column on load. `colGet` decodes only the rows of the detail actually opened. On a 300k-AE benchmark this cuts the one-time resolve on first expand (string materialization + full map scan) from ~630ms to ~46ms, with no change in payload size. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Shrink the drill-down payload: pack codes, bitmask parameter membership The embedded listing store carried its bulk integers as JSON number arrays. Two changes make it smaller and faster to parse: - Dictionary codes (and the per-record data they index) now travel as a base64 little-endian typed array `{b, w, n}` -- ~1-2 bytes per value instead of digits+commas -- and the client turns each into a TypedArray (far faster than parsing a big number array). The store is parsed with JSON.parse rather than evaluated as an object literal. - Per-parameter record membership is one bitmask per distinct record (`pmask`, bit i = parameter i), replacing the per-parameter index lists that repeated a record's index once per matching criterion. The client scans the masks for a parameter's bit on first expand. On a 300k-AE benchmark: HTML 38->27 MB, load 6.5->4.7 s, and the first page's detail build (parse + sizing) 885->232 ms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Wrap detail tables, highlight hovered rows Scope the clip-on-hover rule to body data cells only (header, footer and drill-down detail cells now wrap normally), and add a background highlight on the hovered data row. Collapse the stylesheet into one nested block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Merge AE styles into one dependency, simplify store JSON Fold the drill-down listing CSS into forestly-widgets.css and drop the separate html_dependency_ae_drilldown() so the main table and its drill-down ship as a single stylesheet dependency. Move the surviving html_dependency_forestly_widgets() alongside its sole caller in ae_forestly.R and delete R/html_dependency.R. Rescope the drill-down rules under .forestly-ae .lti-detail (the old .forestly-ae-drilldown class is gone), share bold headers and zebra rows with the main table, and shrink the detail type. Build the on-demand drill-down store with xfun::tojson(dict = 0) directly instead of toggling the global option. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Drop implementation-detail assertions from ae_forestly tests Remove tests that grep generated JS source for internal identifiers (getStore, s.records, LT.ui.*, tbl._lt.resetDetail), assert lt spec serialization fields ("type":"select", "wrap":{"class":...}, "download", width px), inspect htmlDependency wiring (dep names, lt-interactive.js in scripts, stylesheet paths), or check for the absence of long-deleted widgets. These break on harmless refactors without catching real regressions. What remains tests observable behavior: the table renders without error, public options surface their documented effect in the output (filter headers, dropped incidence slider), and the drill-down picker offers the listing's own arms rather than the aggregate "Total". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Decouple the drill-down store from xfun's dict wire format forestly now owns the {u, c} shape of each dict-encoded store column and uses xfun only to base64-pack the codes (pack_vec) and decode them client-side (unpack, via xfun::json_unpack); it no longer injects xfun::json_reviver or hardcodes xfun's internal dict keys. pack_vec passes dict = 0 so a packed column is always a bare width-tagged object unpack() can read (a dict-encoded mask would otherwise break it). Also ship one row per distinct record (u_row) in the store columns, so they line up with pmask and the per-record indices the client looks up. Requires xfun (>= 0.61.7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Adapt to xfun::tojson()'s numeric pack and new mode argument xfun dropped the list(mode, threshold) form of pack (now numeric-only) and moved the json/js switch to a separate mode argument whose default flipped to "js". Set xfun.tojson.pack = 100 (threshold) and xfun.tojson.mode = "js" via options for the main table, and pass mode = "json" explicitly on the two calls that must stay valid JSON (the base64 packer and the JSON.parse'd store). Also drop the now redundant pretty = FALSE on store_json since the global option covers it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
To solve issue #174