QT Safety Explorer: test evidence Experimental

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

Scope
18 coverage rows43 distinct requirement IDs
Tests executed
197 automated checks25 browser ยท 172 unit
Result
pass all passing4 evidence screenshots
Generated
2026-07-22 02:43 UTC
Environment
linux 6.17.0-1020-azure ยท node v22.23.1 ยท playwright 1.61.1 ยท chromium 149.0.7827.55

Scope & approach

Traceability for the qt-explorer module (a Chart.js reimplementation of the SafetyGraphics/qtexplorer QT Safety Explorer, Phase 1, under #68; parent requirement obot.roadmap#36), per the convention in CONTRIBUTING.md. Requirement IDs use the module's condensed **QT-*** scheme cited in the source and test names โ€” QT-CT-* (central tendency: per-arm change over time, ฮ” / ฮ”ฮ”, CI band, reference line, peak-effect visit, ICH-E14 metric), QT-OUT-* (outlier scatter: absolute-QTc diagonals, change lines, arm marks, tooltip), QT-CAT-* (categorical by-arm exceedance), QT-CTRL-* (view / correction / statistic / display / timepoint / filter controls), and QT-DATA-* (data contract, cleaning, baseline/change derivation). The Phase-1 scope is QTcF/QTcB corrections plus heart rate; QTcI, PR/QRS/JT, per-subject drill-down, QT-RR hysteresis, the guided ICH-E14 workflow, and a moxifloxacin positive-control arm are deferred to Phase 2 (the CDISC Pilot ADEG carries none of them). A reviewed source requirement matrix (qt-explorer.md in the requirements repo) is not yet published, so the source-matrix column is pending; each row lists the module IDs its test covers.

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/qt-explorer.spec.js)

9 requirement rows ยท 9 tests

RequirementSource matrix rowsIssueTests & evidence
QT-CTRL-001, QT-CTRL-002, QT-CTRL-003

QT-CTRL-001 A View control switches between the three views โ€” central tendency, outlier scatter, and categorical exceedance โ€” rendering one styled, clickable row per view with the active view marked.

QT-CTRL-002 A Correction control offers the configured parameter/correction options (default QTcF, QTcB, Heart Rate) and selects start_measure (QTcF) on first render, falling back to the first available measure when start_measure is unlisted.

QT-CTRL-003 Statistic (mean / median), display (ฮ” / ฮ”ฮ”), timepoint, and categorical filter controls narrow the view; applyFilters constrains rows on the active selections only, so an empty selection constrains nothing.

