Safety Results Over Time: test evidence

Requirement-traced qualification evidence for the safety.viz results-over-time module.

Scope
27 coverage rows48 distinct requirement IDs
Tests executed
223 automated checks36 browser ยท 187 unit
Result
pass all passing11 evidence screenshots
Generated
2026-08-15 09:05 UTC
Environment
linux 6.17.0-1022-azure ยท node v22.23.2 ยท playwright 1.61.1 ยท chromium 149.0.7827.55

Scope & approach

Traceability for the results-over-time module (a Chart.js reimplementation of RhoInc/safety-results-over-time, under #27), per the convention in CONTRIBUTING.md. Requirement IDs (SROT-*) come from the reviewed 58-row matrix at safety.agent docs/requirements/safety-results-over-time.md, whose Evidence Type column routes rows (unit โ†’ Vitest, browser โ†’ Playwright).

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/results-over-time.spec.js)

16 requirement rows ยท 16 tests

RequirementSource matrix rowsIssueTests & evidence
SROT-FUNC-001/SROT-FUNC-002

SROT-FUNC-001 This drop-down menu is used to filter the lab variable that is displayed in the chart. The default measure is set to the first variable in the data set, but the default value can be adjusted to any measure upon the project team's request. Users can select any pre-defined measure in this filter. Selecting a measure will display the data points associated with that measure and will display the visits that the measure is collected at underneath the chart.

SROT-FUNC-002 The chart can be configured to have any number of data filters. Each specified filter has a drop-down menu that is used to filter the visit data that is displayed in the chart.

