Skip to content

Allow non-display of subjects from placebo arm - #175

Closed
LittleBeannie wants to merge 1 commit into
mainfrom
174-add-ae_listing_placebo-to-control-placebo-subject-drill-down-in-ae-forest-plots
Closed

LittleBeannie wants to merge 1 commit into
mainfrom
174-add-ae_listing_placebo-to-control-placebo-subject-drill-down-in-ae-forest-plots

Conversation

@LittleBeannie

@LittleBeannie LittleBeannie commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

To solve issue #174

@LittleBeannie LittleBeannie changed the title Add new arguments to prepare_ae_forestly, tests and new contents to… Allow non-display of subjects from placebo arm Oct 8, 2026
yihui added a commit that referenced this pull request Oct 8, 2026
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>

@yihui yihui left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

yihui added a commit that referenced this pull request Oct 8, 2026
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>
@LittleBeannie

Copy link
Copy Markdown
Collaborator Author

Let's close my PR175 for now to avoid making the function getting complicated!

@yihui
yihui deleted the 174-add-ae_listing_placebo-to-control-placebo-subject-drill-down-in-ae-forest-plots branch October 8, 2026 21:24
LittleBeannie pushed a commit that referenced this pull request Oct 15, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ae_listing_placebo to control placebo-subject drill-down in AE forest plots

2 participants