Architecture
One bundle, one shared shell, one data contract per chart — built to be embedded anywhere JavaScript or R can reach.
The big picture
safety.viz mirrors the architecture proven by OpenRBQM's gsm.kri ↔ gsm.viz pairing: a plain JavaScript charting library (this repository) owns all rendering, and a thin R package (gsm.safety) wraps the committed bundle as htmlwidgets. The JavaScript side never knows about R; the R side never draws.
- Data contract — every module publishes a JSON Schema (src/data/schema/) describing its long-format input records and settings mapping. The same schema validates data at the boundary at runtime and generates the API reference's data-contract table.
- Module — a factory (for example
histogram(element, settings)) that mounts into any container. Settings merge onto the module's defaults, so callers supply only overrides. Internals follow the gsm.viz-style flow:checkInputs→configure→structureData→getScales/getPlugins→new Chart. - Shared shell —
src/shell.jsrenders the chrome every renderer shares by construction: a collapsiblesv--prefixed control sidebar beside a main column of chart, footnote, small-multiples, and listing slots. - Charts — rendering is Chart.js 4 with module-scoped plugins (normal-range overlay, annotations, selection styling) — no bespoke SVG layer to maintain.
- Committed bundle — esbuild emits versioned IIFE and ESM bundles to
dist/safety.viz-1.1.0/, committed to the repository so consumers (and this site's demos) pin exactly the artifact that was tested. CI fails whendist/drifts fromsrc/. - R bindings — gsm.safety vendors the same IIFE bundle as
Widget_*htmlwidgets, completing the gsm.kri ↔ gsm.viz mirror.
The data-contract philosophy
Clinical data rarely arrives renderer-shaped, so no column name is hard-coded. Each renderer names its inputs through a settings mapping whose defaults follow ADaM conventions (STRESN, USUBJID, STNRLO…): standard data works untouched, and anything else is one mapping away. Validation happens once, at the boundary — missing required columns throw with a rendered message, missing or non-numeric results are removed with a reported count, and optional columns degrade gracefully (the normal-range control, for instance, hides for measures without normal data). The JSON Schema is the single source of truth: runtime validation, the generated API data-contract table, and the requirement matrix all key to it.
Module anatomy
Every module exposes the same lifecycle, kept shape-compatible with the original safetyGraphics renderers: init(data) / setData(data) to bind and draw, setSettings(overrides) to re-normalize and rebuild, render() to redraw from current state, resize() for host layouts that change container size without a window resize (the R htmlwidget case), and destroy() to tear down every Chart.js instance. The full per-module surface is documented on each API reference page, generated from JSDoc — the build fails on undocumented public surface.
How a renderer plugs into the shell
A module never rolls its own layout. It calls renderShell(element), which empties the container and returns named slots — controls, chart canvas, annotation, footnote, small-multiples, listing — then declares its control panel with the bound controlBuilders helpers (sections, rows, labeled inputs) and draws into the main-column slots. One stylesheet is injected per document by whichever module mounts first, and the browser suite enforces per available renderer that its demo renders this shared chrome.
Quality machinery
Each migration starts from a reviewed requirement matrix in safety.agent; matrix rows route to unit (Vitest) or browser (Playwright) tests whose names carry the requirement IDs. The evidence pipeline replays both suites and commits results plus screenshots to docs/evidence/, which this site joins into each module's qualification report. The site build itself fails on broken links or missing screenshots, so a bad page cannot publish.