nepExplorer screens trial lab data for acute declines in renal function using the KDIGO acute-kidney-injury staging criteria. Its main object is a scatter — one point per participant, maximum fold change in serum creatinine on x against maximum absolute change on y, over coloured L-shaped stage zones — paired with a stage summary table and a click-through patient profile. That is the same shape as hep-explorer's eDISH quadrant plot, one organ over, so the architecture question was settled by the assessment: it is a canvas-first safety.viz module on the proven lifecycle, and the stage zones are a Chart.js background plugin like quadrantPlugin.
This design does four things:
DELTA_STAGE trap turns out to have a sharper form than "an ordering bug": the R app's chart and its own table stage the same participant differently.adbds.csv (D8), designed to exercise every zone including the ≥ 4.0 mg/dL rule no real dataset here can reach; the RhoInc set stays Phase 2's source, as D1 decided (§5).Every number in §5 was computed from the files themselves — safety.viz/site/data/adbds.csv at dev, and RhoInc/data-library's data/clinical-trials/renderer-specific/adbds.csv — with baseline taken as the participant's baseline-flagged visit (Baseline / Screening), maximum fold and maximum delta taken over that participant's post-baseline records, and µmol/L converted at 88.4. It is a scoping calculation done outside the module, not module output; the implementation will reproduce it under test.
D1 — Phase-2 demo data · DECIDED 2026-07-19. Derive creatinine-based eGFR (CKD-EPI) and retain the RhoInc renderer-specific dataset as a nep-specific demo. Folds into #25.
D2 — Phasing · DECIDED 2026-07-19. Ship Phase 1 (the KDIGO creatinine scatter) as a standalone renderer at the standard done gate; Phase 2 (the patient profile) starts once D1's data lands.
D3 — Visit animation · DECIDED 2026-07-19. The scatter's plotly time animation is deferred to a follow-up enhancement; not part of v1.
D8–D7 below are new and open. D8 and D9 are refinements of D1 in light of §5's measurements; D4–D7 are the clinical-logic calls the port has to make and the source does not answer cleanly.
The KDIGO AKI criteria stage a participant on serum creatinine three ways:
| Stage | Fold change (value ÷ baseline) | Absolute criteria |
|---|---|---|
| 1 | 1.5 – 1.9× | or an increase of ≥ 0.3 mg/dL |
| 2 | 2.0 – 2.9× | — |
| 3 | ≥ 3.0× | or an increase to ≥ 4.0 mg/dL (or initiation of renal replacement therapy — out of scope, it is not a lab value) |
Two consequences the port has to respect. First, the absolute-change axis carries exactly one KDIGO cut-point — 0.3 mg/dL, and it only ever produces Stage 1. There is no KDIGO Stage 2 or Stage 3 defined on absolute change. Second, the ≥ 4.0 mg/dL rule is about the absolute value reached, not a change, so it is not a region of the (fold, delta) plane at all.
Reading R/creatinine_data_fcn.R and R/creatinine_scatter_charts.R side by side:
The table stages absolute change with
DELTA_STAGE = case_when(
DELTA_C > .3 ~ "Stage 1",
DELTA_C > 1.5 ~ "Stage 2",
DELTA_C > 2.5 ~ "Stage 3",
TRUE ~ "Did not trigger Delta Creatinine Stage"
)
and because case_when returns the first match, every delta above 0.3 is labelled Stage 1 — the Stage 2 and Stage 3 branches are unreachable. That much was already flagged on #35. The sharper finding is what the chart does with the same numbers: draw_creatinine_scatter paints nested rectangles, largest first, so the visible zone boundaries are descending — Stage 3 fills the panel, Stage 2 is overpainted at x<3 ∧ y<2.5, Stage 1 at x<2 ∧ y<1.5, and white at x<1.5 ∧ y<0.3.
So in the shipped app, a participant with a 2.0 mg/dL rise and no fold change plots inside the orange Stage-2 zone and is labelled "Stage 1" in the table beside it. The chart geometry is right and the table is wrong; they were written from the same cut-points in opposite order. Porting either one alone would carry the disagreement forward.
Two smaller fidelity notes from the same file, offered as observations rather than defects to fix upstream:
value[1L] after arrange(desc(baseline_flag)) — it works because "Y" sorts before "N", but it silently picks an arbitrary record when a participant has two flagged rows, and it depends on the flag's coding. The port takes an explicit baseline setting instead (D7).scale_y_continuous(limits = c(0, max_delta)) starts the axis at zero, so a participant whose creatinine only ever fell — max delta < 0 — is dropped by ggplot rather than drawn at the floor. In the RhoInc data that is a real population: the minimum per-participant max-delta is −0.71 mg/dL (D6).D4 — What the stage zones encode · DECIDED 2026-07-28. Faithful-to-source (the four nested rectangles above, with the table's ordering corrected to match the chart) · KDIGO-proper (a fold-change ladder at 1.5/2/3 plus a single 0.3 mg/dL Stage-1 trigger on the delta axis) · parameterized cut-points with KDIGO-proper defaults.
DECIDED 2026-07-28 — parameterized, KDIGO-proper defaults. The 1.5 and 2.5 mg/dL absolute cut-points in the source are not KDIGO criteria and are not in nepExplorer's own outline.md either (which proposed 0.7/1.2 alongside eGFR percent declines) — three different ladders across two files and a spec. Rather than pick one of the three and inherit the argument, the module takes the KDIGO ladder as its default and exposes the cut-points as a stages setting, exactly as hep-explorer exposes cuts. A sponsor who wants the original rectangles back sets three numbers.
Under the default, the zone under a point is the worse of the two axes, which keeps the L-shaped silhouette a nepExplorer reader recognises:
Stage 1 is everything at or above 0.3 mg/dL up to 2× fold, plus the 1.5–2× band at any delta; the no-stage box is the one region where both criteria are clear.
D5 — The ≥ 4.0 mg/dL Stage-3 rule · DECIDED 2026-07-28. It is a property of the participant's maximum value, not a region of the plane. Draw it as a mark property (distinct symbol + tooltip line, counted as Stage 3 in the table) · drop the rule · give the y-axis a second mode showing absolute value.
DECIDED 2026-07-28 — mark property. A ringed point plus an explicit tooltip row keeps the rule visible without distorting the axes, and the summary table's Stage-3 count stays clinically complete. The rule fires on zero participants in either demo dataset (the RhoInc maximum creatinine is 1.93 mg/dL), so its evidence has to be a unit test on synthetic input — noted in §8 so it does not get quietly skipped for want of a demo case.
D6 — Participants whose creatinine only fell · DECIDED 2026-07-28. Faithful (y-axis floors at 0, those participants leave the chart) · clamp them to the floor · extend the domain below zero when the data has them.
DECIDED 2026-07-28 — extend the domain, and never drop silently. 58 of 110 stageable participants in the RhoInc data sit below the 0.3 mg/dL trigger and 21 of them are negative — a fifth of the cohort would leave the plot under the source's y-limits, unannounced. They are the reference cloud that makes the flagged corner readable. Where a record genuinely cannot be plotted it is counted and exportable, following hep-explorer's HEP-DROP-* pattern — a note giving the count plus a CSV download of exactly which records left. Nine modules already carry some form of this; hep-explorer's is the richest and the one to copy.
D7 — How baseline is identified · DECIDED 2026-07-28. An explicit baseline-flag setting with a fallback · earliest study day · lowest visit number.
DECIDED 2026-07-28 — baseline_col / baseline_value, falling back to the earliest post-sort record. hep-explorer already carries exactly this pair (baseline_col: null, baseline_value: 'Y'), so nep-explorer inherits a vocabulary reviewers know. When no baseline column is configured or a participant has no flagged record, fall back to the earliest record by study day, then visit number, then input order — and count participants resolved by fallback so the number is visible rather than assumed. Neither demo dataset ships a baseline flag (the RhoInc set's is derived downstream, in nepExplorer's own adlb), so the fallback is the path the demo actually exercises.
The fold-change axis is a ratio and therefore unit-free. Everything else on this chart is not: 0.3 mg/dL and 4.0 mg/dL are absolute quantities, and creatinine is reported in µmol/L across most of the world — including in both demo datasets. The conversion is 1 mg/dL = 88.4 µmol/L.
Three requirements follow:
μmol/L with U+03BC (Greek small mu); pharmaverseadam writes umol/L. Match case-insensitively after folding µ/μ/u and trimming whitespace.units: {
target: 'mg/dL',
factors: { 'mg/dl': 1, 'umol/l': 1 / 88.4 } // keys are normalized before lookup
}
Sponsor-specific factors vary, which nepExplorer itself warns about, so factors is a setting rather than a constant.
Phase 1 needs one tall lab domain and nothing else. Phase 2 adds vitals (blood pressure) and the kidney-specialized measures.
| Setting | Default | Phase | Used for |
|---|---|---|---|
id_col | USUBJID | 1 | One point per participant; the selection key |
measure_col | TEST | 1 | Matching the creatinine records via measure_values.CREAT |
value_col | STRESN | 1 | The numeric result; non-numeric rows drop with a counted note |
unit_col | STRESU | 1 | Per-record conversion to mg/dL (§4) |
baseline_col / baseline_value | null / 'Y' | 1 | Baseline identification, with the D7 fallback |
visit_col / visitn_col | VISIT / VISITNUM | 1 | Ordering; the baseline fallback; the tooltip's "max at visit" |
studyday_col | DY | 1 (optional) | The tooltip's "max on study day"; ordering preference |
measure_values | { CREAT: 'Creatinine' } | 1 | The measure map; grows to the profile panel in Phase 2 |
arm_col, filters, details | ARM, [], null | 1 | Shell filters and the listing columns, per the house pattern |
age_col / sex_col | AGE / SEX | 2 | CKD-EPI eGFR derivation |
Phase 1 requires only id, measure, value, unit and a way to find baseline. That is a deliberately small contract — it is what makes the KDIGO scatter portable to a study that has nothing but a chemistry panel.
The assessment recorded Phase 1 as "demoable on today's data" because creatinine is present. It is present, and the chart renders — but it does not demonstrate. Computed over the current site/data/adbds.csv:
| pharmaverseadam (current demo) | RhoInc renderer-specific | |
|---|---|---|
| Participants with baseline + post-baseline creatinine | 208 | 110 |
| Maximum fold change observed | 1.45× | 5.60× |
| Fold-change stages populated (0 / 1 / 2 / 3) | 208 / 0 / 0 / 0 | 73 / 24 / 10 / 3 |
| Participants above the 0.3 mg/dL trigger | 9 of 208 | 52 of 110 |
| Maximum absolute change | 0.60 mg/dL | 1.14 mg/dL |
On the current demo data every point lands in the white no-stage box, the three coloured zones stay empty, and the summary table reads 208 / 0 / 0 / 0. The renderer would ship with a gallery hero that shows a blob in a corner and an evidence page that cannot exercise the staging at all — the same "the demo has no signal" problem already filed for the hepatic data as safety.viz#89, but total rather than partial.
D8 — Which dataset backs the Phase-1 demo · DECIDED 2026-07-28. Keep pharmaverseadam adbds.csv as the assessment assumed · build a nep-specific site/data/adnep.csv from the RhoInc source · inject a deterministic synthetic AKI cohort into the shared adbds.csv, the way build-hep-composite-cohort.mjs injects the CLD-* chronic-liver-disease cohort.
DECIDED 2026-07-28 — the synthetic AKI cohort, injected into the shared adbds.csv, over the recommended adnep.csv. This is the house mechanism, it is exactly what safety.viz#89's DEMO-3 asks for across every under-fed demo, and it keeps DEMO-4's one versioned extract intact instead of making the nep renderer the fourth exception to it.
The recommendation was adnep.csv, on three grounds — real data rather than simulated injury, a single answer to Phase 1 and Phase 2's data question, and a PR that does not touch ten other renderers' evidence. Two of those become live consequences of the decision and are planned for in §5.3 and §5.4 rather than discovered later. The third is a genuine gain: a synthetic cohort can be designed to exercise the chart, which real data cannot be. In particular it can carry a participant who trips the ≥ 4.0 mg/dL rule (D5) — something neither real dataset can do, since the RhoInc maximum is 1.93 mg/dL — turning a unit-test-only branch into demo-visible behaviour.
A deterministic AKI cohort injected into site/data/adbds.csv after the main pharmaverseadam build, on the mechanism scripts/build-hep-composite-cohort.mjs already uses: fixed seed, idempotent, clearly-labelled participant IDs (AKI-* beside the existing CLD-*), labelled site and arm, provenance in docs/DATA_SOURCES.md.
What the cohort has to contain, given the decisions above — this is a chart-driven spec, not a plausible-population one:
adbds.csv has no such column, so the demo runs on D7's fallback, which is the path most real studies will take too.Plan for the blast radius. adbds.csv is shared by ten renderers, so injecting rows regenerates canonical Linux evidence baselines across all of them inside a PR that is otherwise about the kidney. That is the accepted cost of the CLD-* mechanism and #89's DEMO-6 asks for exactly this regeneration — but it belongs in the PR description up front, not as a surprise in the diff. Two further guards worth having: the injected participants must not perturb the other renderers' displays beyond the new rows (the CLD-* precedent shows how), and the cohort's data-shape assertions (the stage cases exist and land where intended) are their own tests, per DEMO-6.
D8 settles Phase 1 and leaves Phase 2 where D1 put it: the RhoInc renderer-specific dataset — RhoInc/data-library, MIT-licensed, and safety.viz's own demo data before the v1.1.0 pharmaverse migration. It is the only source here of the measures pharmaverseadam lacks: cystatin C, eGFR, eGFRcys, urine albumin/creatinine and bicarbonate, alongside creatinine, BUN, the electrolytes and blood pressure. Two things to carry forward when Phase 2 starts:
AGE and DY, which the v1.0.0 projection dropped — CKD-EPI needs age and sex, and the profile needs a study-day axis.But it solves the availability gap and exposes a coherence gap. Pairing every measure by participant and visit:
| Pair | n | Pearson r | Expected in a real cohort |
|---|---|---|---|
| Creatinine vs eGFR | 517 | 0.005 | strongly negative — eGFR is a function of creatinine |
| Cystatin C vs eGFRcys | 582 | −0.023 | strongly negative, same reason |
| Creatinine vs cystatin C | 577 | −0.054 | positive — two markers of the same filtration |
| eGFR vs eGFRcys | 523 | −0.023 | strongly positive |
The measures were simulated independently. The medians are plausible — the shipped eGFR has median 88.8, a CKD-EPI-2021 recomputation from the same file's creatinine gives 85.5 — which is exactly why this needed checking per record rather than by summary. And the unit column confirms the columns were never derived: it labels eGFR μmol/L, which is not a filtration rate at all; the values (median 88.9, range 21–186) are mL/min/1.73m².
The Phase-1 scatter is untouched by this — it reads creatinine and nothing else. The Phase-2 profile is not: as shipped, it would show a participant whose creatinine tripled beside a flat eGFR panel and an unrelated cystatin C trace. That is a demo that teaches the wrong thing.
D9 — eGFR in the nep demo (refines D1) · DECIDED 2026-07-28. Derive eGFR and eGFRcys with CKD-EPI 2021 from the file's own creatinine / cystatin C, age and sex, and drop the incoherent shipped columns · ship the source columns as they are with a caveat on the demo page · regenerate the whole kidney panel synthetically from one underlying filtration trajectory.
DECIDED 2026-07-28 — derive and drop, for Phase 2; nothing needed for Phase 1. Deriving makes eGFR coherent with the creatinine the scatter stages, by construction, and it is what D1 already chose — this only adds "and therefore the shipped columns go". It does not fix creatinine vs cystatin C, which stays near zero and would need the third option to repair; that is a bigger piece of work than Phase 2 should carry, and the honest interim is a line on the demo page saying the cystatin C panel is illustrative. Worth confirming at the Phase-2 gate rather than now, since Phase 1 does not depend on it.
src/nep-explorer.js plus src/nep-explorer/, following the delta-delta / hep-explorer layout exactly: checkInputs → configure → structureData → getScales / getPlugins → new Chart, over the shared shell, with the standard lifecycle (init / setData / setSettings / render / resize / destroy) and registration in src/main.js as nepExplorer.
src/nep-explorer.js public factory + lifecycle + shell wiring
src/nep-explorer/
configure.js DEFAULT_SETTINGS, syncSettings, the stages + units settings
checkInputs.js required columns, the creatinine measure, unit resolvability
structureData.js records → one staged participant per point
getScales.js axis domains, the 3.5× / 0.3-visible floors, tick labels
getPlugins.js stageZonesPlugin, the ≥4 mg/dL mark, selection borders
src/data/schema/nep-explorer.json the data schema, per the house convention
One reduce per setData, producing one row per participant:
{ id, baseline, baselineVisit, max, maxVisit, maxDay,
fold, delta, // delta in mg/dL, or null when the unit is unknown
foldStage, deltaStage, stage, // stage = worse of the two, plus the ≥4 rule
absoluteRule } // true when max ≥ 4.0 mg/dL
Records drop, counted and exportable, when: the value is non-numeric; the participant has no resolvable baseline; the participant has no post-baseline record. The unit is resolved per record before any comparison (§4).
[0, max(3.5, observed)] so all three fold cut-points are always on screen — the R chart's floor, worth keeping because it is what makes an all-Stage-0 study readable as "nothing here" rather than "nothing plotted". y = absolute change in mg/dL, domain [min(0, observed), max(0.4, observed)] per D6. Ticks are labelled at the cut-points (1.5×, 2.0×, 3.0×; 0.3 mg/dL).stageZonesPlugin — a beforeDatasetsDraw plugin painting the zones from §3's geometry, largest stage first, so points are always drawn on top. Same hook and the same "read the live scales, paint in pixel space" technique as hep-explorer's quadrantPlugin and outlier-explorer's normal-range band; the difference is filled regions rather than cut-lines. Zone labels sit in the left margin of each band and can be hidden, as quadrant_labels already does.#ffeda0 / #feb24c / #f03b20). Stage is ordinal severity, so it takes the house status ramp rather than a categorical slot, at a background-appropriate opacity, and every zone is labelled — never colour alone.participantsSelected event — which is the seam Phase 2 mounts onto, so Phase 1 wires it even though the profile is not built yet.The R app's table is not a cross-tabulation, as the assessment described it — it is two marginal distributions sharing a stage row label: Stage 0–3 down the side, then N and % for the fold-change staging and N and % for the delta staging. Keep that shape, in the shell's listing area, with a third pair for the combined stage (the one the zones show), because with D4's ladder the combined column is what a reviewer reads off the chart.
Not in scope for Phase 1 and gated on D9, but two things belong on the record now because they change what Phase 1 should leave behind.
It should be the participant-profile module, not a bespoke panel. #75 shipped the shared profile as a right rail with a labs-over-time chart, a measure table and an AE block, adopted by eight renderers. nepExplorer's profile is five small-multiple panels grouped by measure family (creatinine + cystatin C, eGFR + eGFRcys, related electrolytes, blood pressure, urine ACR) with KDIGO reference lines. The right question at the Phase-2 gate is whether that becomes a measure-group option on the shared profile rather than a second profile implementation. Phase 1 keeps the seam open by dispatching the standard selection event.
A third source trap, for whoever picks Phase 2 up. drawPercentChange plots (value − baseline) / baseline — a percent change, where a 1.5× rise is 0.5 — and then draws its KDIGO reference lines at y = 1.5, 2 and 3, which on that scale are 2.5×, 3× and 4×. The "KDIGO Stage 1" line is drawn where Stage 2 nearly begins. Whichever scale Phase 2 picks, the thresholds have to be expressed on the same one.
Each row is meant to be shippable on its own. The whole of Phase 1 is one safety.viz implementation issue (drafted, awaiting the design gate) and, per the one-PR-per-session convention, likely one PR to dev.
| # | Increment | Depends on |
|---|---|---|
| 1 | Synthetic AKI cohort generator (§5.3) + DATA_SOURCES.md section + data-shape assertions + regenerated baselines for every adbds.csv consumer (D8) | — |
| 2 | Module skeleton: settings, checkInputs, schema, shell, registration in main.js | — |
| 3 | Unit resolution and the mg/dL contract, including the refuse-to-guess path (§4) | 2 |
| 4 | structureData: baseline resolution (D7), per-participant maxima, staging (D4, D5), dropped-record accounting (D6) | 3 |
| 5 | Scales, stageZonesPlugin, marks, tooltip, selection dispatch | 4 |
| 6 | Summary table in the listing area (§6.4) | 4 |
| 7 | requirements/nep-explorer.md matrix (§9) | — |
| 8 | Done gate: gallery demo + guide page, evidence page with Linux baselines, API reference, coverage doc | all |
One evidence note. The unknown-unit fallback (§4) cannot be reached from a demo whose units are all known, so it needs a unit test on synthetic input — the kind of branch that ships untested precisely because the demo looks complete. The ≥ 4.0 mg/dL rule was the other one, and D8 removes it from that list: the cohort is specified to carry a participant who trips it, so it earns browser evidence like everything else.
safety.viz/requirements/nep-explorer.md, on the NEP-<AREA>-<NNN> base-ID scheme so the evidence pages resolve each row by exact ID, structured like qt-explorer.md — the closest precedent, being the other phased port of a SafetyGraphics Shiny app.
| Area | Covers | Increment |
|---|---|---|
NEP-CFG-* | Settings, defaults, the stages and units objects, measure_values resolution | 2, 3 |
NEP-UNIT-* | Per-record conversion, string normalization, the unknown-unit suppression path | 3 |
NEP-DATA-* | Baseline resolution and fallback, per-participant maxima, dropped records and their export | 4 |
NEP-STAGE-* | Fold ladder, the 0.3 mg/dL trigger, the ≥ 4.0 mg/dL rule, the combined stage | 4 |
NEP-ZONE-* | Zone geometry and paint order, labels, the axis floors, colour and labelling rules | 5 |
NEP-SCAT-* | Marks, tooltip contents, selection and the dispatched event | 5 |
NEP-TBL-* | Summary table rows, the three N/% pairs, empty-stage rows | 6 |
Where a row exists because the port diverges from the R source — the staging ladder, the negative-delta domain, the mark-based ≥ 4 rule — the matrix row says so in its Notes, with §3 as the citation. That is the record that keeps "we fixed their bug" from reading like "we got it wrong" at review time three months from now.
CLD-* precedent means the mechanism and its labelling are proven, but the guide page should be as plain about it as the hepatic composite view is — and the RhoInc set (§5.4) remains the real-data fallback if the synthetic cloud reads as too tidy.adbds.csv regenerates canonical baselines across ten modules. That is expected and #89 asks for it, but it makes the diff large and the review harder, and it couples this work to whatever else is in flight against those baselines. Sequencing it against #89 rather than racing it is the cheap mitigation.