Assessment & Plan · Requirement #9 · Project P005
The open-source R ecosystem has largely solved the guide's tables and has barely touched its figures — and the untouched figures are, almost exactly, the safetyGraphics chart set we already own. That coincidence should decide the whole strategy.
Build the figures. Borrow the tables. Share the rules, not the renderer.
The FDA's Integrated Guide specifies 60 tables and 22 figures across three delivery tiers. Reimplementing all 82 displays in R would take years and would duplicate work the pharmaverse has already done. It is also unnecessary, because the coverage in the existing ecosystem is strikingly lopsided:
Twelve unserved FDA figures collapse into just four reusable chart engines — quadrant scatter (DILI), shift scatter, box-plot-over-time, and the dot-plus-forest risk-difference panel — and every one of the four already exists as a working, requirement-traced Chart.js renderer in safety.viz. That is the whole project's leverage, and it is the proposed Phase 1.
Neither a single integrated JS library nor two fully parallel libraries. Instead: one shared R derivation layer carrying the guide's normative rules, feeding two thin renderers — ggplot2/gt for static submission output, and the existing safety.viz widgets for interactive exploration. This is precisely the pattern gsm.kri already runs (Visualize_Scatter ↔ Widget_ScatterPlot over identical inputs), extended with the observation that for regulatory work the rules are the asset worth single-sourcing, not the pixels.
Version 2.0 (April 2025) is governed by MAPP 6025.9, effective 13 June 2025, and is produced by CDER's Biomedical Informatics and Regulatory Review Science team. It organises displays into three delivery tiers, which matters enormously for scoping:
| Tier | Section | Delivery rule | Contents |
|---|---|---|---|
| Core | 2 | Generated in every ST&F package, automatically | Tables 1–28, Figures 1–12 |
| Expanded | 3 | Also generated automatically; deeper cuts of the same topics | Tables 29–38, Figure 13 |
| Optional | 4 | Only on review-team request | Tables 39–55, Figures 14–22 |
| Appendix | 5 | Reference criteria — not displays | Tables 56–60 |
Three structural facts shape any implementation plan:
The 22 figures collapse to 8 chart engines. Five KM/time-to-event plots, six median-and-IQR box plots over time, two mean-change line plots with CI, two DILI quadrant scatters, two baseline-versus-postbaseline shift scatters, two paired-bar data-availability plots, two dot-plus-forest AE panels, and one incidence-rate point-range plot. The guide multiplies these by analyte panel — chemistry, kidney, liver, lipids, haematology — so parameterisation, not new code, generates most of the catalogue.
Most of the work is derivation, not drawing. The guide is largely a specification of analysis rules: treatment-emergent AE definitions, narrow-versus-broad OND Custom Medical Queries (OCMQs, formerly FMQs), three-level laboratory abnormality grading, extreme-value exclusion, a sparse-visit rule dropping timepoints with under 10% of subjects, the 30-day pairing window for DILI quadrant assignment, risk difference with unadjusted 95% CI, and incidence rates per 100 person-years. These rules are renderer-agnostic, and they are the part nobody has packaged.
Appendix Tables 56–60 are data, not displays. Level 1/2/3 abnormality thresholds for chemistry and haematology, plus implausible-value thresholds for chemistry, haematology and vital signs in both US-conventional and SI units. These should ship as package datasets on day one: they are small, self-contained, immediately useful to both renderers, and currently unavailable in any R package.
The guide ships no companion code. A full-text search of v2.0 for R package, SAS, open source, GitHub, PHUSE and reference implementation returns nothing. Every display instead carries the placeholder footnote "Source: [include Applicant source, datasets and/or software tools used]" — the FDA asks the applicant to name its own tooling. There is no FDA repository of reference outputs, and no PHUSE deliverable implements the guide display-by-display. The mock displays embedded in the PDF are the de-facto specification.
The package formerly called falcon was renamed cardinal in July 2024. It is a Roche-led, industry-collaborative effort (Sanofi, Boehringer Ingelheim and Moderna contributing), Apache-2 licensed, and its tables were updated to the 2025 v2.0 numbering in August 2025. Two caveats materially change how we should use it:
It has never been on CRAN. Treat it as an excellent, permissively-licensed reference for table logic, and as evidence that the figure half is genuinely unclaimed.
All actively released and on CRAN. tern exports seven graph functions, of which g_km and g_lineplot map cleanly onto the guide's KM and mean-change families. There is no box-plot function, no shift scatter, and no DILI screening plot. J&J's junco (CRAN, May 2026) now supplies production RTF and DOCX export on top of this stack, which closes the historic submission-format gap.
Verified against CRAN: safetyCharts and safetyGraphics were both archived on 25 March 2026, as collateral from Tendril's removal for uncorrected check failures. This is a transitive-dependency casualty rather than a quality judgement — and notably, the retirement of the Tendril bridge is already done in the unmerged gsm.safety v1.0.0 branch. More importantly for this plan: safetyCharts was always overwhelmingly interactive. Only two of its charts have static ggplot2 versions. There is no large static codebase waiting to be ported.
The central table of this assessment. "safety.viz" marks a shipped, requirement-traced interactive renderer we already control.
| FDA figure | Chart engine | cardinal | tern | safety.viz | Static-R status |
|---|---|---|---|---|---|
| F1, F4, F11, F13, F14 | Kaplan-Meier / cumulative incidence | 3 templates | g_km | — | Covered — wrap, don't build |
| F6, F9 | Mean change over time, 95% CI | 1 template | g_lineplot | results-over-time | Covered |
| F7, F8 | DILI quadrant scatter (eDISH) | — | — | hep-explorer | Gap — we own the twin |
| F15, F22 | Baseline vs. max/min shift scatter | — | — | shift-plot | Gap — we own the twin |
| F10, F16–F20 | Median & IQR box plot over time | — | — | outlier-explorer | Gap — 6 figures, one engine |
| F2, F3 | Dot plot + risk-difference forest panel | — | g_forest (partial) | ae-explorer | Gap — we own the twin |
| F5, F21 | Paired-bar data availability / retention | — | — | — | Gap — no twin, but simple |
| F12 | Incidence rate over time, point-range | — | — | — | Gap — no twin |
Fifteen of the guide's twenty-two figures have no static-R implementation in any open-source package. Twelve of those fifteen already exist as shipped safety.viz interactive renderers, and they reduce to four chart engines. The gap in the global ecosystem and the assets on our own shelf are very nearly the same set. Building the figures is not a detour from the safetyGraphics modernisation — it is the modernisation, pointed at a regulatory specification.
The framing question was whether to run parallel R and JS libraries or integrate them. Both pure options have disqualifying problems, and the right answer moves the sharing to a different layer than either implies.
Export static images from safety.viz via headless Chromium. Fails on two counts: Chart.js draws to canvas, so output is raster — submission figures want vector PDF or SVG — and making a browser toolchain a deliverable-production dependency is a qualification liability in a validated environment. It also gives no path to RTF or DOCX tables.
Static R and interactive JS each implement everything. This duplicates the guide's normative rules — extreme-value exclusion, the under-10% sparse-visit rule, Level 1/2/3 grading, the 30-day DILI pairing window — in two languages. Two implementations of a regulatory definition will drift, and drift here is the expensive kind.
Put every rule the FDA specifies in R, once. Both renderers consume its output. Static and interactive differ only in drawing medium — exactly how gsm.kri pairs Visualize_* with Widget_* over one shared data interface.
Package datasets for Appendix Tables 56–60, plus functions applying the guide's normative rules: TEAE flagging, OCMQ narrow/broad assignment, abnormality grading, extreme-value exclusion, ULN-multiple computation, DILI quadrant assignment, last-on-treatment windows, risk difference with CI, incidence rates. Output is enriched participant-level data — the input frame plus derived columns — not a pre-aggregated result set.
Visualize_* ggplot2 functions returning plot objects, and Table_* / Listing_* functions returning gt or gtsummary objects. Exports to PNG, PDF and SVG now; RTF and DOCX when a submission path is needed.
The existing safety.viz Chart.js modules and their Widget_* htmlwidget bindings, unchanged in kind, now reading L1-enriched data rather than deriving flags client-side.
Why enriched participant-level data rather than pre-aggregated results. This is the design choice that makes the shared layer viable. safety.viz's value is client-side interactivity — brushing, filtering, drill-down — which needs participant-level data in the browser. Forcing everything through a pre-aggregated R summary would break that. Adding derived columns instead single-sources the rules while leaving the interactive renderers fully capable. Presentational shaping (binning, sorting, layout) stays where it is, in each renderer.
safety.viz currently derives flags in JS, inside each module's structureData.js. Moving normative derivations upstream to R is a genuine change to its data contract, not a free alignment. Scope it deliberately: only rules the FDA specifies move to L1; everything presentational stays in JS. Expect this to be the main integration cost of Phase 1, and treat the DILI pair (Figures 7 and 8, where hep-explorer already computes ULN multiples and quadrant membership client-side) as the proving ground.
Ordered so that every phase ships something reviewable, and the riskiest integration question is answered first.
Phase 0 · unblocked today
Convert the display inventory produced for this assessment into an obot.agent requirement matrix, keyed the same way as the safety.viz renderer matrices so one requirement ID can span a static and an interactive rendering. Ship Appendix Tables 56–60 as package datasets with unit tests. Populate the Data Requirement and Design sections of hub #9.
Nothing blocks this, and the reference data is independently useful to every later phase.
Phase 1 · the differentiated twelve
In order of decreasing leverage: DILI quadrant scatter (Figures 7 and 8, plus companion Tables 21 and 22), shift scatter (15 and 22), box-plot-over-time (10 and 16–20 — six figures from one engine), and the dot-plus-forest AE panel (2 and 3).
Start with the DILI pair as a vertical slice: requirement, L1 derivation, ggplot2 renderer, vdiffr snapshot evidence, published gallery entry beside the existing hep-explorer demo. It exercises the entire chain, and it is the display where our asset is most clearly differentiated — nothing in cardinal or tern implements an eDISH plot.
Phase 1b · cheap completions
Paired-bar retention plots (Figures 5 and 21) and the incidence-rate point-range plot (Figure 12). Simple engines, no interactive counterpart to reconcile, and they complete the "exists nowhere else" set at fifteen.
Phase 2 · borrow
Figures 1, 4, 11, 13 and 14. Wrap ggsurvfit or tern::g_km and adapt cardinal's fig_01 template rather than writing new code — but supply the guide's mandated furniture, which the existing templates do not fully provide: at-risk and cumulative-event strip tables beneath every plot, pointwise 95% CI bars, and earliest-qualifying-event semantics.
Phase 3 · tables and listings
Do not rebuild 55 displays. Port the table logic we actually need from cardinal's Quarto templates into callable Table_* functions over gtsummary and cards, prioritising the tables that pair with Phase 1 figures — 21 and 22 with the DILI plots, 52 and 53 with the lab thresholds, the Table 20 family with abnormality grading. Cardinal's Apache-2 licence permits this with attribution; the ARD-first structure aligns with CDISC's Analysis Results Standard.
P005's home is gsm.safety, and its v1.0.0 rewrite — seven Widget_* bindings, the vendored safety.viz 1.4.0 bundle, qcthat qualification — is still sitting in open, unmerged PR #39. main remains the stale v0.1.0 safetyCharts bridge. Phase 0 can proceed against the release branch, but Phase 1 should not start until #39 lands, or the static work will be built on a foundation that is still moving.
Four calls that change what gets built. Recommendations given; none are executed.
Options: a new Visualize_* / Table_* family inside gsm.safety alongside the existing widgets, or a separate ST&F package.
Full L1 — safety.viz stops deriving normative flags and consumes R-enriched data — versus loose coupling, where both sides implement independently and share only the requirement matrix and demo data.
The differentiated twelve first, or tier-complete coverage of everything in Core?
Tier-complete Core means 28 tables and 12 figures including the KM family and the whole OLE section — defensible as a coherent deliverable, but it spends early effort on displays cardinal and tern already cover.
PNG and PDF from ggplot2 is trivial. Submission-grade RTF and DOCX for tables is a larger commitment — reachable via gt::as_rtf() for the gtsummary stack, or by adopting junco on rtables.
Cardinal is pharmaverse-hosted, Apache-2, industry-collaborative, and has had no figure activity since August 2024 — with four templates whose numbering is stale against the v2.0 guide it claims to implement. It covers the tables well and the figures barely. We would be building precisely its missing half, from assets nobody else has.
Offering the figure engines upstream — or simply publishing them as the interoperable complement — turns a solo modernisation into a visible contribution to the shared ecosystem, and it is a considerably stronger keynote narrative than a parallel implementation. It costs coordination and some API compromise. Flagged as a strategic option, not folded into the plan.