Patient Journey Explorer: test evidence Prototype

Requirement-traced qualification evidence for the safety.viz patient-journey-explorer module.

Scope
61 coverage rows61 distinct requirement IDs
Tests executed
1069 automated checks133 browser Β· 936 unit
Result
pass all passing17 evidence screenshots
Generated
2026-10-04 12:03 UTC
Environment
linux 6.17.0-1022-azure Β· node v22.23.3 Β· playwright 1.61.1 Β· chromium 149.0.7827.55

Scope & approach

Traceability for the patient-journey-explorer module (the Patient Journey Explorer β€” one participant's whole safety record on one shared study-day axis, with time anchorable on any event and a mechanical context panel β€” under #142; parent requirement obot.roadmap#349, R widget follow-on obot.roadmap#350), per the convention in CONTRIBUTING.md. The reviewed source matrix is requirements/patient-journey-explorer.md in this repo, and each row below cites the matrix rows its test covers.

Requirement IDs use the module's condensed PJE-* scheme cited in the source and test names β€” PJE-CFG-* (settings, the camelCase alias contract, lane and window coercion), PJE-DATA-* (both input forms, the per-domain drop table, the data contract, fallback day columns, end-before-start), PJE-SUBJ-* (the subject picker), PJE-LANE-* (the seven lanes: order, toggles, groups, alignment by construction, the labs small multiples, exposure segmentation, empty and unplaceable states, fit-to-height, the row cap), PJE-DERIV-* (dose changes and the lab baseline), PJE-ANCH-* (anchoring, the elapsed-day window and the "days from anchor" rule), PJE-CTX-* (the four context queries), PJE-PANEL-* (the side panel), PJE-SRC-* (source-row traceability), PJE-FILT-* (the three filters), PJE-TIME-* (study day ↔ calendar date), PJE-EVT-* (the three-channel event surface), PJE-KEY-* (the keyboard model and the live region), PJE-ACC-* (the palette gate, redundant encoding, rendered text contrast β€” the area token is letters only, because the extractor's ID grammar admits no digits), and PJE-DEMO-* (the vendored CDISC Pilot 01 extracts, the built demo page, and the Definition of Done executed against it).

