Eleven of the thirteen charts in the safety chart library can be called from R. The two that cannot are the participant profile and the Kaplan–Meier survival chart, and the reason recorded against both is that they need two tables of data while the R binding only accepts one. That reason is wrong about one of them and understates the problem with the other. The participant profile needs one table — and it is already on screen inside eight of the eleven widgets that ship today, one click away, which nobody has written down anywhere. The survival chart genuinely needs two, and under today's binding it does not refuse them: it builds without a word of complaint and then draws an error message in the browser.
The next release of the safety R package, which is the item you put at the head of the ranked ten on 21 August as the smallest piece of clinical work that still runs the whole lane. Both charts are already inside the JavaScript bundle the package ships — nothing upstream is waiting, and no re-vendoring is needed. What is waiting is the shape of the R function, and that is what these four questions decide.
Every chart in the library is reached from R by one function that hands the chart a single table of results plus a list of settings, and two charts do not fit that shape — the survival chart, which composes its endpoint from a table of events and a table of the population at risk, and the participant profile, which draws one participant at a time in response to a click somewhere else on the page. Both have sat as recorded deferrals since the library's last release, each citing the requirement this page answers, and neither can be built until somebody says what the R function should look like. This page builds all four candidate shapes, runs them, and reports what each one actually did — including the two failures that happen quietly, which are the reason this is a decision rather than a mechanical wrap.
Everything below was run in this session against the safety R package's integration branch, with the chart library bundle it actually ships. Nothing here is read off a specification.
We wrote the obvious binding: hand the survival chart the events table as the one table the contract allows. R accepted it. It accepted it again when we replaced every column name in the settings with a name that does not exist in the data. The same nonsense handed to any of the eleven working charts is refused immediately.
The cause is not laxness. The shared check reads the list of required columns from one specific place in each chart's data contract — the part that describes a chart with a single table. A two-table contract describes its required columns per table instead, so the check looks in a place that is empty, finds nothing to check, and reports success.
| Chart's data contract | Tables it declares | Column checks the R side performs |
|---|---|---|
| Histogram | one | 2 |
| Kidney explorer | one | 3 |
| Participant profile | one | 4 |
| Survival chart | two — events and population | 0 |
Loaded into a browser, the widget that R had just declared healthy rendered a single line of red text: Required dataset(s) missing: events, population — pass { events, population } arrays of records. The chart library caught what the R package did not.
The recorded reason says the contract cannot carry two tables. It is worse than that: it carries them without noticing, and the first thing that notices is the page in front of a reviewer. Whatever shape is chosen has to make the R side able to fail, not merely able to pass.
The participant profile already reads a second domain: it draws a participant's adverse events beneath their laboratory results, and the chart library takes those records inside the settings object rather than as a second table. That looked like a free answer — carry the second table in the settings and change no signatures at all. It is not.
R serialises a table of data column by column. The one table the binding names is converted back into records in the browser by the binding itself; a table hidden inside the settings is not, so it arrives as an object of columns where the chart expects a list of rows. The chart tests for a list of rows, finds an object, and skips the whole domain without an error, a warning, or a mark on the page.
This is the same class of defect as the first one, arrived at from the other direction, and it is why the settings route is not a shortcut.
We built the profile widget properly: correct contract, correct column checks, real laboratory data, no errors anywhere. Loaded in a browser it rendered an empty control panel and one line of text — Waiting for selection — listening on document.
The profile is a drill-down. It waits for another chart on the page to announce which participants a reviewer picked, and in a standalone R report there is no other chart. This, and not the number of tables, is the real reason the profile has no binding. Its data contract declares one table, and always has.
Looking for where the profile does get its selection, we found that eight of the eleven widgets the package ships today already carry it, mounted as a panel beside the chart, switched on by default. Opening the liver-safety widget from R with its own documented example and clicking a participant produces the full profile — header, demographics, the R ratio, the standardised laboratory trajectory, the measure table with sparklines — with no new code and no change to anything.
None of this is written down. The setting that controls it is not in any of the vendored data contracts, so it is undocumented, on by default, and invisible to the checks. The measurement, taken from the bundle the package actually ships:
| Widget | Opens the participant profile on a click | Which domain the profile then shows |
|---|---|---|
| Liver-safety explorer, histogram, shift plot, delta-delta, outlier explorer, QT explorer | Yes — 6 widgets | Laboratory results only |
| Adverse-event explorer, adverse-event timelines | Yes — 2 widgets | Adverse events only |
| Results over time, liver waterfall, kidney explorer | No — 3 widgets | — |
Which sharpens the profile question rather than answering it. A reviewer inside the liver widget sees a participant's laboratory story; one inside the adverse-event widget sees their event story; nobody sees both, because a host chart only feeds the profile the domain it holds itself. Both domains together is exactly what a standalone widget would be for.
| Shape | How the second table gets there | Working files it touches | What it costs |
|---|---|---|---|
| Named tables — recommended | The function takes the tables it needs by name, and the payload carries them under one labelled slot the binding converts | 0 of the 22 | One shared helper gains a second entry point. The eleven keep the argument they have today, unchanged and unread. |
| One stacked table | The caller stacks both domains into one table with a column saying which is which; the binding splits it again | 0 of the 22 | Measured on the real data: a quarter of the stacked table is padding that exists only to give the other domain its columns, and two columns appear in both domains meaning different things. The per-table column check has nothing to check against. |
| Inside the settings | The second table travels as a settings value, the way the profile's adverse events do now | 0 of the 22 | Fails silently today, as measured above. Data hides in the settings, so the contract's own split between data and settings stops being true and there is nowhere natural to validate the second table. |
| One argument for everything | Every widget takes a single labelled collection of tables, one-table charts included | 22 of the 22 | Every R function and every browser binding for eleven working charts is rewritten, plus their tests and any caller outside the package. The gain over named tables is tidiness. |
We implemented the named-tables shape end to end and ran it. The survival chart draws from R: 254 participants across three treatment arms, 217 events and 37 censored, confidence bands, censor marks, the at-risk table, and the filters that compose the endpoint live.
Every failure the naive shape swallowed is now refused before a browser is involved:
And the eleven working widgets do not notice the change:
| Test suite | Assertions passing | Failing | Skipped |
|---|---|---|---|
| Integration branch, untouched | 2,941 | 0 | 13 |
| Same branch with both new widgets and the contract change | 2,941 | 6 | 13 |
The six are all one thing: four roster lists that name the eleven widgets by hand and now name thirteen — the export list, the deferral list, the gallery, and the example pages. Not one assertion belonging to an existing chart changed from passing to failing. The pipeline half works too: a two-table report workflow, declaring both domains the way every risk-indicator workflow in the ecosystem already declares two, ran start to finish and wrote its HTML report.
The profile, given the participants to show and both of its domains, draws both — laboratory trajectory, adverse-event summary, adverse-event timeline on the same study-day axis, and a stepper through the selected cohort.
Every widget here takes one table today. The survival chart needs two, and something in the library will need two again — the profile's second domain is already a second table wearing a disguise. This is the one answer both charts wait on.
The alternatives, with what each forecloses:
One consequence to accept with it: ask the chart library to declare the profile's adverse-event table as a real second table in its data contract, rather than leaving it inside the settings. That is one change upstream and one re-vendoring, and without it the second table stays the one thing the R side cannot check.
It is a drill-down, and eight widgets already open it on a click. The question is whether it is also a thing on its own — a page you can produce for a named participant or a named cohort, save, and send to someone.
This package holds two vocabularies. The metric steps speak the risk-monitoring pipeline's names — lower-case, dates rather than study days. The widgets speak the analysis-dataset names a statistical programmer would recognise. The population table has to pick one.
The broken widget from the first failure above passes every check this package has. The cross-repository guard confirms a binding file exists for each chart. The gallery test confirms a screenshot file exists and is larger than ten kilobytes — and those screenshots are copied from the chart library, so they show the chart working there, not here. The example-page test confirms a page mentions the widget's name. The workflow tests confirm the bundle and the data reached the HTML file. Nothing anywhere loads that file in a browser and checks that a chart appeared.
That gap is invisible while every widget takes one table and copies a binding that works. It stops being invisible the moment two widgets ship on a payload shape nothing has rendered.
Two widgets, one additive change to a shared helper, one new example dataset, no change to any of the eleven that work, and a rendering check that fails on the failure this page opens with. Roughly thirty files, of which twenty are new.
One claim we expected to find wrong and did not: the shared binding helper genuinely does validate every one-table chart properly, and the eleven are in good order. The defect is confined to the shape nothing has used yet.
Twelve of the thirteen charts are created with the same two arguments — the element and the settings. The participant profile takes three, with the data in the middle. A binding copied from any of the others hands the settings in where the data belongs, and what you get is a chart complaining that four columns are missing from data it never received. That cost this session twenty minutes.
Measured for this page, not relayed. Everything above was produced on 23 August 2026 against the safety R package's integration branch at commit 277fae8, in a scratch worktree, with R 4.3.3 and gsm.core 1.2.0 — the version installed on this machine. The chart library bundle is the one the package vendors, version 1.7.0, read from the package's own files rather than from the library's source. Thirteen tests skip on this machine because the census figures were qualified against gsm.core 1.3.1 and this machine has 1.2.0; none of the thirteen touches a widget, and the skip count is identical before and after the change, so no figure on this page depends on the difference.
Sources. The safety R package on its integration branch — the shared binding helper, all eleven widget bindings, the vendored data contracts, the report workflows, the test suite and the cross-repository parity guard. The chart library at its current release — the two unwrapped charts' source and data contracts, and the shipped bundle, which was read directly to establish which widgets carry the profile panel. The risk-indicator package next door, for the multiple-table precedent. The standard mapping specifications for the subject and adverse-event domains, read from the installed package.
What was not done. Nothing was committed to the safety R package, no branch was pushed, and no issue was filed or edited. The probe worktree exists only on this machine and carries no work anyone should merge — the implementation is the answer to W1, not a thing to adopt sight unseen. The screenshots are of that probe, running locally.