A second chart library and its R package, for comparing groups and relating variables in biomarker data, with the test result on the chart. It repeats nothing safety.viz already draws.
2 Oct 2026Six core chartsScope: charts, statistics, export, specificationsNot here: data loading, mapping, the app
Decisions
Agreed with @jwildfire in session on 2 Oct 2026 unless marked as a default. A default stands until someone objects on the objective.
D1One rule divides the two libraries: safety.viz describes, bio.viz compares and relates.A chart whose main output is a test, an effect estimate or a relationship between two variables belongs in bio.viz. A chart that shows one measure's distribution or course stays in safety.viz and is reused untouched.
D2R computes every test. Neither chart library holds any inference code. AgreedThis replaces the question of which library p-values live in. They live in R, as plain functions in gsm.bio, and a chart shows them when R is attached. No statistical library is written in JavaScript.
D3bio.viz runs beside safety.viz on the page and borrows its shared parts. Agreedsafety.viz will be present for any study using this. So bio.viz does not copy the control sidebar, filters, record listing, participant rail or the Kaplan–Meier estimator; safety.viz exposes them as a kit and bio.viz calls them. That is one small, additive change in safety.viz, and the only one this design asks of it.
D4Charts reach R through one connection with three forms: precomputed, in the browser, or on a server. DefaultIn the browser is the default for the app, because it needs no server and data stays on the machine. Precomputed serves static reports. The server form waits until someone needs it. The second requirement measures what the browser form costs, and that measurement confirms or overturns this default.
D5Six chart types are the core, including the biomarker screen. AgreedFive answer the standard exploratory questions one biomarker at a time. The screen answers them across every biomarker at once, which is otherwise done by writing one figure per biomarker. It pulls the hazard ratio forward, because its survival rows need one.
D6Static figures and tables come from R, as ggplot twins that share the chart's settings.Same pattern as the FDA static figures planned for gsm.safety. The interactive chart also offers a PNG download so nobody waits for the R path.
D7A specification is the chart's own settings saved as JSON. No expressions are ever evaluated.A specification is data, so loading one can never run code. A filter is a column, an operator and values, nothing more.
D9Only the results table is required. Participant data is optional. AgreedWith it you get filters and participant-level variables. Without it the charts still draw and the filters are simply absent. A group may come from a column on the results rows.
D8Names: bio.viz and gsm.bio, matching safety.viz and gsm.safety.The dot keeps the family consistent.
Statistics engine
Before writing tests in JavaScript, the question was whether a framework already exists. One partly does, and it does not cover this design. So no inference is written in JavaScript at all.
Log-rank and hazard ratios would be ours to write.
So adopting a library still leaves five tests to write by hand, including the hardest one, and every number still has to be reconciled with R's defaults.
Option 1
A JavaScript library
stdlib for what it covers, our own code for the rest.
For
Instant, no extra download.
Fits inside one offline HTML file.
Against
We still write and prove rank-sum, Fisher, Spearman, log-rank and Cox.
Every statistic exists twice: once here, once in R for the static figure.
A statistician has to trust our code, not R's.
Option 2 · recommended default
R in the browser
webR is R itself compiled to WebAssembly by Posit. It runs on the user's machine, off the main thread.
For
No inference code to write. The number printed is R's own.
The same functions make the static figure, so nothing exists twice.
Data never leaves the machine and no server is needed.
Works on plain static hosting such as GitHub Pages.
Against
Weight: the engine is about 12 MB and the full distribution about 49 MB unpacked, fetched as needed and cached.
The first test waits for R to start. I have not measured how long.
It will not fit inside the single offline HTML file. It ships as a folder beside the page, or is fetched.
The project says its interface may still change, and packages are rebuilt for each release.
Option 3
R on a server
gsm.bio installed on a server and called over the network. OpenCPU exposes any R package's functions this way with no extra code; plumber and Shiny are alternatives.
For
Lightest page and full R, any package.
Familiar wherever Shiny already runs.
Against
Needs hosting, and someone to keep it up.
Participant data travels to the server.
No offline use, and the app would need a server behind it.
Recommendation: R behind one connection
Statistics are plain R functions in gsm.bio that rely only on the stats and survival packages. They are the one source of every test.
A chart never computes a test. It hands over its one-row-per-participant table and receives estimates, intervals, p-values and counts.
Three forms of the connection run the same functions and give the same answers.
Precomputed: gsm.bio works out the opening view and ships the results with the widget. For static reports.
In the browser: webR, loaded the first time a test is asked for. The default for the app.
Server: built later, only if an organisation wants it.
With no R attached, a chart still draws and says that statistics are unavailable.
Start with the first two forms. The first release measures how long R takes to start and how much it downloads, and that measurement decides whether the default holds.
What this settles about p-values
They live in R, not in either chart library. The earlier three-way choice goes away.
The connection to R is built in bio.viz and handed to a chart as a setting. safety.viz's time-to-event chart can be handed the same connection to show the log-rank test the FDA figures require, without importing anything, and its static twin gets it from R as already planned.
The Kaplan–Meier estimate stays in safety.viz as it is. It is needed to draw the curve and is already checked against R.
The histogram's two approximate screens could later be swapped for R's real tests the same way. That is a separate decision.
Checked on 2 Oct 2026: webR 0.6.0, released May 2026, ships R 4.6.0; sizes read from the published package and its file headers; survival 3.8-6 is available prebuilt for it. Not checked: start-up time, and the size of survival with its dependencies.
webR documentation ·
serving pages with webR ·
stdlib statistics
The boundary
Reused from safety.viz, not rebuilt
Keep Histogram, for one biomarker's distribution.
Keep Results over time, for box plots by visit, and the mean and median lines when they land there.
Keep Outlier explorer, for one line per participant.
Keep Shift plot and delta-delta, for one measure across two visits and change against change.
Keep Time to event, for curves by arm on a safety endpoint.
Keep Participant profile, as the drill-down from every bio.viz chart.
New in bio.viz
New Group comparison, with tests.
New Association scatter, with correlation.
New Correlation matrix.
New Cross-tabulation, with tests.
New Stratified survival, with biomarker cut-offs and a log-rank test.
New Biomarker screen, one row per biomarker.
Two places the line needs care
Scatter. Shift plot and delta-delta stay as they are. The association scatter takes any two variables and exists to report a correlation, so it does not replace either. The earlier suggestion to widen delta-delta is dropped.
Survival. bio.viz does not write a second Kaplan–Meier estimator. It calls safety.viz's, and adds what safety.viz lacks: a ready-made time and censor column as input, groups cut from a biomarker, and, from R, the log-rank test and a hazard ratio.
Core chart types
Six charts. The sketches show layout only; they are not drawn from data.
Sketch
Group comparison
groupComparison · Widget_GroupComparison
Question
Does this biomarker differ between these groups?
Draws
One value across the levels of a category at chosen visits: box, violin or points, with the number in each group beneath.
Controls
Value and visit; category on the axis and its levels; second grouping by colour; panels by one further variable; log scale; test; pairwise comparisons on or off.
Statistics
Welch t-test or Wilcoxon for two groups; one-way ANOVA or Kruskal–Wallis for more; pairwise tests with Holm adjustment; difference in means with its interval.
Click
A box or point lists its participants and opens the profile.
Not overlap
Results over time always has visits on the axis and reports no test.
Sketch
Association scatter
associationScatter · Widget_AssociationScatter
Question
Do these two variables move together?
Draws
One point per participant; either axis can be a biomarker at a visit or any participant-level number.
Controls
X and Y variable; colour by group; panels; log scale per axis; fitted line (linear, smooth, identity) with band; correlation method.
Statistics
Pearson or Spearman coefficient with interval and p-value, overall and per group; slope and intercept of the linear fit.
Click
Brush a region to list participants; click a point for the profile.
Not overlap
Shift plot is one measure at two visits; delta-delta is change against change. Neither reports a correlation.
Sketch
Correlation matrix
correlationMatrix · Widget_CorrelationMatrix
Question
Which of these biomarkers, or which visits of one biomarker, are related?
Draws
A grid: marks sized and coloured by the coefficient on one side, the numbers on the other. For six variables or fewer, a small-multiple scatter mode.
Controls
Mode (across biomarkers at one visit, or across visits for one biomarker); which biomarkers or visits; value type; method; minimum pairs required to show a cell.
Statistics
Pairwise Pearson or Spearman on complete pairs, with the pair count and interval per cell. No p-values on the grid.
Click
A cell opens the association scatter for that pair.
Not overlap
Nothing like it in safety.viz.
Sketch
Cross-tabulation
crossTab · Widget_CrossTab
Question
Is this category associated with that one?
Draws
A two-way table of counts with totals, beside stacked proportion bars of the same numbers.
Controls
Row and column variable; either can be a biomarker cut at the median, tertiles, quartiles or typed values; row or column percentages; test.
Statistics
Chi-square or Fisher's exact test, with a warning when expected counts are too small for chi-square.
Click
A cell lists its participants.
Not overlap
safety.viz has fixed count tables inside the QT, liver and kidney charts; none is general and none is tested.
Sketch
Stratified survival
stratifiedSurvival · Widget_StratifiedSurvival
Question
Do participants with high and low levels of this biomarker have different outcomes?
Draws
Kaplan–Meier curves per group with band, censor marks and at-risk strip, above a small histogram of the biomarker showing where the cut falls and how many land each side.
Controls
Endpoint; grouping by a biomarker cut (median, tertile, quartile, typed value, on raw, baseline, change or fold change at a chosen visit) or by any category; drag the cut line.
Statistics
Log-rank test; median survival with interval; participants and events per group; hazard ratio with interval when there are two groups.
Click
A curve or an at-risk cell lists its participants.
Not overlap
Uses safety.viz's estimator and drawing. safety.viz's own chart builds a safety endpoint from event records and groups by arm only.
Sketch
Biomarker screen
biomarkerScreen · Widget_BiomarkerScreen
Question
Across every biomarker, where is the signal?
Draws
One row per biomarker: an estimate and its interval for a comparison chosen once, sorted, with raw and adjusted p-values alongside. The estimate is unit-free so rows can be compared.
Controls
The comparison (group difference, correlation with one fixed variable, or survival split); visit; adjustment method; sort.
Statistics
Per biomarker: a standardised difference between two groups, a correlation coefficient, or a hazard ratio for high against low. The same tests as the single charts, with Benjamini–Hochberg or Holm adjustment across the rows.
Click
A row opens the matching single chart for that biomarker.
Not overlap
Nothing like it in safety.viz. The usual alternative is one figure per biomarker. This is the one place where adjustment for many tests has a clear meaning.
The shared core
All six charts are thin layers over the same four steps. Steps 1, 2 and 4 are bio.viz; step 3 is R.
1 · VariableName a variable once: a biomarker at a visit with a value type, or a participant-level column. Any axis, group or split takes one.
2 · FrameResolve the named variables to one row per participant. Rows that cannot be resolved are dropped and counted.
3 · StatisticThe frame goes to R, which returns estimates, intervals, p-values and the counts it used.
4 · DrawThe chart draws the frame and prints the statistic with its method and counts.
A variable is written the same way everywhere, whether it is a biomarker at a visit or a plain column.
Value types are raw, baseline, change, fold change and percent change, computed from the baseline visits named in settings, the way the shift plot already does.
Any continuous variable can be cut into groups by one shared rule: median, tertiles, quartiles or typed cut points. Cross-tabulation, group comparison and stratified survival all use it.
A wide, one-row-per-participant dataset needs no special handling: every column is already a participant-level variable.
One thin R function per row in gsm.bio, each a wrapper that fixes the inputs and the shape of the answer around the R function named. Nothing here is reimplemented.
Statistic
Used by
R function called
Note
Welch two-sample t
Group comparison
t.test()
Unequal variances, R's default.
Wilcoxon rank-sum
Group comparison
wilcox.test()
R's defaults, including its switch between exact and approximate.
One-way ANOVA
Group comparison
aov()
Kruskal–Wallis
Group comparison
kruskal.test()
With tie correction.
Pearson correlation
Scatter, matrix
cor.test()
Interval by Fisher's z.
Spearman correlation
Scatter, matrix
cor.test(method = "spearman")
Chi-square
Cross-tabulation
chisq.test()
Continuity correction on two-by-two, R's default; small expected counts flagged.
Fisher's exact
Cross-tabulation
fisher.test()
Any table size R will accept.
Standardised difference
Screen
A few lines of our own R
Unit-free, with interval, so biomarkers on different scales share one axis. The one row not handed to an existing function, to avoid a heavy dependency; tested against effectsize::hedges_g().
Log-rank
Stratified survival, screen
survdiff()
Two or more groups.
Median survival and interval
Stratified survival
survfit()
For the printed table. The curve itself is still drawn from safety.viz's estimate.
Holm, Benjamini–Hochberg
Pairwise tests, screen
p.adjust()
Cox hazard ratio
Stratified survival, screen
coxph()
Arrives with stratified survival, because the screen's survival rows need it.
How a p-value is shown
These rules are written once and one shared function applies them, whichever chart prints the number.
Always with the method's name and the counts it used, never alone.
Recomputed on whatever the filters leave, with the filter stated in the footnote.
Labelled exploratory and unadjusted by default. Adjustment is offered only where the family of tests is visible: pairwise comparisons inside one chart, and rows of the screen.
No stars and no word “significant”.
Not computed below a minimum group size; the chart says why instead of printing a number.
Data the charts expect
One required table and two optional ones, each with column names supplied as settings and ADaM names as defaults, so the basic app's mapping page can drive them the same way it drives safety.viz.
Table
Required
One row per
Needs
What it adds
results
Yes
participant, biomarker, visit
participant, measure, value, visit, visit order
Every chart except stratified survival runs on this alone.
participants
No
participant
participant; any categories and numbers
Filters, and participant-level variables for groups, panels and axes.
outcomes
No
participant, endpoint
participant, endpoint, time, censor flag
Stratified survival, and survival rows in the screen.
Without participant data there are no filters. A group can still come from a biomarker cut, or from a column already carried on the results rows.
Without outcomes, stratified survival reports that it needs them and the screen offers only its other two comparisons.
bio.viz publishes a chart list in the same format as safety.viz's portfolio manifest, so the app can list both libraries' charts together. That file is the whole interface to the data loading, mapping and app work, which is designed elsewhere.
Example data is public or made up: the pharmaverse ADaM sets gsm.safety already ships, plus a seeded synthetic biomarker panel with known planted effects so a test can assert the answer.
Repositories
safety.vizexisting · one changeExposes its shared parts as a kit: control sidebar, filters, axis limits, record listing, participant rail, box drawing, Kaplan–Meier estimator, and the copy of Chart.js it already carries.
bio.viznew · needs safety.viz on the pageThe connection to R and the p-value formatter, the six charts, the variable and frame core, PNG download, specification read and write. No statistics code. Same stack and conventions as safety.viz.
gsm.safetyexisting · unchangedStill carries the safety.viz bundle for R users.
gsm.bionew · imports gsm.safetyThe statistics functions, a widget per chart, a static ggplot twin per chart, table builders with RTF output, and a batch runner for saved specifications.
The statistics functions depend on nothing from the gsm packages, so the same file runs in a normal R session, in the browser and on a server.
bio.viz follows safety.viz's house rules: Chart.js on canvas, one requirement matrix per chart, tests named by requirement, an evidence page per chart, a gallery, and committed bundles.
A chart is done when it is in the gallery with evidence, an API reference and its gsm.bio widget, the same gate safety.viz charts pass.
Every chart raises safety.viz's participant-selected event, so the existing profile rail and the app's shared drill-down work without new code.
Getting results out
Titles and footnotes. Every bio.viz chart takes a title, a subtitle and a list of footnotes, with named placeholders filled from its settings. One footnote is written automatically: date, library version, method and counts.
From the browser. Download the figure as a PNG with its title and footnotes drawn in; download the statistics and the underlying frame as CSV.
From R. Each chart has a static twin in gsm.bio that returns a ggplot from the same settings, saved to PDF, PNG or SVG. Tables come back as data frames with an RTF writer on top.
Specifications. A chart can hand back its current settings and filters as JSON, and be rebuilt from them. A list of these is a specification book.
Batch. gsm.bio runs a specification book against a dataset and writes every figure and table to a folder. One specification can be expanded across every biomarker in the data.
The interface that collects specifications while someone explores is part of the app, so it is not designed here.
Order of work
Eleven requirements, one session and one release each. The first three do not depend on one another and can run in parallel; only the first touches safety.viz.
Requirement
Ships in
Why this order
1
safety.viz's shared parts opened to a second library (#354)
safety.viz 1.10.0
One additive pull request. Merging it to dev is all bio.viz needs.
The static twins copy settings that are by then stable.
The first seven are the smallest useful product. Version numbers after the first three are proposed and fixed when each requirement is prepped.
Risks and open points
R in the browser is heavy and its start-up time is unmeasured. If the first release shows it is too slow, the fallback is the server form, not hand-written JavaScript.
It does not fit the single offline HTML file the safety app promises. A bio study offline would ship as a folder.
Tests arrive a moment after the chart draws, so every chart needs a visible waiting state and must never show a stale number after a filter changes.
webR's interface may change and its packages are rebuilt each release, so the version is pinned and upgrades are deliberate.
The PNG from the browser is not vector quality. Anything bound for a document should come from the R twin.
Exposing the kit turns safety.viz's internals into a public surface that bio.viz depends on. Changes there will need version discipline.
A correlation matrix across hundreds of biomarkers needs a cap or background computation; the first version limits the count and says so.
Tests can be printed by charts in two libraries. The shared formatter is what stops their wording drifting apart.