File size: 5,075 Bytes
50841a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
 * Human-impact modeling for accessibility findings.
 *
 * Maps each WCAG 2.x success criterion to the Section 508 Functional
 * Performance Criteria (FPC) it relates to, and each FPC to a disability
 * group with a US population prevalence estimate. This lets a finding say
 * *who* it affects and roughly *how many people*, instead of an opaque
 * "requires manual testing".
 *
 * Sources:
 *  - WCAG -> FPC mapping: GSA Section 508 program,
 *    https://www.section508.gov/develop/mapping-wcag-to-fpc/ (extended to
 *    WCAG 2.2 criteria here). FPC categories also align with EU EN 301 549
 *    Table B.2.
 *  - Prevalence: US Census Bureau American Community Survey (ACS) 2022
 *    1-Year Estimates (disability tables), with supplemental CDC/NIDCD/NEI
 *    figures. These are population-level estimates, not site analytics —
 *    use them to convey scale, not as precise measurements.
 *
 * Approach borrowed from mgifford/daily-dap.
 */

// FPC code -> disability group + US prevalence (fraction of population).
export const FPC = {
  WV:    { code: 'WV',    group: 'Without vision',        prevalence: 0.010 },
  LV:    { code: 'LV',    group: 'Limited vision',        prevalence: 0.024 },
  WPC:   { code: 'WPC',   group: 'Without color perception', prevalence: 0.045 },
  WH:    { code: 'WH',    group: 'Without hearing',       prevalence: 0.003 },
  LH:    { code: 'LH',    group: 'Limited hearing',       prevalence: 0.035 },
  WS:    { code: 'WS',    group: 'Without speech',        prevalence: 0.005 },
  LM:    { code: 'LM',    group: 'Limited manipulation/strength', prevalence: 0.130 },
  LR:    { code: 'LR',    group: 'Limited reach',         prevalence: 0.020 },
  LCLA:  { code: 'LCLA',  group: 'Limited cognitive, language, or learning ability', prevalence: 0.110 },
};

// WCAG success criterion -> the FPC codes it most directly affects.
// Covers WCAG 2.0/2.1/2.2 Level A and AA. A criterion with no entry
// returns an empty impact (treated as undetermined rather than guessed).
const SC_TO_FPC = {
  // 1.1 Text Alternatives
  '1.1.1': ['WV', 'LV', 'LCLA'],
  // 1.2 Time-based Media
  '1.2.1': ['WV', 'WH'],
  '1.2.2': ['WH', 'LH'],
  '1.2.3': ['WV', 'LV'],
  '1.2.4': ['WH', 'LH'],
  '1.2.5': ['WV', 'LV'],
  // 1.3 Adaptable
  '1.3.1': ['WV', 'LV', 'LCLA'],
  '1.3.2': ['WV', 'LCLA'],
  '1.3.3': ['WV', 'LV', 'LCLA'],
  '1.3.4': ['LV', 'LM'],
  '1.3.5': ['LCLA', 'LM'],
  // 1.4 Distinguishable
  '1.4.1': ['WPC', 'LV'],
  '1.4.2': ['WV', 'LV'],
  '1.4.3': ['LV', 'WPC'],
  '1.4.4': ['LV'],
  '1.4.5': ['LV', 'WPC'],
  '1.4.10': ['LV', 'LM'],
  '1.4.11': ['LV', 'WPC'],
  '1.4.12': ['LV', 'LCLA'],
  '1.4.13': ['LV', 'LM', 'LCLA'],
  // 2.1 Keyboard Accessible
  '2.1.1': ['WV', 'LM', 'LR'],
  '2.1.2': ['WV', 'LM', 'LR'],
  '2.1.4': ['WV', 'LM'],
  // 2.2 Enough Time
  '2.2.1': ['LM', 'LR', 'LCLA'],
  '2.2.2': ['LV', 'LCLA'],
  // 2.3 Seizures
  '2.3.1': ['LCLA'],
  // 2.4 Navigable
  '2.4.1': ['WV', 'LM'],
  '2.4.2': ['WV', 'LV', 'LCLA'],
  '2.4.3': ['WV', 'LM'],
  '2.4.4': ['WV', 'LV', 'LCLA'],
  '2.4.5': ['WV', 'LV', 'LCLA'],
  '2.4.6': ['WV', 'LV', 'LCLA'],
  '2.4.7': ['LV', 'LM'],
  '2.4.11': ['LV', 'LM'],
  // 2.5 Input Modalities
  '2.5.1': ['LM', 'LR'],
  '2.5.2': ['LM', 'LR'],
  '2.5.3': ['WV', 'WS'],
  '2.5.4': ['LM', 'LR'],
  '2.5.7': ['LM', 'LR'],
  '2.5.8': ['LM', 'LR', 'LV'],
  // 3.1 Readable
  '3.1.1': ['WV', 'LCLA'],
  '3.1.2': ['WV', 'LCLA'],
  // 3.2 Predictable
  '3.2.1': ['WV', 'LV', 'LCLA'],
  '3.2.2': ['WV', 'LV', 'LCLA'],
  '3.2.3': ['LV', 'LCLA'],
  '3.2.4': ['LV', 'LCLA'],
  '3.2.6': ['LCLA'],
  // 3.3 Input Assistance
  '3.3.1': ['WV', 'LV', 'LCLA'],
  '3.3.2': ['WV', 'LV', 'LCLA'],
  '3.3.3': ['WV', 'LV', 'LCLA'],
  '3.3.4': ['LCLA'],
  '3.3.7': ['LCLA', 'LM'],
  '3.3.8': ['LCLA', 'LM'],
  // 4.1 Compatible
  '4.1.1': ['WV', 'LV'],
  '4.1.2': ['WV', 'LV', 'LM'],
  '4.1.3': ['WV', 'LV'],
};

/**
 * Impact for a WCAG success criterion: the disability groups affected,
 * each with prevalence. Returns { groups: [{code, group, prevalence}],
 * maxPrevalence } or null when the SC is unmapped/unknown.
 */
export function impactFor(sc) {
  const codes = SC_TO_FPC[sc];
  if (!codes || codes.length === 0) return null;
  const groups = codes.map((c) => FPC[c]).filter(Boolean);
  if (groups.length === 0) return null;
  return {
    groups,
    // The single most-affected group's prevalence — a conservative
    // headline figure (groups overlap, so we don't sum them).
    maxPrevalence: Math.max(...groups.map((g) => g.prevalence)),
  };
}

/**
 * Optional people-excluded estimate, when page-load data is supplied.
 *   prevalence: fraction (0-1)
 *   pageLoads:  loads attributable to the affected pages
 * Rough by design — meant to convey scale, not precision.
 */
export function estimateExcluded(prevalence, pageLoads) {
  if (!pageLoads || pageLoads <= 0) return null;
  return Math.round(prevalence * pageLoads);
}

/** Format a prevalence fraction as a percent string, e.g. 0.024 -> "2.4%". */
export function pct(fraction) {
  return `${(fraction * 100).toFixed(1)}%`;
}