SROT-FUNC-001, SROT-FUNC-002#27
  • pass SROT-FUNC-001/SROT-FUNC-002: renders measure, group, filter, y-axis, scale, and display controls (#27)
Evidence screenshot SROT-FUNC-001-control-panel.png
SROT-FUNC-003/SROT-REG-001

SROT-FUNC-003 Displayed under the header, this statement provides the total number of participants in the data set in integers as well as the percentage of participants shown to one decimal place. In the default view, all participant data is included in the chart, or 100%, unless the project team requests otherwise. When filters are used, the data shown in the histogram is manipulated and the number of participant records shown at the top of the page may increase or decrease, depending on the variable selected.

SROT-REG-001 Confirm that the number and percentage of participants is displayed

SROT-FUNC-003, SROT-REG-001#27
  • pass SROT-FUNC-003: participant note updates when a filter is applied (#27)
Evidence screenshot SROT-FUNC-003-participant-count.png
SROT-DATA-002

SROT-DATA-002 one record per participant per visit per medical sign

SROT-DATA-002#27
  • pass SROT-DATA-002: missing and non-numeric results are dropped with a reported count and visible note (#27)
Evidence screenshot SROT-DATA-002-invalid-data-note.png
SROT-FUNC-008/SROT-REG-009

SROT-FUNC-008 The chart includes box and whisker plots to show the data distribution for a given measure. These plots show the data distribution for a data measure and can be added to the chart by clicking the box to the right of the Box plots text located above the chart. To take the plots away, the user can uncheck the box by clicking in it again. The box is checked by default, so the box and whisker plots appear when the chart is rendered.

SROT-REG-009 Confirm that selecting the box plot option causes box plots to appear around the data ranges

SROT-FUNC-008, SROT-REG-009#27
  • pass SROT-FUNC-008: box-and-whisker marks render and toggle off with the Box plots control (#27)
Evidence screenshot SROT-FUNC-008-box-plots.png
SROT-REG-002/SROT-REG-003

SROT-REG-002 Confirm that the filters are working as expected (Measure, Group): selecting a variable changes the plot values. Customize the chart to add the Group filter: {"groups":["SEX","RACE"]}

SROT-REG-003 Confirm that when a grouping is selected, the order of box/violin plots for each x-axis value match the group order in the legend.

SROT-REG-002, SROT-REG-003#27
  • pass SROT-REG-002/SROT-REG-003: grouping draws side-by-side boxes with a group-ordered legend (#27)
Evidence screenshot SROT-REG-002-grouped-box-plots.png
SROT-REG-010/SROT-REG-012

SROT-REG-010 Confirm that checking the Outliers checkbox causes results outside the 5th and 9th percentiles (i.e. results outside the horizontal lines of box plots) to be displayed in the chart.

SROT-REG-012 Confirm that unchecking the Outliers checkbox hides the outliers from the chart.

SROT-REG-010, SROT-REG-012#27
  • pass SROT-REG-010/SROT-REG-012: the outlier overlay shows and hides with the Outliers control (#27)
Evidence screenshot SROT-REG-010-outliers.png
SROT-REG-011

SROT-REG-011 Confirm that Outlier points are clearly visible in the chart, even with Violin plots selected and that hovering on an outlier causes it to expand.

SROT-REG-011#27
  • pass SROT-REG-011: outlier points carry a larger hover radius than their resting radius (#27)
SROT-REG-014/SROT-REG-015

SROT-REG-014 Confirm that hovering over a plot displays a tooltip with information about the data (N, Min, Median, etc.). Verify that

SROT-REG-015 Min and Max have the lowest precision; 5th %, Q1, Median, Q3, 95th %, and Mean have precision to one more decimal place than the lowest precision; and StDev has precision to two more decimal places than the lowest precision.

SROT-REG-014, SROT-REG-015#27
  • pass SROT-REG-014/SROT-REG-015: hovering a box exposes the summary statistics tooltip (#27)
SROT-FUNC-004/SROT-REG-016/SROT-REG-017

SROT-FUNC-004 Users are able to adjust the lower limit of the y-axis by manipulating the input within the Lower Limit box. Users are able to adjust the upper limit of the y-axis by manipulating the input within the Upper Limit box. Each limit box has up and down arrows to adjust the value of the box by 1. Users can select an arrow to change the input or delete the current value in the box and type in a new value. When typing in a value, users must click outside of the box to apply the changes. When a user manipulates a y-axis limit, the y-axis on the chart below the controls will reflect the change.

SROT-REG-016 Confirm that typing in a number into either the lower limit or upper limit control updates the y-axis domain accordingly.

SROT-REG-017 Confirm that the lower limit cannot be equal to or greater than the upper limit and the upper limit cannot be equal to or less than the lower limit. Entering this condition should cause the limits to flip automatically and always have the larger limit as the Upper Limit.

SROT-FUNC-004, SROT-REG-016, SROT-REG-017#27
  • pass SROT-FUNC-004/SROT-REG-016/SROT-REG-017: y-limit inputs redraw and invert a crossed pair (#27)
Evidence screenshot SROT-FUNC-004-y-limits.png
SROT-FUNC-005/SROT-REG-020

SROT-FUNC-005 Users can change the upper and lower y-axis limits back to the default values for a measure by selecting the "Reset Limits" button. Pressing the button will erase the current values in the upper and lower limit boxes and replace them with defaults. The chart below will also reflect the changes and will show the default axes.

SROT-REG-020 Confirm that the Reset Limits button resets the y-axis to the minimum and maximum values in the data.

SROT-FUNC-005, SROT-REG-020#27
  • pass SROT-FUNC-005/SROT-REG-020: Reset Limits restores the data extent (#27)
SROT-REG-018

SROT-REG-018 Confirm that a Scale radio control appears next to the y-axis limit controls with two options, linear and log, and that selecting either radio button updates the scale of the y-axis updates as expected.

SROT-REG-018#27
  • pass SROT-REG-018: the Scale control switches the y-axis between linear and log (#27)
Evidence screenshot SROT-REG-018-log-scale.png
SROT-FUNC-006/SROT-REG-004/SROT-REG-005

SROT-FUNC-006 Users can select the checkbox to view visits that do not contain data. Checking the box will show a timepoint for the visit. Unchecking the checkbox will hide the timepoint for visits without data; visits without data are hidden on default.

SROT-REG-004 Confirm that checking the "Visits without data" checkbox shows a timepoint for visits with no associated data points

SROT-REG-005 Confirm that unchecking the "Visits without data" checkbox hides the timepoint for visits with no associated data points

SROT-FUNC-006, SROT-REG-004, SROT-REG-005#27
  • pass SROT-FUNC-006/SROT-REG-004/SROT-REG-005: the Visits without data control adds empty timepoints (#27)
Evidence screenshot SROT-FUNC-006-visits-without-data.png
SROT-FUNC-007/SROT-REG-006/SROT-REG-007

SROT-FUNC-007 Users can select the checkbox to view data that is not associated with a scheduled visit. Checking the box will show a timepoint for the unscheduled visit and its associated data. Unselecting the checkbox will hide the unscheduled visit timepoint and its data; unscheduled visits are hidden on default.

SROT-REG-006 Confirm that checking the "Unscheduled visits" checkbox shows a timepont for unscheduled visits and their associated data

SROT-REG-007 Confirm that unchecking the "Unscheduled visits" checkbox hides the timepoint for unscheduled visits and their associated data

SROT-FUNC-007, SROT-REG-006, SROT-REG-007#27
  • pass SROT-FUNC-007/SROT-REG-006/SROT-REG-007: the Unscheduled visits control shows unscheduled timepoints (#27)
Evidence screenshot SROT-FUNC-007-unscheduled-visits.png
SROT-REG-023/SROT-REG-024

SROT-REG-023 Confirm that you can update the start_value setting for a Measure to have the Measure dropdown default to displaying a measure that is not the first one alphabetically in the list.

SROT-REG-024 Confirm that you can update the start_value setting for a Measure to have a non-existent Measure and chart will handle it and display an existing measure by default instead. Verify that a warning is written to the console explaining that the specified measure does not exist.

SROT-REG-023, SROT-REG-024#27
  • pass SROT-REG-023/SROT-REG-024: start_value selects the initial measure and falls back when absent (#27)
SROT-API
โ€” (see legacy-API note)#27
  • pass SROT-API: lifecycle API supports init, setData, setSettings, render, resize, and destroy (#27)
SROT-AXIS-001/002/003

SROT-AXIS-001 On load and after every render, each Y-axis Limits input displays the limit currently in force for the selected measure โ€” the measure's data extent (positive values only on a log scale) by default, or the user's override โ€” formatted to three significant figures of the axis range. Neither box is ever blank.

SROT-AXIS-002 Displaying a limit does not pin it. A limit the user has not edited remains automatic โ€” it is re-derived from the data on every render, so changing the measure, the filters, or the y-axis scale updates the value shown โ€” while an edited limit persists as an override until it is reset. The distinction is held in state (an override slot separate from the recorded domain), not inferred from an empty input.

SROT-AXIS-003 The Reset Limits control clears both y-axis limits overrides and repopulates both inputs with the derived limits, so there is always a way back to automatic now that an empty box no longer means it.

SROT-AXIS-001, SROT-AXIS-002, SROT-AXIS-003#85
  • pass SROT-AXIS-001/SROT-AXIS-002/SROT-AXIS-003: y-limit inputs load pre-filled with the drawn axis, follow the measure, and Reset restores them (#85)
Evidence screenshot SROT-AXIS-001-y-limits-prefilled.png

Unit evidence (Vitest โ€” tests/unit/results-over-time/)

11 requirement rows ยท 33 tests

RequirementSource matrix rowsIssueTests & evidence
SROT-CFG-004/005/006/007/009

SROT-CFG-004 name of variable that captures unique identifier of participant

SROT-CFG-005 name of variable that captures visit name

SROT-CFG-007 name of variable that captures measure name

SROT-CFG-009 name of variable that captures measure result

SROT-CFG-006 name of variable that captures visit order

SROT-CFG-004, SROT-CFG-005, SROT-CFG-007, SROT-CFG-009#27

configure.test.js

  • pass results-over-time configure SROT-CFG-004/005/006/007/009: default column mappings match the original renderer (#27)
  • pass results-over-time structureData SROT-CFG-005: computeVisitOrder falls back to alphanumeric order without an order column (#27)
  • pass results-over-time structureData SROT-CFG-005/006: computeVisitOrder sorts by the numeric order column when present (#27)
SROT-CFG-008/012/013/014/015

SROT-CFG-008 name of variable that captures measure unit

SROT-CFG-012 an array of filter variables and associated metadata

SROT-CFG-013 an array of grouping variables and associated metadata

SROT-CFG-014 grouping variable; defaults to first item in groups setting; set to 'srot_none' to display chart without grouping on initialization

SROT-CFG-015 controls initial display of outliers outside the 5th and 95th percentiles

SROT-CFG-008, SROT-CFG-012, SROT-CFG-013, SROT-CFG-014#27

configure.test.js

  • pass results-over-time configure SROT-CFG-012: filters normalize to { value_col, label } specs and drop blanks (#27)
  • pass results-over-time configure SROT-CFG-013/014: groups always offer a leading None and group_by falls back to it (#27)
  • pass results-over-time configure SROT-CFG-014: an explicit group_by is honored and its column is offered (#27)
  • pass results-over-time configure SROT-CFG-015/DATA-003: box/outlier/visit toggles default to the original renderer values (#27)
SROT-CFG-017/018/019, SROT-DATA-003

SROT-CFG-017 a regular expression that identifies unscheduled visits

SROT-CFG-019 an array of strings that identify unscheduled visits; overrides unscheduled_visit_pattern

SROT-DATA-003 controls display of visits without data for the current measure

SROT-CFG-018 default: `"/unscheduled\\

SROT-CFG-017, SROT-CFG-019, SROT-DATA-003#27

configure.test.js / structureData.test.js

  • pass results-over-time configure SROT-CFG-017/018/019: unscheduled-visit detection defaults are preserved (#27)
  • pass results-over-time structureData SROT-CFG-017/018: parseUnscheduledPattern reads the /.../flags string form (#27)
  • pass results-over-time structureData SROT-CFG-019: isUnscheduledVisit lets an explicit values list take precedence over the pattern (#27)
SROT-DATA-001/002

SROT-DATA-001 The Safety Results Over Time accepts JSON data of the format returned by d3.csv(). The renderer visualizes clinical medical signs data with one row per participant per visit per medical sign plus the required variables specified below.

SROT-DATA-002 one record per participant per visit per medical sign

SROT-DATA-001, SROT-DATA-002#27

checkInputs.test.js / structureData.test.js

  • pass results-over-time checkInputs SROT-DATA-001: passes when the required measure, result, and visit columns are present (#27)
  • pass results-over-time checkInputs SROT-DATA-001: throws naming every missing required column (#27)
  • pass results-over-time checkInputs SROT-DATA-002: an empty dataset reports all required columns missing (#27)
  • pass results-over-time structureData SROT-DATA-002: cleanData drops missing and non-numeric results with a reported count (#27)
Evidence screenshot SROT-DATA-002-invalid-data-note.png
SROT-CFG-005/006

SROT-CFG-005 name of variable that captures visit name

SROT-CFG-006 name of variable that captures visit order

SROT-CFG-005, SROT-CFG-006#27

structureData.test.js

  • pass results-over-time structureData SROT-CFG-005: computeVisitOrder falls back to alphanumeric order without an order column (#27)
  • pass results-over-time structureData SROT-CFG-005/006: computeVisitOrder sorts by the numeric order column when present (#27)
SROT-REG-010/012

SROT-REG-010 Confirm that checking the Outliers checkbox causes results outside the 5th and 9th percentiles (i.e. results outside the horizontal lines of box plots) to be displayed in the chart.

SROT-REG-012 Confirm that unchecking the Outliers checkbox hides the outliers from the chart.

SROT-REG-010, SROT-REG-012#27

structureData.test.js

  • pass results-over-time structureData SROT-REG-010/012: flagOutliers marks values outside the 5th/95th percentiles only when enabled (#27)
Evidence screenshot SROT-REG-010-outliers.png
SROT-REG-002/010 (grouping-change regression)

SROT-REG-002 Confirm that the filters are working as expected (Measure, Group): selecting a variable changes the plot values. Customize the chart to add the Group filter: {"groups":["SEX","RACE"]}

SROT-REG-010 Confirm that checking the Outliers checkbox causes results outside the 5th and 9th percentiles (i.e. results outside the horizontal lines of box plots) to be displayed in the chart.

SROT-REG-002, SROT-REG-010#27

structureData.test.js

  • pass results-over-time structureData SROT-REG-002/010: flagOutliers recomputes each row group when the grouping changes between renders (#27)
  • pass results-over-time structureData SROT-REG-010/012: flagOutliers marks values outside the 5th/95th percentiles only when enabled (#27)
Evidence screenshot SROT-REG-002-grouped-box-plots.pngEvidence screenshot SROT-REG-010-outliers.png
SROT-REG-021

SROT-REG-021 Confirm that the unit value for a given measure is appended to the measure in the Measure dropdown and in the y-axis label.

SROT-REG-021#27

structureData.test.js

  • pass results-over-time structureData SROT-REG-021: measureLabel appends the unit when present (#27)
SROT-REG-015/016/017/020

SROT-REG-015 Min and Max have the lowest precision; 5th %, Q1, Median, Q3, 95th %, and Mean have precision to one more decimal place than the lowest precision; and StDev has precision to two more decimal places than the lowest precision.

SROT-REG-016 Confirm that typing in a number into either the lower limit or upper limit control updates the y-axis domain accordingly.

SROT-REG-017 Confirm that the lower limit cannot be equal to or greater than the upper limit and the upper limit cannot be equal to or less than the lower limit. Entering this condition should cause the limits to flip automatically and always have the larger limit as the Upper Limit.

SROT-REG-020 Confirm that the Reset Limits button resets the y-axis to the minimum and maximum values in the data.

SROT-REG-015, SROT-REG-016, SROT-REG-017, SROT-REG-020#27

getScales.test.js

  • pass results-over-time getScales SROT-REG-015: statPrecisions gives min/max the base, quantiles +1, and StDev +2 (#27)
  • pass results-over-time getScales SROT-REG-015: yPrecision follows the original log10-range rule (#27)
  • pass results-over-time getScales SROT-REG-016/020: resolveYDomain uses the data extent unless a limit overrides it (#27)
  • pass results-over-time getScales SROT-REG-017: normalizeDomain swaps a crossed lower/upper pair in place (#27)
SROT-REG-003/011/014/015

SROT-REG-003 Confirm that when a grouping is selected, the order of box/violin plots for each x-axis value match the group order in the legend.

SROT-REG-011 Confirm that Outlier points are clearly visible in the chart, even with Violin plots selected and that hovering on an outlier causes it to expand.

SROT-REG-014 Confirm that hovering over a plot displays a tooltip with information about the data (N, Min, Median, etc.). Verify that

SROT-REG-015 Min and Max have the lowest precision; 5th %, Q1, Median, Q3, 95th %, and Mean have precision to one more decimal place than the lowest precision; and StDev has precision to two more decimal places than the lowest precision.

SROT-REG-003, SROT-REG-011, SROT-REG-014, SROT-REG-015#27

getPlugins.test.js

  • pass results-over-time getPlugins SROT-REG-003: groupColors assigns a stable color per group in order (#27)
  • pass results-over-time getPlugins SROT-REG-011: outlierTooltip names the participant and its value (#27)
  • pass results-over-time getPlugins SROT-REG-014/015: summaryTooltip lists every statistic at the required precision (#27)
  • pass results-over-time getScales SROT-REG-015: statPrecisions gives min/max the base, quantiles +1, and StDev +2 (#27)
  • pass results-over-time getScales SROT-REG-015: yPrecision follows the original log10-range rule (#27)
  • pass results-over-time structureData SROT-REG-014/015: summarize returns the tooltip statistics for a sorted sample (#27)
SROT-AXIS-001/002/003/004

SROT-AXIS-001 On load and after every render, each Y-axis Limits input displays the limit currently in force for the selected measure โ€” the measure's data extent (positive values only on a log scale) by default, or the user's override โ€” formatted to three significant figures of the axis range. Neither box is ever blank.

SROT-AXIS-002 Displaying a limit does not pin it. A limit the user has not edited remains automatic โ€” it is re-derived from the data on every render, so changing the measure, the filters, or the y-axis scale updates the value shown โ€” while an edited limit persists as an override until it is reset. The distinction is held in state (an override slot separate from the recorded domain), not inferred from an empty input.

SROT-AXIS-003 The Reset Limits control clears both y-axis limits overrides and repopulates both inputs with the derived limits, so there is always a way back to automatic now that an empty box no longer means it.

SROT-AXIS-004 Pre-filling must not let an invalid domain reach the chart: a limit typed across the other limit as currently displayed is swapped rather than inverted, an empty or non-numeric entry returns that limit to automatic, and a log-scale domain stays positive.

SROT-AXIS-001, SROT-AXIS-002, SROT-AXIS-003, SROT-AXIS-004#85

axis-limits.test.js

  • pass results-over-time axis-limit prefill (SROT-AXIS-001) SROT-AXIS-001: displaying a limit does not pin it โ€” the overrides stay null until edited (#85)
  • pass results-over-time axis-limit prefill (SROT-AXIS-001) SROT-AXIS-001: the Y-axis limit inputs load pre-filled with the domain the chart drew (#85)
  • pass results-over-time limit guardrails with prefilled inputs (SROT-AXIS-004) SROT-AXIS-004: a lower limit typed above the prefilled upper limit is swapped, never inverted (#85)
  • pass results-over-time limit guardrails with prefilled inputs (SROT-AXIS-004) SROT-AXIS-004: clearing a prefilled input returns that limit to auto and the box refills (#85)
  • pass results-over-time Reset Limits (SROT-AXIS-003) SROT-AXIS-003: Reset Limits clears the overrides and repopulates both inputs with the derived domain (#85)
  • pass results-over-time unedited limits stay automatic (SROT-AXIS-002) SROT-AXIS-002: an edited limit survives a filter change and stays an override (#85)
  • pass results-over-time unedited limits stay automatic (SROT-AXIS-002) SROT-AXIS-002: an unedited limit re-derives on a measure change and the input follows (#85)
  • pass results-over-time unedited limits stay automatic (SROT-AXIS-002) SROT-AXIS-002: an unedited lower limit re-derives when the scale switches to log (#85)
Evidence screenshot SROT-AXIS-001-y-limits-prefilled.png

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 (58 rows)

Covered: 44 of 58 rows (the browser and unit tables above). The remaining 14 rows are descoped, honestly, for the reasons below.

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

One primary display โ€” the per-visit box-and-whisker plot with an outlier overlay. "Scale" is an axis-type toggle and the Display checkboxes are non-exclusive overlay toggles on one chart, not a chooser between displays. Ruled single-view in the shared view-selector rollout (#76) โ€” no view control is added; 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/results-over-time/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.