Two things about how the browser rows are read. Every synthetic-data assertion in tests/e2e/patient-journey-explorer.spec.js reads its expected numbers from window.PJE_FIXTURE.expectations (the fixture's single source of truth) and never from a literal, so the spec and the fixture cannot disagree. And the browser suite asserts against the module's own recorded geometry (chart.$pjeMarks, chart.$pjeBand) and its documented read model (instance.getContext(), instance.getTimeMode(), instance.laneCharts) rather than reading pixels or private state. The pilot-data numbers live only in PJE-DEMO-003, which runs against the built demo page in site.spec.js.

Each table row traces one requirement to the automated test(s) that evidence it: the Requirement column shows the reviewed requirement text and its ID, the source-matrix rows link back to the specification, the issue column links the implementing work, and the result column shows the recorded outcome of every matching test from the committed evidence.json with its captured screenshots. Browser evidence is captured at fixed conditions (1280Γ—800, device scale 1) on the canonical Linux CI environment.

Browser evidence (Playwright β€” tests/e2e/patient-journey-explorer.spec.js, tests/e2e/site.spec.js)

29 requirement rows Β· 29 tests

RequirementSource matrix rowsIssueTests & evidence
PJE-SUBJ-001

PJE-SUBJ-001 The subject selector lists every participant present in any domain, sorted, and opens on the first (or on the configured subject).

PJE-SUBJ-001#142
  • pass PJE-SUBJ-001: the subject selector lists every participant and opens on the first (#142)
Evidence screenshot PJE-SUBJ-001-subject-journey.png
PJE-SUBJ-002

PJE-SUBJ-002 A search box filters the subject list case-insensitively by substring; selecting a subject redraws every lane, clears any anchor, and announces the new subject.

PJE-SUBJ-002#142
  • pass PJE-SUBJ-002: filtering the subject list and selecting a subject redraws every lane (#142)
PJE-LANE-001

PJE-LANE-001 One lane renders per enabled domain in the fixed order exposure, dose changes, adverse events, labs, con-meds, medical history, disposition, each with a permanent text label in the left gutter.

PJE-LANE-001#142
  • pass PJE-LANE-001: every enabled domain renders a labelled lane in the fixed order (#142)
Evidence screenshot PJE-LANE-001-lanes-all.png
PJE-LANE-004

PJE-LANE-004 Every lane chart shares one x-axis minimum and maximum and one pinned y-axis width, so the lanes align by construction rather than by measurement.

PJE-LANE-004#142
  • pass PJE-LANE-004: every lane shares one x domain and one y width (#142)
PJE-LANE-002

PJE-LANE-002 A lane toggle hides and shows its lane without changing the shared day domain, so the other lanes do not move.

PJE-LANE-002#142
  • pass PJE-LANE-002: toggling a lane hides it and leaves the shared domain unchanged (#142)
Evidence screenshot PJE-LANE-002-lane-toggled.png
PJE-LANE-003

PJE-LANE-003 Lanes are grouped into collapsible groups with aria-expanded disclosure buttons; collapsing a group hides its lanes and keeps the axis fixed.

PJE-LANE-003#142
  • pass PJE-LANE-003: collapsing a lane group hides its lanes (#142)
PJE-LANE-005

PJE-LANE-005 The labs lane renders one small-multiple sparkline per configured test on the shared day axis, with the reference range drawn as a band and out-of-range points marked by shape as well as colour.

PJE-LANE-005#142
  • pass PJE-LANE-005: the labs lane draws one sparkline per configured test with its reference band (#142)
Evidence screenshot PJE-LANE-005-labs-small-multiples.png
PJE-LANE-006

PJE-LANE-006 Exposure bars segment at each dose change, and each change carries a direction glyph (increase, reduction, interruption, restart) rather than being encoded by colour.

PJE-LANE-006#142
  • pass PJE-LANE-006: exposure bars segment at each dose change and each change carries a direction glyph (#142)
Evidence screenshot PJE-LANE-006-dose-change.png
PJE-LANE-009

PJE-LANE-009 The opening participant's enabled lanes fit the configured panel height without vertical scrolling: heights scale down to their floors first, and when even the floors do not fit the panel scrolls and says so rather than cropping.

PJE-LANE-009#142
  • pass PJE-LANE-009: the opening participant's journey fits the panel without scrolling (#142)
Evidence screenshot PJE-LANE-009-one-screen.png
PJE-LANE-010

PJE-LANE-010 A lane whose rows exceed the row cap draws a deterministic subset in the lane's documented sort order and names both the remainder and the sort rule in prose, and the anchor panel reconciles its counts against what was drawn.

PJE-LANE-010#142
  • pass PJE-LANE-010: a lane over its row cap draws a deterministic subset and says so (#142)
PJE-LANE-007

PJE-LANE-007 A subject with no adverse events renders the adverse-event lane with an explanatory note rather than removing the lane or failing.

PJE-LANE-007#142
  • pass PJE-LANE-007: a subject with no adverse events renders the lane with a note (#142)
Evidence screenshot PJE-LANE-007-empty-ae-lane.png
PJE-ANCH-001

PJE-ANCH-001 Activating any placeable mark anchors time on it: the axis relabels to days from the anchor, the anchor reference line is drawn on every lane, and anchor(null) clears it. Anchoring never re-ranges or zooms the shared day domain β€” the whole journey stays visible.

PJE-ANCH-001#142
  • pass PJE-ANCH-001: clicking an adverse event anchors time on it and leaves the day domain unchanged (#142)
Evidence screenshot PJE-ANCH-001-anchored.png
PJE-ANCH-003

PJE-ANCH-003 Marks inside the window are drawn at full strength inside a highlighted band; marks outside it are de-emphasized while keeping their hit target, tooltip, tab order and full-strength listing in the source drawer.

PJE-ANCH-003#142
  • pass PJE-ANCH-003: in-window marks stay full strength and out-of-window marks de-emphasize (#142)
Evidence screenshot PJE-ANCH-003-context-window.png
PJE-PANEL-001

PJE-PANEL-001 The side panel lists the four context kinds with counts, an explicit empty state per kind, the not-evaluated counters, and a standing sentence that co-occurrence is not causation.

PJE-PANEL-001#142
  • pass PJE-PANEL-001: the side panel lists the four context kinds with counts (#142)
Evidence screenshot PJE-PANEL-001-context-panel.png
PJE-SRC-001

PJE-SRC-001 Every mark and every panel item links back to its source row by a real control, not only by a named id: the drawer shows the raw object as supplied under a stable anchor id, and both the mark's own open-source control and a panel item scroll that row into view and focus it.

PJE-SRC-001#142
  • pass PJE-SRC-001: both a mark and a panel item jump to their source row (#142)
Evidence screenshot PJE-SRC-001-source-row.png
PJE-DATA-003

PJE-DATA-003 Rows that cannot be used are dropped with a per-domain named reason, counted by domain and reason on the instance, reported in the notes, and exportable as CSV led by the reason column β€” nothing is dropped silently.

PJE-DATA-003#142
  • pass PJE-DATA-003: unusable rows are counted in the notes and exportable (#142)
Evidence screenshot PJE-DATA-003-dropped-rows.png
PJE-DATA-005

PJE-DATA-005 An absent domain is not an error: its lanes render an explanatory empty state and their toggles are disabled, so a partial journey still draws.

PJE-DATA-005#142
  • pass PJE-DATA-005: an absent domain renders an explanatory empty lane and a disabled toggle (#142)
PJE-FILT-001

PJE-FILT-001 The serious-only filter restricts the adverse-event lane to serious events and leaves the other lanes untouched.

PJE-FILT-001#142
  • pass PJE-FILT-001: serious only restricts the adverse-event lane and nothing else (#142)
Evidence screenshot PJE-FILT-001-serious-only.png
PJE-FILT-003

PJE-FILT-003 The con-med class filter is a multiselect over the recorded classes, with uncoded records kept as their own selectable bucket rather than hidden.

PJE-FILT-003#142
  • pass PJE-FILT-003: the con-med class multiselect filters the lane and keeps UNCODED selectable (#142)
PJE-TIME-001

PJE-TIME-001 The display switches between study day and calendar date, relabelling ticks, tooltips and panel text while the underlying scale stays linear in days.

PJE-TIME-001#142
  • pass PJE-TIME-001: switching to calendar date relabels ticks and tooltips (#142)
Evidence screenshot PJE-TIME-001-date-mode.png
PJE-EVT-001

PJE-EVT-001 Subject selection, anchoring and context changes are delivered three ways β€” settings callbacks, instance listeners and a DOM CustomEvent on the shell root β€” carrying the whole context bundle, so a consumer never reads the DOM.

PJE-EVT-001#142
  • pass PJE-EVT-001: anchoring emits the context bundle on all three channels (#142)
PJE-KEY-001

PJE-KEY-001 Every drawn mark is a real focusable control with an accessible name naming its domain, label, days and flags; one mark per lane is in the tab order and a visible two-ring focus indicator is drawn.

PJE-KEY-001#142
  • pass PJE-KEY-001: marks are focusable controls with accessible names and a visible focus ring (#142)
Evidence screenshot PJE-KEY-001-mark-focus.png
PJE-KEY-002

PJE-KEY-002 Enter or Space on a focused mark anchors time on it and re-activating clears it; Escape clears the anchor.

PJE-KEY-002#142
  • pass PJE-KEY-002: Enter anchors the focused mark and Escape clears it (#142)
PJE-KEY-003

PJE-KEY-003 Arrow keys move chronologically within a lane and to the nearest mark in the adjacent lane, and a polite live region announces the subject, the anchor and the context counts.

PJE-KEY-003#142
  • pass PJE-KEY-003: arrows move within and between lanes and the live region announces (#142)
PJE-KEY-004

PJE-KEY-004 Focus is restored onto the recreated control after any rebuild, so filtering or toggling a lane does not drop the keyboard user to the top of the page.

PJE-KEY-004#142
  • pass PJE-KEY-004: focus is restored after a filter rebuild (#142)
PJE-ACC-002

PJE-ACC-002 No encoding is carried by colour alone: severity varies bar height and border weight at a single opaque fill, an unrecorded severity draws a distinct hatched mark named in its accessible name, seriousness adds a filled start dot and an SAE tag, lab direction is a glyph, and dose direction is a caret shape.

PJE-ACC-002#142
  • pass PJE-ACC-002: no encoding is colour-alone (#142)
PJE-ACC-003

PJE-ACC-003 Module text meets AA at its rendered size: every text node the module renders measures at least 4.5:1 against its computed background in the browser, and the library's #7b8b96 muted ink, which measures 3.51:1, appears nowhere in the module's source.

PJE-ACC-003#142
  • pass PJE-ACC-003: module text meets AA at its rendered size (#142)
Evidence screenshot PJE-ACC-003-contrast.png
PJE-DEMO-002

PJE-DEMO-002 The built demo page mounts against the vendored extracts with no console errors and opens on the seeded subject.

PJE-DEMO-002#142
  • pass built patient-journey-explorer demo mounts the shared shell with no console errors (#7) (#17)
PJE-DEMO-003

PJE-DEMO-003 Against the vendored CDISC Pilot 01 extracts, anchoring the seeded participant's day-30 adverse event produces the panel counts the guide documents, with each item linking back to its source row β€” the Definition of Done, executed.

PJE-DEMO-003#142
  • pass PJE-DEMO-003: the seeded CDISC Pilot 01 participant's day-30 anchor produces the documented context (#142)
Evidence screenshot PJE-DEMO-003-pilot-anchor.png

Unit evidence (Vitest β€” tests/unit/patient-journey-explorer/)

32 requirement rows Β· 202 tests

RequirementSource matrix rowsIssueTests & evidence
PJE-CFG-001

PJE-CFG-001 The default settings carry a per-domain column mapping for all six domains (EX, AE, LB, CM, MH, DS) plus the identity, time, window, lane and filter configuration; every column is overridable.

PJE-CFG-001#142

configure.test.js β€” every documented column default present and snake_case

  • pass DEFAULT_SETTINGS PJE-CFG-001: carries a column mapping for all six domains, every key snake_case and domain-prefixed (#142)
  • pass DEFAULT_SETTINGS PJE-CFG-001: every column setting is overridable through syncSettings (#142)
  • pass DEFAULT_SETTINGS PJE-CFG-001: the documented column defaults are present with their documented values (#142)
  • pass DEFAULT_SETTINGS PJE-CFG-001: the seven lanes, three lane groups and three filters are the documented defaults (#142)
PJE-CFG-002

PJE-CFG-002 The plan's camelCase setting spellings (contextWindowDays, onAnchorEvent, sourceUrlTemplate, lane-level severityCol/seriousCol/tests) are accepted as aliases and normalized onto the canonical snake_case keys; the canonical key wins when both are given.

PJE-CFG-002#142

configure.test.js β€” the full alias accept and reject lists; the plan's literal settings object yields three live filters and a day column per domain

  • pass aliases (PJE-CFG-002) PJE-CFG-002: every top-level camelCase alias maps onto its snake_case key with one warning each (#142)
  • pass aliases (PJE-CFG-002) PJE-CFG-002: filters[].col maps to value_col and the literal plan Β§7 object yields three live filters (#142)
  • pass aliases (PJE-CFG-002) PJE-CFG-002: lane-level severityCol, seriousCol and tests are lifted to the top level; domain, derivedFrom and smallMultiple are ignored (#142)
  • pass aliases (PJE-CFG-002) PJE-CFG-002: the canonical key wins silently when both it and its alias are given (#142)
  • pass aliases (PJE-CFG-002) PJE-CFG-002: the plan Β§7 object gives every domain a resolvable day column and lifts the lane-level columns (#142)
  • pass aliases (PJE-CFG-002) PJE-CFG-002: time.day_col without "--" is rejected with a named warning and changes nothing (#142)
  • pass aliases (PJE-CFG-002) PJE-CFG-002: unknown time keys, filters without a column and unknown lanes are rejected with a named warning each (#142)
PJE-CFG-003

PJE-CFG-003 lanes merges key by key so a partial override back-fills; a lane set to null is disabled; an unknown lane key is dropped with a console warning.

PJE-CFG-003#142

configure.test.js β€” partial lane override back-fills, a null lane disables, an unknown lane warns and is dropped

  • pass coercion (PJE-CFG-004 and merge rules) PJE-CFG-003: a lane naming a group that is not a lane_groups key warns and falls back β€” to its default group, else the first configured group (#142)
  • pass coercion (PJE-CFG-004 and merge rules) PJE-CFG-003: a lane_groups list that drops a default group leaves no lane orphaned β€” every lane belongs to a configured group (#142)
  • pass lanes and lane groups (PJE-CFG-003) lane_groups normalize to { key, label, collapsed } and fall back to the defaults when unusable (#142)
  • pass lanes and lane groups (PJE-CFG-003) PJE-CFG-003: a lane set to null is disabled and keeps its label (#142)
  • pass lanes and lane groups (PJE-CFG-003) PJE-CFG-003: a partial lane override back-fills label and group from the defaults (#142)
  • pass lanes and lane groups (PJE-CFG-003) PJE-CFG-003: an unknown lane key is dropped with a console warning and enabled is boolean-coerced (#142)
PJE-CFG-004

PJE-CFG-004 context_window_days is coerced to a non-negative integer, defaulting to 30; zero is a legal window meaning the anchor day only.

PJE-CFG-004#142

configure.test.js β€” the window coercion table, zero legal

  • pass coercion (PJE-CFG-004 and merge rules) callbacks are kept only when they are functions (#142)
  • pass coercion (PJE-CFG-004 and merge rules) ds_reference_cats and the ongoing-value lists are upper-cased and trimmed; an empty ds_reference_cats warns once (#142)
  • pass coercion (PJE-CFG-004 and merge rules) filters normalize through normalizeFilterSpec keeping domain, type and flag_value, and drop specs without a recognized domain (#142)
  • pass coercion (PJE-CFG-004 and merge rules) layout numbers coerce to positive integers with their documented fallbacks and fit_to_height to a boolean (#142)
  • pass coercion (PJE-CFG-004 and merge rules) lb_change_factor must be a finite number above 1 and lb_baseline_day a finite integer (#142)
  • pass coercion (PJE-CFG-004 and merge rules) lb_tests and ae_severity_values arrayify to strings; an empty lb_tests means every test and an empty severity list falls back (#142)
  • pass coercion (PJE-CFG-004 and merge rules) mh_day_source accepts only 'onset' exactly and otherwise falls back to 'collection' with a warning (#142)
  • pass coercion (PJE-CFG-004 and merge rules) PJE-CFG-003: a lane naming a group that is not a lane_groups key warns and falls back β€” to its default group, else the first configured group (#142)
  • pass coercion (PJE-CFG-004 and merge rules) PJE-CFG-003: a lane_groups list that drops a default group leaves no lane orphaned β€” every lane belongs to a configured group (#142)
  • pass coercion (PJE-CFG-004 and merge rules) PJE-CFG-004: context_window_days coerces to a non-negative integer defaulting to 30; zero is legal (#142)
  • pass coercion (PJE-CFG-004 and merge rules) PJE-DATA-007: every day column accepts a single name or a fallback chain and syncs to an array (#142)
  • pass coercion (PJE-CFG-004 and merge rules) time deep-merges key by key and date mode is honoured only when allow_date_mode is true (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-CFG-004: a cleared context-window field restores the configured width rather than collapsing the window to the anchor day (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-CTX-001: a lane turned off or a filter never changes the context lists; the panel says why a listed record is not drawn and that the anchor is hidden (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-DATA-004: after init() throws on invalid data, a later init() with valid data draws into the shell again, in the host element (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-DATA-004: the checkInputs message is inserted as text, never as markup (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-FILT-004: the absent-column filter warning and the source_url_template warning are printed once per data load, not once per interaction (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-KEY-002: Escape inside the subject search or the window field keeps its native meaning and does not clear the anchor (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-KEY-004: clearing the anchor from its own button, or resetting, never drops focus to the document body (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-LANE-003: with every lane turned off the stack says so and the anchoring hint is not shown (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-SUBJ-003: the footnote is cleared when its mark leaves the page β€” another subject, its lane off β€” and rewritten when the anchor changes (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-TIME-001: date mode with no reference date shows study days everywhere β€” title, select, getTimeMode β€” and keeps the preference for a subject that has one (#142)
Evidence screenshot PJE-TIME-001-date-mode.png
PJE-SUBJ-003

PJE-SUBJ-003 selectSubject(id) selects programmatically; an unknown id changes nothing and warns.

PJE-SUBJ-003#142

events.test.js β€” selectSubject with an unknown id leaves the subject unchanged and warns once; a known id selects it and clears the anchor first

  • pass selectSubject (PJE-SUBJ-003) PJE-SUBJ-003: a known id selects it, clears the anchor first, and the subject list follows (#142)
  • pass selectSubject (PJE-SUBJ-003) PJE-SUBJ-003: an unknown id changes nothing and warns once (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-SUBJ-003: the footnote is cleared when its mark leaves the page β€” another subject, its lane off β€” and rewritten when the anchor changes (#142)
Evidence screenshot PJE-TIME-001-date-mode.png
PJE-DATA-001

PJE-DATA-001 init accepts either a merged array carrying a domain column or an object of per-domain arrays, with domain keys and domain values matched case-insensitively; both forms normalize to the same internal model.

PJE-DATA-001#142

normalize.test.js β€” both input forms produce an identical domain map; case-insensitive keys and values, ADAE β†’ AE

  • pass detectDomain PJE-DATA-001: recognizes the six codes case-insensitively and the ADaM synonyms (#142)
  • pass normalizeInput PJE-DATA-001: form A keys are matched case-insensitively and unknown keys warn (#142)
  • pass normalizeInput PJE-DATA-001: form B domain values are upper-cased, trimmed and synonym-mapped (#142)
  • pass normalizeInput PJE-DATA-001: the per-domain object and the merged array produce the same domain map (#142)
PJE-DATA-002

PJE-DATA-002 A merged-array row whose domain value is missing or unrecognized is dropped with a named reason rather than being assigned to a default domain.

PJE-DATA-002#142

normalize.test.js β€” an unknown or blank domain value drops the row with its reason text

  • pass normalizeInput PJE-DATA-002: a merged row with a blank or unrecognized domain is dropped with a named reason (#142)
  • pass normalizeInput PJE-DATA-002: the merged array reads the configured domain column (#142)
PJE-DATA-004

PJE-DATA-004 Required columns are validated per supplied domain against the data contract, and every missing dataset column is named in one error whose message renders into the target element.

PJE-DATA-004#142

checkInputs.test.js β€” one error naming every missing column per supplied domain; the all-empty message

  • pass patient-journey-explorer checkInputs PJE-DATA-004: a missing id column in the AE domain is named as ae.USUBJID (#142)
  • pass patient-journey-explorer checkInputs PJE-DATA-004: a valid per-domain map with the required columns passes (#142)
  • pass patient-journey-explorer checkInputs PJE-DATA-004: all-empty input throws the "no usable data" message naming the domain column (#142)
  • pass patient-journey-explorer checkInputs PJE-DATA-004: every entry of DOMAINS resolves a lower-case schema property carrying requiredSettings (#142)
  • pass patient-journey-explorer checkInputs PJE-DATA-004: multiple misses across domains are named in one error, chains shown as their alternatives (#142)
  • pass patient-journey-explorer checkInputs PJE-DATA-004: remapped column settings are validated against the remapped names (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-CFG-004: a cleared context-window field restores the configured width rather than collapsing the window to the anchor day (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-CTX-001: a lane turned off or a filter never changes the context lists; the panel says why a listed record is not drawn and that the anchor is hidden (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-DATA-004: after init() throws on invalid data, a later init() with valid data draws into the shell again, in the host element (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-DATA-004: the checkInputs message is inserted as text, never as markup (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-FILT-004: the absent-column filter warning and the source_url_template warning are printed once per data load, not once per interaction (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-KEY-002: Escape inside the subject search or the window field keeps its native meaning and does not clear the anchor (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-KEY-004: clearing the anchor from its own button, or resetting, never drops focus to the document body (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-LANE-003: with every lane turned off the stack says so and the anchoring hint is not shown (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-SUBJ-003: the footnote is cleared when its mark leaves the page β€” another subject, its lane off β€” and rewritten when the anchor changes (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-TIME-001: date mode with no reference date shows study days everywhere β€” title, select, getTimeMode β€” and keeps the preference for a subject that has one (#142)
Evidence screenshot PJE-TIME-001-date-mode.png
PJE-DATA-006

PJE-DATA-006 The raw row objects passed to init are retained by reference and never mutated; every derived value lives on the module's own normalized record.

PJE-DATA-006#142

normalize.test.js β€” source is the identical input object and the inputs are deep-equal after normalization

  • pass normalizeDomain β€” the shared record shape PJE-DATA-006: an AE record carries the documented EventRecord fields and its source by reference (#142)
  • pass normalizeDomain β€” the shared record shape PJE-DATA-006: sourceIndex is the index within the domain array in both forms (#142)
  • pass normalizeInput PJE-DATA-006: form A keeps the caller arrays by reference and no row is mutated in either form (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-DATA-006: the raw rows are untouched and every event keeps its source by reference (#142)
Evidence screenshot PJE-DATA-003-dropped-rows.png
PJE-DATA-007

PJE-DATA-007 Each day column setting accepts an ordered fallback chain resolved per row, so both the analysis-day and the domain-day vocabularies place a record, and the column that resolved it is recorded on the event.

PJE-DATA-007#142

normalize.test.js β€” the day-column chain resolves per row and records dayCol; configure.test.js β€” a single name and a chain are both legal

  • pass coercion (PJE-CFG-004 and merge rules) PJE-DATA-007: every day column accepts a single name or a fallback chain and syncs to an array (#142)
  • pass normalizeDomain β€” the shared record shape PJE-DATA-007: the day chain resolves per row, left to right, and records the resolving column (#142)
  • pass normalizeDomain β€” the shared record shape PJE-DATA-007: the plan's SDTM MHSTDY places a medical-history row under mh_day_source: onset, through the default onset chain (D15) (#142)
  • pass patient-journey-explorer checkInputs PJE-DATA-007: a chain is satisfied by any one of its columns appearing in any row (#142)
PJE-DATA-008

PJE-DATA-008 A record whose end day precedes its start day is kept, drawn as a single-day mark, and counted in a named flag rather than being silently converted into an open-ended record.

PJE-DATA-008#142

normalize.test.js β€” an end day before the start day keeps the record as a single-day mark under a named flag

  • pass conMedsActiveAt (PJE-CTX-001, D8, D16) PJE-CTX-001: a con-med whose recorded end precedes its start is active only on its start day and is not counted as end-unrecorded (PJE-DATA-008) (#142)
  • pass normalizeDomain β€” interval end handling (D16) PJE-DATA-008: an end day before the start is kept as a single-day mark, unrecorded, and flagged with the exact reason (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-DATA-008: end-before-start and date-conflict records are kept and counted study-wide in flaggedCounts (#142)
Evidence screenshot PJE-DATA-003-dropped-rows.png
PJE-LANE-008

PJE-LANE-008 Records with no usable study day are kept and named in prose beneath their lane rather than dropped, so absence of a day is visible.

PJE-LANE-008#142

structureData.test.js β€” unplaceable rows counted per lane and excluded from the shared domain

  • pass buildLaneDatasets (PJE-LANE-005/006, PJE-ANCH-003) PJE-LANE-008: unplaceable events are left out of every dataset, and empty input yields no datasets (#142)
  • pass markGeometry (PJE-LANE-005/006) PJE-LANE-008: a clipped start draws from the domain edge, points are zero-width at their day, and an unplaceable event has no geometry (#142)
  • pass normalizeDomain β€” the shared record shape PJE-LANE-008: 'NA', blank and non-numeric days leave the record unplaceable but kept (#142)
  • pass normalizeDomain β€” the shared record shape PJE-LANE-008: study day 0 is not a day β€” a record whose only day is 0 (or '-0') is kept, unplaceable, and flagged dayZero (#142)
  • pass planLanes empty states (PJE-LANE-007, PJE-LANE-008) PJE-DATA-003: labs for tests outside lb_tests are named in the footer when the lane draws, and in the empty text when it does not (#142)
  • pass planLanes empty states (PJE-LANE-007, PJE-LANE-008) PJE-LANE-007: a participant with no records in a lane reads "No <domain> recorded", with no footer (#142)
  • pass planLanes empty states (PJE-LANE-007, PJE-LANE-008) PJE-LANE-007: records that exist but were all filtered out read "No records match the current filters" (#142)
  • pass planLanes empty states (PJE-LANE-007, PJE-LANE-008) PJE-LANE-008: a lane whose every record has no usable day does NOT say "no records" β€” it says no record has a usable day, and the footer names them (#142)
  • pass planLanes empty states (PJE-LANE-007, PJE-LANE-008) PJE-LANE-008: a record whose only day is 0 is named as such in the footer, apart from the blank-day records (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-DATA-003: a lab for a test outside lb_tests stays in the record β€” counted, in allEvents, named β€” but is never drawn, filtered, anchored, window-counted or part of the domain (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-DATA-003: dropped rows are aggregated study-wide by domain and by reason (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-DATA-005: a domain that was not supplied is marked so its lanes can render the empty state (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-DATA-006: the raw rows are untouched and every event keeps its source by reference (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-DATA-008: end-before-start and date-conflict records are kept and counted study-wide in flaggedCounts (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-FILT-001: events is the post-filter set over ENABLED lanes in lane order then day order; allEvents is pre-filter (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-FILT-002: the abnormal-only filter keeps the lab series baseline from the whole record (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-FILT-004: the live filter list drops absent columns with the house warning and their state is ignored (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-LANE-001: byLane carries every lane key in render order, sorted by the lane rule, with dose changes derived and labs limited to the configured tests (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-LANE-004: the shared domain is an elapsed pair, the clamped con-med is flagged clippedStart, and events carry the subject reference date (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-LANE-004: with nothing placeable the domain is null, the lab series carry a null domain, and lanes still list their unplaceable rows (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-LANE-008: unplaceable rows are kept, listed per lane, counted per domain and lane, and excluded from the domain (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-LANE-010: the row cap draws a deterministic head of the lane order, names the remainder and the sort rule, and carries the truncation into the context bundle (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-SUBJ-001: opens on the first subject in sorted order, honours settings.subject and state.subject, and falls back for an unknown id (#142)
  • pass tooltipLines (design Β§6.5) PJE-LANE-008: an unplaceable event says so instead of a span (#142)
Evidence screenshot PJE-ANCH-003-context-window.pngEvidence screenshot PJE-LANE-005-labs-small-multiples.pngEvidence screenshot PJE-DATA-003-dropped-rows.pngEvidence screenshot PJE-LANE-007-empty-ae-lane.pngEvidence screenshot PJE-FILT-001-serious-only.pngEvidence screenshot PJE-LANE-001-lanes-all.pngEvidence screenshot PJE-SUBJ-001-subject-journey.png
PJE-DERIV-001

PJE-DERIV-001 Dose changes are derived from consecutive exposure records per subject, ordered by start day, classified as increase, reduction, interruption or restart, and dated to the day the new dose begins.

PJE-DERIV-001#142

doseChanges.test.js β€” increase, reduction, interruption and restart at the new record's start day; 0β†’0β†’0 yields none

  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: 0 -> 0 -> 0 (placebo) yields no change (#142)
  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: 54 -> 81 is an increase dated to the day the new dose begins, not the day the old one ended (#142)
  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: a record with no usable start or a non-numeric dose is skipped, not paired (#142)
  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: an exposure gap at the same dose is not a change (#142)
  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: empty and non-array input yield [] (#142)
  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: records are walked in start order regardless of input order, and a same-day tie is broken by sourceIndex (#142)
  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: same-day duplicates at the same dose produce nothing (#142)
  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: the change carries both defining rows for the source drawer (#142)
  • pass deriveDoseChanges (PJE-DERIV-001) PJE-DERIV-001: the four directions β€” increase, reduction, interruption, restart (#142)
  • pass tooltipLines (design Β§6.5) PJE-DERIV-001: dose changes, exposure, medical history and disposition each read their domain word and detail (#142)
PJE-DERIV-002

PJE-DERIV-002 A lab test's baseline is the record flagged as the analysis baseline when the data carries that flag, falling back to the last value on or before the configured baseline day and then to the earliest recorded value, with the rule that fired recorded on the result.

PJE-DERIV-002#142

labs.test.js β€” flag-first baseline, the day-rule and earliest fallbacks, ties on the smaller sourceIndex, the rule recorded

  • pass buildLabSeries (PJE-LANE-005) PJE-DERIV-002: baselines come from baselineEvents (the whole record) when the drawn points are filtered (#142)
  • pass buildLabSeries (PJE-LANE-005) PJE-DERIV-002: each series carries its baseline with the rule that fired (#142)
  • pass labBaseline (PJE-DERIV-002, D17) PJE-DERIV-002: accepts LabSeries points ({ day, value, event }) as well as event records (#142)
  • pass labBaseline (PJE-DERIV-002, D17) PJE-DERIV-002: falls back to the earliest value when every record is after the baseline day (#142)
  • pass labBaseline (PJE-DERIV-002, D17) PJE-DERIV-002: falls back to the last value on or before the baseline day when no flag column resolves (#142)
  • pass labBaseline (PJE-DERIV-002, D17) PJE-DERIV-002: returns null when no usable point exists (#142)
  • pass labBaseline (PJE-DERIV-002, D17) PJE-DERIV-002: same-day ties break on the SMALLER sourceIndex at every level (#142)
  • pass labBaseline (PJE-DERIV-002, D17) PJE-DERIV-002: the ABLFL record is the baseline even when a later, closer-to-day-1 unflagged record exists (#142)
  • pass labBaseline (PJE-DERIV-002, D17) PJE-DERIV-002: the day rule honours a configured lb_baseline_day and a null flag column (#142)
  • pass labBaseline (PJE-DERIV-002, D17) PJE-DERIV-002: with several flagged records the latest day wins (#142)
Evidence screenshot PJE-LANE-005-labs-small-multiples.png
PJE-ANCH-002

PJE-ANCH-002 The context window is the anchor plus and minus the configured elapsed days, inclusive; interval records match by overlap and point records by containment, and a record with no end day matches as still running while carrying whether its end is recorded as ongoing or simply not recorded.

PJE-ANCH-002#142

anchor.test.js β€” inclusive window bounds, interval overlap and point containment, ongoing and unrecorded ends both still running

  • pass buildContext (PJE-PANEL-001, PJE-EVT-001) PJE-ANCH-002: the window honours the configured context_window_days, including zero (#142)
  • pass dayLabel / spanLabel (PJE-TIME-001, D16) PJE-ANCH-002: the terminal phrase comes from endState β€” "to day N", "to ongoing (<outcome>)", or "end not recorded" (#142)
  • pass inWindow (PJE-ANCH-002) PJE-ANCH-002: a point exactly on either bound matches, and one just outside does not (#142)
  • pass inWindow (PJE-ANCH-002) PJE-ANCH-002: an interval matches by overlap, inclusive at both edges (#142)
  • pass inWindow (PJE-ANCH-002) PJE-ANCH-002: an unplaceable event never matches, and a clipped-start con-med uses its true start (#142)
  • pass inWindow (PJE-ANCH-002) PJE-ANCH-002: ongoing and end-not-recorded intervals both run past their start, and a closed one stops at its end (#142)
  • pass inWindow (PJE-ANCH-002) PJE-ANCH-002: with a zero-day window only the anchor day matches (#142)
  • pass markGeometry (PJE-LANE-005/006) PJE-ANCH-002: an open interval runs to the domain end with its end cap; a same-day event is zero-width but keeps the minimum width (#142)
  • pass normalizeDomain β€” interval end handling (D16) PJE-ANCH-002: a blank end with an ongoing outcome is ongoing; a blank end with any other outcome is unrecorded (#142)
  • pass normalizeDomain β€” interval end handling (D16) PJE-ANCH-002: with no outcome column configured (con-meds by default) every blank end is unrecorded (#142)
  • pass severityStyle / glyphFor / endCapFor (PJE-ACC-002, D16, D23) PJE-ANCH-002: the end cap is closed / arrow (ongoing) / fade (end not recorded) by endState, never by a bare missing value (#142)
  • pass tooltipLines (design Β§6.5) PJE-ANCH-002: ongoing and end-not-recorded produce different terminal text, and a con-med shows its class and dose (#142)
  • pass windowBounds (PJE-ANCH-002) PJE-ANCH-002: an unusable anchor day, or non-finite days, yields null (#142)
  • pass windowBounds (PJE-ANCH-002) PJE-ANCH-002: windowBounds(30, 30) spans 30 elapsed days each side, labelled in study days (#142)
  • pass windowBounds (PJE-ANCH-002) PJE-ANCH-002: windowBounds(5, 0) is a zero-width window at the anchor (#142)
  • pass windowBounds (PJE-ANCH-002) PJE-ANCH-005: windowBounds(10, 30) is symmetric across first dose: {-21, 39} in elapsed space, {-21, 40} as study days (#142)
Evidence screenshot PJE-PANEL-001-context-panel.pngEvidence screenshot PJE-TIME-001-date-mode.pngEvidence screenshot PJE-LANE-005-labs-small-multiples.png
PJE-ANCH-004

PJE-ANCH-004 The anchored axis is an offset count labelled "Days from anchor": the anchor is day 0 and the scale is linear across it, deliberately unlike CDISC study-day numbering, and the guide states the difference.

PJE-ANCH-004#142

anchor.test.js β€” relativeDay is 0 at the anchor and the anchored axis title is Days from anchor

  • pass axisTicks PJE-ANCH-004: anchored ticks are round offsets from the anchor β€” 0 always, then multiples of the step β€” not the study-day ticks shifted (#142)
  • pass formatTick PJE-ANCH-004: with an anchor day the tick is a signed elapsed offset and the anchor reads 0 (#142)
  • pass relativeDay (PJE-ANCH-004, PJE-ANCH-005) PJE-ANCH-004: null in, null out (#142)
  • pass relativeDay (PJE-ANCH-004, PJE-ANCH-005) PJE-ANCH-004: the anchor itself is day 0, the axis title is "Days from anchor", and the offset is a plain count across first dose (#142)
  • pass relativeDay (PJE-ANCH-004, PJE-ANCH-005) PJE-ANCH-005: relativeDay(a, b) equals the calendar-day difference between dayToDate(a) and dayToDate(b) for all 30 ordered pairs, including every pair straddling first dose (#142)
  • pass tooltipLines (design Β§6.5) PJE-ANCH-004: the "+N days from anchor" line appears only when anchored, signed, in elapsed days (#142)
PJE-ANCH-005

PJE-ANCH-005 All window and offset arithmetic runs in a continuous elapsed-day space with no day 0, so a window of N days spans N elapsed days on each side of the anchor and "days from anchor" equals the calendar difference between the two dates for every pair of days, including pairs that straddle first dose.

PJE-ANCH-005#142

anchor.test.js β€” the elapsed-day round-trip skips 0 and relativeDay equals the calendar difference for all 30 ordered pairs

  • pass elapsed helpers (re-exported from getScales.js) PJE-ANCH-005: toElapsed and toStudyDay round-trip every day in {-30,-5,-1,1,5,30} and skip day 0 (#142)
  • pass elapsed-day space (D22) PJE-ANCH-005: toElapsed and toStudyDay round-trip every non-zero day and skip day 0 (#142)
  • pass relativeDay (PJE-ANCH-004, PJE-ANCH-005) PJE-ANCH-004: null in, null out (#142)
  • pass relativeDay (PJE-ANCH-004, PJE-ANCH-005) PJE-ANCH-004: the anchor itself is day 0, the axis title is "Days from anchor", and the offset is a plain count across first dose (#142)
  • pass relativeDay (PJE-ANCH-004, PJE-ANCH-005) PJE-ANCH-005: relativeDay(a, b) equals the calendar-day difference between dayToDate(a) and dayToDate(b) for all 30 ordered pairs, including every pair straddling first dose (#142)
  • pass windowBounds (PJE-ANCH-002) PJE-ANCH-005: windowBounds(10, 30) is symmetric across first dose: {-21, 39} in elapsed space, {-21, 40} as study days (#142)
PJE-ANCH-006

PJE-ANCH-006 A notice above the lane stack says that clicking any mark shows the events associated with it and names the keyboard path; once a mark is anchored the notice says which mark is anchored and how to move or release the anchor; with every lane off, or before data is bound, the notice is not shown.

PJE-ANCH-006#145

events.test.js β€” the notice above the lanes says clicking any mark shows its associated events, names the anchor once set, hides with every lane off

  • pass the click-any-mark notice (PJE-ANCH-006) PJE-ANCH-006: a notice above the lanes says clicking any mark shows its associated events, names the anchor once one is set, and hides with every lane off (#145)
PJE-CTX-001

PJE-CTX-001 Con-meds active at the anchor are those starting on or before the anchor day and not ending before it; a con-med with no usable start day is not asserted to be active and is counted as not evaluated, and a con-med whose end was never recorded is counted as active but separately counted and named as such in the panel rather than asserted to have continued.

PJE-CTX-001#142

context.test.js β€” the active-at rule at the edges; unrecorded ends counted; a con-med with no start excluded and counted; later starts listed separately

  • pass buildContext (PJE-PANEL-001, PJE-EVT-001) PJE-CTX-001: anchoring on a non-AE mark still answers every query, with no prior events (#142)
  • pass conMedsActiveAt (PJE-CTX-001, D8, D16) PJE-CTX-001: a con-med whose end was never recorded IS active and raises endUnrecorded; an ongoing one is active and does not (#142)
  • pass conMedsActiveAt (PJE-CTX-001, D8, D16) PJE-CTX-001: a con-med whose recorded end precedes its start is active only on its start day and is not counted as end-unrecorded (PJE-DATA-008) (#142)
  • pass conMedsActiveAt (PJE-CTX-001, D8, D16) PJE-CTX-001: a con-med with no usable start day is never asserted active and is counted in withoutStart (#142)
  • pass conMedsActiveAt (PJE-CTX-001, D8, D16) PJE-CTX-001: active means started on or before the day and not ended before it β€” end === day is active, end === day - 1 is not (#142)
  • pass conMedsActiveAt (PJE-CTX-001, D8, D16) PJE-CTX-001: the active list is ordered by start day, then name, then sourceIndex (#142)
  • pass conMedsActiveAt (PJE-CTX-001, D8, D16) PJE-CTX-001: the day comparison runs in elapsed space and an unusable day returns nothing active (#142)
  • pass conMedsStartingLater (PJE-CTX-001) PJE-CTX-001: returns only con-meds starting inside the window and AFTER the anchor day (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-CTX-001: a lane turned off or a filter never changes the context lists; the panel says why a listed record is not drawn and that the anchor is hidden (#142)
Evidence screenshot PJE-PANEL-001-context-panel.pngEvidence screenshot PJE-TIME-001-date-mode.png
PJE-CTX-002

PJE-CTX-002 Abnormal labs in the window are those whose normal-range indicator is present and not normal, or whose value is at least twice or at most half the baseline, with the triggering reason recorded.

PJE-CTX-002#142

labs.test.js β€” the flag rule and the symmetric change rule at exactly 2Γ— and Β½Γ—; a blank indicator is not abnormal

  • pass abnormalLabsInWindow (PJE-CTX-002) PJE-CTX-002: a flag-only point reports "flag" and the baseline is computed over the whole record, not the window (#142)
  • pass abnormalLabsInWindow (PJE-CTX-002) PJE-CTX-002: keeps in-window points where the flag or the change rule fires, with the reason recorded on a copy (#142)
  • pass abnormalLabsInWindow (PJE-CTX-002) PJE-CTX-002: results are sorted by day then test name, and empty input yields [] (#142)
  • pass isAbnormalByChange (PJE-CTX-002, D28) PJE-CTX-002: a missing, zero or negative baseline disables the change rule (#142)
  • pass isAbnormalByChange (PJE-CTX-002, D28) PJE-CTX-002: exactly 2x and exactly 0.5x the baseline are abnormal (inclusive on both arms) (#142)
  • pass isAbnormalByChange (PJE-CTX-002, D28) PJE-CTX-002: lb_change_factor configures both arms (#142)
  • pass isAbnormalByChange (PJE-CTX-002, D28) PJE-CTX-002: values just inside both arms are not abnormal (#142)
  • pass isAbnormalByFlag (PJE-CTX-002) PJE-CTX-002: HIGH, LOW, ABNORMAL, HH and LL are abnormal; NORMAL and blank are not (#142)
  • pass isAbnormalByFlag (PJE-CTX-002) PJE-CTX-002: the configured normal value is compared case-insensitively (#142)
  • pass ratioLine (PC-29) PJE-CTX-002: "Γ— ULN" above the range, "Γ— LLN" below it, two decimals, and no line when the relevant limit is missing (#142)
  • pass referenceRatio (PC-29) PJE-CTX-002: a HIGH flag or a value above ULN divides by ULN and says so (#142)
  • pass referenceRatio (PC-29) PJE-CTX-002: a LOW flag or a value below LLN divides by LLN, never by ULN (#142)
  • pass referenceRatio (PC-29) PJE-CTX-002: no ratio when the relevant limit is missing or when the point is in range (#142)
  • pass tooltipLines (design Β§6.5) PJE-CTX-002: a lab point reads test and value, the indicator, the day, the ratio line and the reference range (#142)
PJE-CTX-003

PJE-CTX-003 Dose changes falling inside the window are listed with their direction and the day the new dose began.

PJE-CTX-003#142

context.test.js β€” a dose change on the window edge is included

  • pass doseChangesInWindow (PJE-CTX-003) PJE-CTX-003: point containment, inclusive at both edges; unplaceable changes never match (#142)
PJE-CTX-004

PJE-CTX-004 Earlier or same-day adverse events with the anchor's preferred term (falling back to the verbatim term) are listed most recent first, over the whole record rather than only the window β€” unchanged by any display filter or lane toggle β€” with each row stating its offset from the anchor and a same-day record saying so.

PJE-CTX-004#142

context.test.js β€” prior same-term ordering, the verbatim-term fallback, same-day ties by sourceIndex

  • pass buildContext (PJE-PANEL-001, PJE-EVT-001) PJE-CTX-004: the four lists are facts about the whole record β€” an active display filter that narrows byLane changes neither the prior events nor the active con-meds (#142)
  • pass priorSameTerm (PJE-CTX-004) PJE-CTX-004: a same-day prior is resolved by sourceIndex, and the anchor never lists itself (#142)
  • pass priorSameTerm (PJE-CTX-004) PJE-CTX-004: an unplaceable candidate is never prior (#142)
  • pass priorSameTerm (PJE-CTX-004) PJE-CTX-004: prior events with the same preferred term over the whole record, most recent first (#142)
  • pass priorSameTerm (PJE-CTX-004) PJE-CTX-004: the key falls back to the verbatim term when the preferred term is blank (#142)
Evidence screenshot PJE-PANEL-001-context-panel.png
PJE-SRC-002

PJE-SRC-002 When source_url_template is configured each source row carries an external link built from the row's own values; with no template configured no link is rendered.

PJE-SRC-002#142

sourceRows.test.js β€” buildSourceUrl fills {domain} and {COLUMN}; a missing column omits the link; no template, no link

  • pass buildSourceUrl PJE-SRC-002: {domain} and each {COLUMN} token are replaced with the URI-encoded row values (#142)
  • pass buildSourceUrl PJE-SRC-002: a template naming a column the row lacks returns null with one warning (#142)
  • pass buildSourceUrl PJE-SRC-002: a template with no tokens is returned as-is and a null cell is an empty value, not "null" (#142)
  • pass buildSourceUrl PJE-SRC-002: no template means no link and no warning (#142)
PJE-FILT-002

PJE-FILT-002 The abnormal-labs-only filter restricts lab points to those failing the abnormality rule.

PJE-FILT-002#142

structureData.test.js β€” the __abnormal__ flag filter keeps only points failing the flag rule and leaves AE and CM untouched

  • pass applyFilters / liveFilters (PJE-FILT-*) PJE-FILT-002: the __abnormal__ flag filter keeps only lab points failing isAbnormalByFlag and leaves AE and CM untouched (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-FILT-002: the abnormal-only filter keeps the lab series baseline from the whole record (#142)
Evidence screenshot PJE-DATA-003-dropped-rows.png
PJE-FILT-004

PJE-FILT-004 A filter whose column is absent from its domain's data is dropped with the library's standard console warning.

PJE-FILT-004#142

structureData.test.js β€” a filter whose column is absent from its domain is dropped with the library's exact phrasing

  • pass applyFilters / liveFilters (PJE-FILT-*) PJE-FILT-004: a filter whose column is absent from every row of its domain is dropped with the library’s exact warning (#142)
  • pass structureData (PJE-LANE-008/010, PJE-DATA-003) PJE-FILT-004: the live filter list drops absent columns with the house warning and their state is ignored (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-CFG-004: a cleared context-window field restores the configured width rather than collapsing the window to the anchor day (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-CTX-001: a lane turned off or a filter never changes the context lists; the panel says why a listed record is not drawn and that the anchor is hidden (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-DATA-004: after init() throws on invalid data, a later init() with valid data draws into the shell again, in the host element (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-DATA-004: the checkInputs message is inserted as text, never as markup (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-FILT-004: the absent-column filter warning and the source_url_template warning are printed once per data load, not once per interaction (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-KEY-002: Escape inside the subject search or the window field keeps its native meaning and does not clear the anchor (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-KEY-004: clearing the anchor from its own button, or resetting, never drops focus to the document body (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-LANE-003: with every lane turned off the stack says so and the anchoring hint is not shown (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-SUBJ-003: the footnote is cleared when its mark leaves the page β€” another subject, its lane off β€” and rewritten when the anchor changes (#142)
  • pass the 2026-09-18 verification fixes (PJE-CFG-004, PJE-TIME-001, PJE-DATA-004, PJE-KEY-002, PJE-LANE-003, PJE-FILT-004, PJE-KEY-004) PJE-TIME-001: date mode with no reference date shows study days everywhere β€” title, select, getTimeMode β€” and keeps the preference for a subject that has one (#142)
Evidence screenshot PJE-DATA-003-dropped-rows.pngEvidence screenshot PJE-TIME-001-date-mode.png
PJE-TIME-002

PJE-TIME-002 Study day maps to calendar date from the subject's reference date with day 1 as that date and no day 0.

PJE-TIME-002#142

getScales.test.js β€” dayToDate with day 1 as the reference date and no day 0; the derived reference date

  • pass dayToDate / isFullDate PJE-TIME-002: day 1 is the reference date, day 2 the next day, day -1 the day before, and day 0 is null (#142)
  • pass referenceDate PJE-TIME-002: picks the first full ref_date_col cell and reports the rule (#142)
PJE-TIME-003

PJE-TIME-003 A partial recorded date (year, or year and month) is shown as recorded and never used to position a mark.

PJE-TIME-003#142

getScales.test.js β€” isFullDate rejects year and year-month values and the event keeps rawDate

  • pass dayToDate / isFullDate PJE-TIME-003: isFullDate accepts YYYY-MM-DD (with an optional time part) and rejects partial or impossible dates (#142)
  • pass normalizeDomain β€” per-domain rules PJE-TIME-003: a partial recorded date is kept as recorded and never positions the mark (#142)
  • pass resolveEventDate PJE-TIME-003: a partial recorded date never positions and never labels; the day does, when a reference exists (#142)
  • pass tooltipLines (design Β§6.5) PJE-TIME-003: a partial recorded date is shown as recorded; PJE-TIME-004: a disagreeing full date is named (#142)
PJE-TIME-004

PJE-TIME-004 When a record's full recorded date disagrees with its study day, the study day positions and labels the mark, the disagreement is named in the tooltip, and the affected records are counted in the notes.

PJE-TIME-004#142

getScales.test.js β€” a full date that disagrees with its day is positioned and labelled from the day, flagged and counted

  • pass normalizeDomain β€” per-domain rules PJE-TIME-004: a full recorded date that disagrees with the study day is overridden by the day and flagged (#142)
  • pass resolveEventDate PJE-TIME-004: a full recorded date that disagrees with the study day is overridden by the day and flagged (#142)
  • pass tooltipLines (design Β§6.5) PJE-TIME-003: a partial recorded date is shown as recorded; PJE-TIME-004: a disagreeing full date is named (#142)
PJE-EVT-002

PJE-EVT-002 Selecting a subject also dispatches the library's shared participantsSelected event carrying that subject.

PJE-EVT-002#142

events.test.js β€” participantsSelected on the shell root with the selected subject

  • pass the shared selection event (PJE-EVT-002) PJE-EVT-002: selecting a subject dispatches participantsSelected on the shell root with { detail: { data: [id] } } (#142)
PJE-EVT-003

PJE-EVT-003 off removes listeners and destroy tears down charts, listeners and the container; the instance is not reusable afterwards.

PJE-EVT-003#142

events.test.js β€” off by name and by handler; destroy empties the element and clears every listener

  • pass listener removal and teardown (PJE-EVT-003) PJE-EVT-003: destroy tears down the charts, the listeners and the container, and a later emit is a no-op (#142)
  • pass listener removal and teardown (PJE-EVT-003) PJE-EVT-003: off(name, fn) removes one listener and off(name) removes them all (#142)
PJE-ACC-001

PJE-ACC-001 The mark palette passes the documented validator at the hardest pairlist in both modes: three categorical hues whose composited fills β€” as drawn, at full strength and at the de-emphasized strength β€” measure at least 3:1 and between 1.9:1 and 2.4:1 against their surface respectively, with worst CVD separation Ξ”E 13.0; lane identity beyond three lanes is carried by position, a permanent label and mark geometry, never hue.

PJE-ACC-001#142

palette.test.js β€” contrast and Ξ”E recomputed in-test from the composited fills at full and de-emphasized strength

  • pass PJE_PALETTE PJE-ACC-001: each categorical hue is opaque and at least 3:1 against its surface in both modes, at the recorded ratios (#142)
  • pass PJE_PALETTE PJE-ACC-001: the de-emphasized fill composites to between 1.9:1 and 2.4:1 per hue, at the recorded ratios (#142)
  • pass PJE_PALETTE PJE-ACC-001: the escalation red, the diverging lab arms and the focus ring hold their recorded contrast in both modes (#142)
  • pass PJE_PALETTE PJE-ACC-001: the three hues stay pairwise separable under normal vision and simulated deuteranopia at the recorded floor (#142)
PJE-DEMO-001

PJE-DEMO-001 The vendored demo extracts are reproducible and guarded: the exposure extract keeps only dosing records, the con-med extract is de-duplicated to one row per distinct course, the medical-history day is the collection day, and the subject count and seeded subjects' key values are asserted.

PJE-DEMO-001#142

demo-data.test.js β€” the six builders' derivation rules plus the committed-file drift guard and the pharmaversesdtm provenance assertion

  • pass buildPjeAeRecords PJE-DEMO-001: keeps treatment-emergent events only and carries AEOUT for the end-state rule (#142)
  • pass buildPjeCmRecords PJE-DEMO-001: de-duplicates the per-visit repeats to one row per course and keeps the lowest CMSEQ (#142)
  • pass buildPjeCmRecords PJE-DEMO-001: keeps UNCODED as an ordinary class value (#142)
  • pass buildPjeDsRecords PJE-DEMO-001: restricts the SDTM disposition rows to the safety population and joins TRTSDT from adsl (#142)
  • pass buildPjeDsRecords PJE-DEMO-001: warns naming every safety participant with no disposition row (#142)
  • pass buildPjeExRecords PJE-DEMO-001: keeps only the PARAMCD=DOSE exposure records, in the declared columns (#142)
  • pass buildPjeExRecords PJE-DEMO-001: writes blanks, never the literal NA, for missing values (#142)
  • pass buildPjeLbRecords PJE-DEMO-001: applies the analysis filter (DTYPE blank, ANL01FL or ABLFL, numeric AVAL) restricted to the liver panel (#142)
  • pass buildPjeLbRecords PJE-DEMO-001: renames the ADaM analysis columns to the module defaults and prefers ANRIND over LBNRIND (#142)
  • pass buildPjeLbRecords PJE-DEMO-001: the default panel is the four liver tests, matched on LBTEST, and a missing test warns by name (#142)
  • pass buildPjeMhRecords PJE-DEMO-001: plots at the collection day MHDY and carries the onset day separately as MHONSDY (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: pje-ae.csv carries the published header, 1122 rows and 217 participants, with no NA cells (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: pje-cm.csv carries the published header, 1081 rows and 229 participants, with no NA cells (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: pje-ds.csv carries the published header, 798 rows and 254 participants, with no NA cells (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: pje-ex.csv carries the published header, 591 rows and 254 participants, with no NA cells (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: pje-lb.csv carries the published header, 6639 rows and 254 participants, with no NA cells (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: pje-mh.csv carries the published header, 1818 rows and 254 participants, with no NA cells (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: the guide's data caveats are the extracts' own numbers β€” 71% of con-med courses have no end date, 83% are UNCODED, and the lab indicator carries only NORMAL, HIGH and LOW (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: the seeded participant 01-701-1203 is the uneventful placebo case with the 1986 con-med (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: the seeded participant 01-705-1310 carries the 4.03 x ULN ALT peak and the adverse-event discontinuation (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: the seeded participant 01-716-1447 carries the documented day-30 anchor story (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: the six files together cover exactly the 254 safety participants (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: the six outputs are declared once, with the published headers (#142)
  • pass committed pje-*.csv extracts PJE-DEMO-001: the study-wide caveats the demo page states hold on the committed files (#142)
  • pass docs/DATA_SOURCES.md provenance PJE-DEMO-001: lists all six extracts in the file table (#142)
  • pass docs/DATA_SOURCES.md provenance PJE-DEMO-001: names pharmaversesdtm, its Apache-2.0 licence and the ds.csv source revision (#142)

Known gaps, stated

0 requirement rows Β· 0 tests

RequirementSource matrix rowsIssueTests & evidence

Visual evidence

Every screenshot below is a committed baseline: the same PNG is the visual-regression baseline the browser suite asserts against and the evidence artifact shown here. Click any capture for the full-resolution image.

Reproducing this report

The evidence set is regenerated from a full test run and committed with the code it qualifies; CI fails when they drift. To verify or rebuild it:

npm ci
npm run evidence:check   # compare a fresh run against the committed evidence
npm run evidence         # regenerate docs/evidence/patient-journey-explorer/evidence.json

Screenshot baselines are canonical to the Linux CI runner; the repository's Update evidence baselines workflow is the authoritative way to refresh them. See CONTRIBUTING.md for the traceability convention.