โ€”#68
  • pass QT-CTRL-001/QT-CTRL-002/QT-CTRL-003: renders the view, correction, statistic, display, and filter controls (#68)
Evidence screenshot QT-CTRL-001-central-tendency-delta.png
QT-CT-002, QT-CT-003, QT-CT-006

QT-CT-002 In mean mode the ฮ” series carries each arm's mean change with a two-sided 90% CI at every visit; in median mode the series carries no CI (lo/hi NaN). The chart draws per-arm lines with the CI band.

QT-CT-003 A mode-labelled horizontal reference line marks the ICH-E14 threshold of regulatory concern on the central-tendency chart.

QT-CT-006 Each arm's peak-effect visit โ€” the visit of maximum effect โ€” is marked on the central-tendency chart.

โ€”#68
  • pass QT-CT-002/QT-CT-003/QT-CT-006: central tendency draws per-arm lines, a CI band, the reference line and the peak marker (#68)
QT-CT-004, QT-CT-005

QT-CT-004 The placebo-corrected ฮ”ฮ” mode drops the placebo arm and subtracts the placebo mean change at each visit; the placebo arm is auto-detected (matching /placebo/i) and ordered first.

QT-CT-005 The ICH-E14 metric is the largest upper bound of the two-sided 90% CI for the placebo-corrected mean difference (ฮ”ฮ”) across visits, flagged against the ~10 ms reference; the metric is reported only in mean + ฮ”ฮ” mode.

โ€”#68
  • pass QT-CT-004/QT-CT-005: ฮ”ฮ” drops placebo and reports the ICH-E14 metric above the reference (#68)
Evidence screenshot QT-CT-005-delta-delta-ich-metric.png
QT-OUT-002, QT-OUT-003, QT-OUT-004

QT-OUT-002 With a specific timepoint selected the scatter picks that visit's row per subject (matching a numeric-typed visit against a string timepoint); otherwise the maximum post-baseline value is used.

QT-OUT-003 The outlier scatter draws the 450/480/500 ms absolute-QTc diagonals, and in per-visit mode adds the 30/60 ms change-from-baseline horizontal lines; the absolute cut-lines apply only to QTc corrections.

QT-OUT-004 Each subject point is coloured and shaped by treatment arm, with a distinct cycling point style per arm.

โ€”#68
  • pass QT-OUT-002/QT-OUT-003/QT-OUT-004: the outlier scatter draws absolute diagonals, the zero line, and per-arm marks (#68)
Evidence screenshot QT-OUT-003-outlier-scatter-max.png
QT-OUT-003

QT-OUT-003 The outlier scatter draws the 450/480/500 ms absolute-QTc diagonals, and in per-visit mode adds the 30/60 ms change-from-baseline horizontal lines; the absolute cut-lines apply only to QTc corrections.

โ€”#68
  • pass QT-OUT-003: a specific visit adds the change-from-baseline lines (#68)
Evidence screenshot QT-OUT-003-outlier-scatter-max.png
QT-CAT-001, QT-CAT-002, QT-CAT-003

QT-CAT-001 The categorical view hides the chart and tabulates by-arm exceedance, with denominators of post-baseline subjects per arm, tracking each subject's maximum value and maximum change independently.

QT-CAT-002 Absolute-QTc thresholds (>450 / >480 / >500 ms) are counted per arm with a strict greater-than, so a value exactly at a threshold (e.g. 500) is excluded.

QT-CAT-003 Change-from-baseline thresholds (>30 / >60 ms) are counted per arm using each subject's maximum change, independently of the maximum value.

โ€”#68
  • pass QT-CAT-001/QT-CAT-002/QT-CAT-003: the categorical view hides the chart and tabulates by-arm exceedance (#68)
Evidence screenshot QT-CAT-002-categorical-table.png
QT-CT-007

QT-CT-007 Heart rate is offered as a central-tendency measure without the QTc ICH-E14 reference line (the ~10 ms threshold applies to QTc only).

โ€”#68
  • pass QT-CT-007: heart rate is offered in central tendency without the QTc reference line (#68)
QT-OUT-007

QT-OUT-007 Heart rate shows a QTc-only note in the outlier scatter view, since the absolute and change diagonals apply to QTc corrections only.

โ€”#68
  • pass QT-OUT-007: heart rate shows a QTc-only note in the outlier view (#68)
QT-DATA-003

QT-DATA-003 cleanData drops rows with a missing or non-numeric value and reports the removed count; the on-page removed-count path leaves no page errors.

โ€”#68
  • pass QT-DATA-003: the participant note / removed-count path leaves no page errors (#68)

Unit evidence (Vitest โ€” tests/unit/qt-explorer/)

9 requirement rows ยท 51 tests

RequirementSource matrix rowsIssueTests & evidence
QT-CFG-001..007 (settings normalization, z table, placebo resolution)

QT-CFG-001 syncSettings fills defaults and normalizes field-list settings (arrayifying single values).

QT-CFG-002 syncSettings sorts and numifies the threshold lists (absolute_thresholds, change_thresholds).

QT-CFG-003 syncSettings falls back start_measure to the first available measure when the configured start_measure is not in the measure list.

QT-CFG-004 syncSettings rejects an out-of-range ci_level, keeping the default (0.9).

QT-CFG-005 The VIEWS list and the TIMEPOINT_MAX ("maximum post-baseline") sentinel are stable constants the controls and timepoint selection read from.

QT-CFG-006 zForCi returns the two-sided z multiplier for a confidence level, interpolating between table points and clamping at the ends.

QT-CFG-007 resolvePlaceboArm prefers the explicit placebo setting when present and otherwise auto-detects the placebo arm by matching /placebo/i.

โ€”#68

configure.test.js

  • pass qt-explorer configure.resolvePlaceboArm QT-CFG-007: auto-detects /placebo/i otherwise
  • pass qt-explorer configure.resolvePlaceboArm QT-CFG-007: prefers the explicit setting when present
  • pass qt-explorer configure.syncSettings QT-CFG-001: fills defaults and normalizes field lists
  • pass qt-explorer configure.syncSettings QT-CFG-002: sorts and numifies threshold lists
  • pass qt-explorer configure.syncSettings QT-CFG-003: falls back start_measure to the first available measure
  • pass qt-explorer configure.syncSettings QT-CFG-004: rejects an out-of-range ci_level, keeping the default
  • pass qt-explorer configure.syncSettings QT-CFG-005: VIEWS and TIMEPOINT_MAX are stable
  • pass qt-explorer configure.zForCi QT-CFG-006: interpolates between table points and clamps the ends
  • pass qt-explorer configure.zForCi QT-CFG-006: returns the two-sided z for common levels
QT-DATA-005 (schema required columns)

QT-DATA-005 checkInputs validates the schema's required column settings (measure_col, value_col, arm_col, baseline_col) against the remapped names and throws naming every missing column; the baseline record is identified by the baseline flag (ABLFL=Y) and excluded from the post-baseline extremes, and when no baseline-flag column is present a change-of-0 row is treated as the baseline.

โ€”#68

checkInputs.test.js

  • pass qt-explorer checkInputs (QT-DATA-005) QT-DATA-005: an empty dataset reports all required columns missing
  • pass qt-explorer checkInputs (QT-DATA-005) QT-DATA-005: passes when every required column is present
  • pass qt-explorer checkInputs (QT-DATA-005) QT-DATA-005: throws naming each missing required column
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-005: flags the ABLFL=Y baseline record as not post-baseline
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-005: when the flag column is present, a post-baseline change-0 row with a missing flag stays post-baseline
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-005: with no baseline-flag column at all, a change-0 row is treated as the baseline
QT-DATA-003/004/006, QT-STAT-001/002 (cleaning, change derivation, stats)

QT-DATA-003 cleanData drops rows with a missing or non-numeric value and reports the removed count; the on-page removed-count path leaves no page errors.

QT-DATA-004 Change from baseline is the source change column when present, and is otherwise derived as value โˆ’ baseline when that column is blank.

QT-DATA-006 measuresPresent lists the distinct measures actually present in the data, so the correction control offers only measures the dataset carries.

QT-STAT-001 The unique helper keeps first-seen non-empty values, preserving order for the arm/visit/measure lists.

QT-STAT-002 The statistics helpers (mean, sd, quantile, median) match hand-computed values, backing the central-tendency means, the CI, and the categorical summaries.

โ€”#68

structureData.test.js

  • pass qt-explorer arms/visits/measures/filters QT-DATA-006: measuresPresent lists distinct measures
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-003: drops missing / non-numeric values with a count
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-004: derives change as value โˆ’ baseline when the change column is blank
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-004: keeps the source change when present
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-005: flags the ABLFL=Y baseline record as not post-baseline
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-005: when the flag column is present, a post-baseline change-0 row with a missing flag stays post-baseline
  • pass qt-explorer cleanData (QT-DATA-003/004) QT-DATA-005: with no baseline-flag column at all, a change-0 row is treated as the baseline
  • pass qt-explorer structureData statistics helpers QT-STAT-001: unique keeps first-seen non-empty values
  • pass qt-explorer structureData statistics helpers QT-STAT-002: mean/sd/quantile/median match hand values
QT-CT-001/002/004/005/006 (central-tendency series, ฮ”ฮ”, ICH-E14, peaks)

QT-CT-001 The central-tendency view plots per-arm change-from-baseline over ordered study visits (visits sorted by numeric visit), one series per treatment arm.

QT-CT-002 In mean mode the ฮ” series carries each arm's mean change with a two-sided 90% CI at every visit; in median mode the series carries no CI (lo/hi NaN). The chart draws per-arm lines with the CI band.

QT-CT-004 The placebo-corrected ฮ”ฮ” mode drops the placebo arm and subtracts the placebo mean change at each visit; the placebo arm is auto-detected (matching /placebo/i) and ordered first.

QT-CT-005 The ICH-E14 metric is the largest upper bound of the two-sided 90% CI for the placebo-corrected mean difference (ฮ”ฮ”) across visits, flagged against the ~10 ms reference; the metric is reported only in mean + ฮ”ฮ” mode.

QT-CT-006 Each arm's peak-effect visit โ€” the visit of maximum effect โ€” is marked on the central-tendency chart.

โ€”#68

structureData.test.js

  • pass qt-explorer arms/visits/measures/filters QT-CT-001: orderVisits sorts by numeric visit
  • pass qt-explorer arms/visits/measures/filters QT-CT-004: armsPresent puts the placebo arm first
  • pass qt-explorer arms/visits/measures/filters QT-CT-004: placeboArmFor auto-detects /placebo/i
  • pass qt-explorer centralTendencySeries (QT-CT-001/004/005) QT-CT-002: median mode carries no CI (lo/hi NaN)
  • pass qt-explorer centralTendencySeries (QT-CT-001/004/005) QT-CT-002: ฮ” mean series carries per-arm means and a 90% CI
  • pass qt-explorer centralTendencySeries (QT-CT-001/004/005) QT-CT-004: ฮ”ฮ” drops placebo and subtracts placebo mean change
  • pass qt-explorer getScales titles + units (QT-OUT-005, QT-CT-001) QT-SCL-001: correctionSuffix maps the QTc formulas
  • pass qt-explorer getScales titles + units (QT-OUT-005, QT-CT-001) QT-SCL-002: isQtcMeasure + measureUnit distinguish QTc from HR
  • pass qt-explorer getScales titles + units (QT-OUT-005, QT-CT-001) QT-SCL-003: central axis title carries mode prefix + correction suffix
  • pass qt-explorer getScales titles + units (QT-OUT-005, QT-CT-001) QT-SCL-004: scatter titles mirror the mockup
  • pass qt-explorer ICH-E14 metric + peak visit (QT-CT-005/006) QT-CT-005: ICH-E14 metric is the largest ฮ”ฮ” upper CI bound, flagged vs 10 ms
  • pass qt-explorer ICH-E14 metric + peak visit (QT-CT-005/006) QT-CT-005: metric is empty outside mean + ฮ”ฮ”
  • pass qt-explorer ICH-E14 metric + peak visit (QT-CT-005/006) QT-CT-006: peakVisits returns each arm peak
Evidence screenshot QT-CT-005-delta-delta-ich-metric.png
QT-OUT-001/002 (subject points, max post-baseline vs visit)

QT-OUT-001 The outlier scatter plots one point per subject at the subject's maximum post-baseline QTc value against their baseline QTc.

QT-OUT-002 With a specific timepoint selected the scatter picks that visit's row per subject (matching a numeric-typed visit against a string timepoint); otherwise the maximum post-baseline value is used.

โ€”#68

structureData.test.js

  • pass qt-explorer subjectPoints (QT-OUT-001/002) QT-OUT-002: a specific visit picks that visit row
  • pass qt-explorer subjectPoints (QT-OUT-001/002) QT-OUT-002: matches a numeric-typed visit against a string timepoint
  • pass qt-explorer subjectPoints (QT-OUT-001/002) QT-OUT-002: maximum post-baseline picks the max absolute value row
QT-CAT-001/002/003 (by-arm exceedance counts and percents)

QT-CAT-001 The categorical view hides the chart and tabulates by-arm exceedance, with denominators of post-baseline subjects per arm, tracking each subject's maximum value and maximum change independently.

QT-CAT-002 Absolute-QTc thresholds (>450 / >480 / >500 ms) are counted per arm with a strict greater-than, so a value exactly at a threshold (e.g. 500) is excluded.

QT-CAT-003 Change-from-baseline thresholds (>30 / >60 ms) are counted per arm using each subject's maximum change, independently of the maximum value.

โ€”#68

structureData.test.js

  • pass qt-explorer classifyThresholds (QT-CAT-002/003) QT-CAT-001: denominators are post-baseline subjects per arm
  • pass qt-explorer classifyThresholds (QT-CAT-002/003) QT-CAT-002: absolute >450 counts D1(470) and D2(500) in Drug
  • pass qt-explorer classifyThresholds (QT-CAT-002/003) QT-CAT-002: absolute >500 excludes the boundary value 500 (strict >)
  • pass qt-explorer classifyThresholds (QT-CAT-002/003) QT-CAT-003: change >60 counts only D2(70) using max change independently
  • pass qt-explorer subjectExtremes (QT-CAT-001) QT-CAT-001: tracks max value and max change independently
Evidence screenshot QT-CAT-002-categorical-table.png
QT-SCL-001..008 (correction suffix, axis titles, domains, arm marks)

QT-SCL-001 correctionSuffix maps the QTc formulas to their axis-title suffix (QTcF โ†’ Fridericia, QTcB โ†’ Bazett).

QT-SCL-002 isQtcMeasure and measureUnit distinguish a QTc correction (msec, cut-lines apply) from heart rate (bpm, no QTc cut-lines).

QT-SCL-003 The central-tendency axis title carries the mode prefix (ฮ” / ฮ”ฮ”) plus the correction suffix and units.

QT-SCL-004 The outlier-scatter axis titles mirror the design mockup (baseline QTc on x, change-from-baseline on y, with the correction suffix).

QT-SCL-005 formatNumber trims trailing zeros and guards non-finite values in tick and tooltip formatting.

QT-SCL-006 formatSigned signs the change value with a real minus glyph (โˆ’) for the tooltip and change labels.

QT-SCL-007 paddedDomain covers the data values plus any forced inclusions (the threshold cut-lines), and handles an empty or degenerate sample without collapsing.

QT-SCL-008 The scatter assigns a distinct cycling point style per arm so arms are distinguishable by shape as well as colour.

โ€”#68

getScales.test.js

  • pass qt-explorer getScales arm point styles (QT-OUT-004) QT-SCL-008: assigns a distinct cycling point style per arm
  • pass qt-explorer getScales formatting + domains QT-SCL-005: formatNumber trims and guards non-finite
  • pass qt-explorer getScales formatting + domains QT-SCL-006: formatSigned signs the change with a real minus glyph
  • pass qt-explorer getScales formatting + domains QT-SCL-007: paddedDomain covers values and forced inclusions
  • pass qt-explorer getScales formatting + domains QT-SCL-007: paddedDomain handles an empty / degenerate sample
  • pass qt-explorer getScales titles + units (QT-OUT-005, QT-CT-001) QT-SCL-001: correctionSuffix maps the QTc formulas
  • pass qt-explorer getScales titles + units (QT-OUT-005, QT-CT-001) QT-SCL-002: isQtcMeasure + measureUnit distinguish QTc from HR
  • pass qt-explorer getScales titles + units (QT-OUT-005, QT-CT-001) QT-SCL-003: central axis title carries mode prefix + correction suffix
  • pass qt-explorer getScales titles + units (QT-OUT-005, QT-CT-001) QT-SCL-004: scatter titles mirror the mockup
QT-PLG-001/002/003 (color scale, rgba, scatter tooltip)

QT-PLG-001 hexToRgba converts a hex colour to an rgba string at a given opacity, backing the CI band and point fills.

QT-PLG-002 armColorScale assigns stable, cycling colours per arm, consistent across the central-tendency, scatter, and categorical views.

QT-PLG-003 scatterTooltip renders the outlier-point tooltip lines โ€” id, arm, baseline, value, change, and visit.

โ€”#68

getPlugins.test.js

  • pass qt-explorer getPlugins helpers (QT-OUT-004/006) QT-PLG-001: hexToRgba converts to rgba at opacity
  • pass qt-explorer getPlugins helpers (QT-OUT-004/006) QT-PLG-002: armColorScale assigns stable cycling colors
  • pass qt-explorer getPlugins helpers (QT-OUT-004/006) QT-PLG-003: scatterTooltip lists id, arm, baseline, value, change, visit
QT-API-001 (module export)

QT-API-001 The public safety.viz collection exposes a qtExplorer factory function, identical to the named qtExplorer export.

โ€”#68

export.test.js

  • pass qt-explorer module export QT-API-001: the public collection exposes the qtExplorer factory

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.

Source-matrix routing status

No reviewed source requirement matrix exists yet for this module (the config's qt-explorer.md matrix link resolves once it is published in the requirements repo), so routing is against the Phase-1 port spec's scope rather than matrix rows.

View-selector rollout (VIEW-2, #76)

The View selector (QT-CTRL-001) is now the shared shell builder renderViewSelector โ€” the module-scoped qt-view-* CSS + builder were removed with no visual or behavioral change (option class names moved to the neutral sv-view-* namespace). See view-selector-inventory.md.

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/qt-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.