diagcalc 3.2.4 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html CHANGED
@@ -6,89 +6,78 @@
6
6
  <title>DIAG-CALC</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" />
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" />
10
10
  <link rel="stylesheet" href="styles.css" />
11
11
  <script defer src="https://analytics.tiagojct.eu/script.js" data-website-id="2c7df365-61e6-42e6-8298-a20ada3e296a"></script>
12
12
  </head>
13
13
  <body>
14
- <header class="intro" role="banner">
15
- <div class="container">
16
- <div class="header-controls">
17
- <button id="themeToggle" class="theme-toggle" aria-label="Toggle dark mode" type="button">
14
+ <header class="app-bar" role="banner">
15
+ <div class="app-bar-inner">
16
+ <div class="app-bar-brand">
17
+ <span class="brand-mark">DIAGCALC</span>
18
+ <span class="app-bar-version" aria-label="version">v5.0.0</span>
19
+ <span class="app-bar-divider" aria-hidden="true">·</span>
20
+ <span class="app-bar-case" id="appCaseStrip" data-i18n="app.case.empty">No case yet</span>
21
+ </div>
22
+ <div class="app-bar-controls">
23
+ <label class="lang-picker" for="languageSelect">
24
+ <span class="visually-hidden" data-i18n="lang.label">Language</span>
25
+ <select id="languageSelect" aria-label="Language">
26
+ <option value="en">English</option>
27
+ <option value="pt-PT">Português</option>
28
+ </select>
29
+ </label>
30
+ <button id="themeToggle" class="theme-toggle" type="button" data-i18n-attr="aria-label=theme.toggle.label">
18
31
  <span class="theme-icon">◐</span>
19
32
  </button>
20
33
  </div>
21
- <p class="tag">Educational tool</p>
22
- <h1>DIAGCALC</h1>
23
- <p class="lead">
24
- Interactive calculator for exploring diagnostic test performance measures
25
- and applying Bayesian reasoning in clinical practice.
26
- </p>
27
- <div class="intro-highlights" role="list">
28
- <article class="highlight" role="listitem">
29
- <h2>Key measures</h2>
30
- <p>Sensitivity, specificity, PPV, NPV, and likelihood ratios with 95%&nbsp;CI.</p>
31
- </article>
32
- <article class="highlight" role="listitem">
33
- <h2>Post-test probability</h2>
34
- <p>Convert pre-test probability into positive and negative post-test probability.</p>
35
- </article>
36
- <article class="highlight" role="listitem">
37
- <h2>Guided interpretation</h2>
38
- <p>Results explained to support self-directed learning.</p>
39
- </article>
40
- </div>
41
34
  </div>
42
35
  </header>
43
36
  <main class="container" id="main" role="main">
44
37
  <div class="workspace">
45
- <section aria-labelledby="library-title" class="panel panel-library">
38
+ <section aria-labelledby="form-title" class="panel panel-form">
46
39
  <div class="panel-heading">
47
- <p class="panel-kicker">Library</p>
48
- <h2 id="library-title">Load a study scenario</h2>
49
- </div>
50
- <div class="form-row">
51
- <label class="full" for="datasetSelect">Scenario</label>
52
- <select id="datasetSelect" class="full" aria-describedby="dataset-help">
53
- <option value="">— Select —</option>
54
- <optgroup label="Generic scenarios">
55
- <option value="screening">Screening programme (low prevalence)</option>
56
- <option value="caseControl">Case-control study</option>
57
- <option value="clinic">Specialist clinic</option>
58
- </optgroup>
59
- <optgroup label="Medical literature cases">
60
- <option value="ddimer">D-dimer for pulmonary embolism</option>
61
- <option value="troponin">High-sensitivity troponin for AMI</option>
62
- <option value="mammography">Screening mammography</option>
63
- <option value="covid_antigen">Rapid COVID-19 antigen test</option>
64
- <option value="hiv_elisa">HIV ELISA</option>
65
- <option value="strep_throat">Rapid Streptococcus test</option>
66
- <option value="xray_pneumonia">Chest X-ray for pneumonia</option>
67
- </optgroup>
68
- </select>
69
- <p id="dataset-help" class="help-text">
70
- Values are filled in automatically and can be adjusted afterwards.
71
- </p>
72
- <div id="dataset-reference" class="dataset-reference" style="display: none;"></div>
73
- </div>
74
- </section>
75
-
76
- <section aria-labelledby="form-title" class="panel panel-inputs">
77
- <div class="panel-heading">
78
- <p class="panel-kicker">Editor</p>
79
- <h2 id="form-title">Build the confusion matrix</h2>
80
- <p>
81
- Use non-negative integers and a pre-test probability under 100%.
82
- </p>
40
+ <p class="panel-kicker" data-i18n="editor.kicker">Editor</p>
41
+ <h2 id="form-title" data-i18n="editor.h">Build the case</h2>
83
42
  </div>
84
43
  <form id="inputForm" novalidate>
44
+ <div class="form-row">
45
+ <label class="full" for="datasetSelect" data-i18n="form.scenario">Scenario</label>
46
+ <select id="datasetSelect" class="full" aria-describedby="dataset-help">
47
+ <option value="" data-i18n="form.scenario.empty">— Select —</option>
48
+ <optgroup label="Generic scenarios" data-i18n-attr="label=form.scenario.group.generic">
49
+ <option value="screening">Screening programme (low prevalence)</option>
50
+ <option value="caseControl">Case-control study</option>
51
+ <option value="clinic">Specialist clinic</option>
52
+ </optgroup>
53
+ <optgroup label="Illustrative medical scenarios" data-i18n-attr="label=form.scenario.group.literature">
54
+ <option value="ddimer">D-dimer for pulmonary embolism</option>
55
+ <option value="troponin">High-sensitivity troponin for AMI</option>
56
+ <option value="mammography">Screening mammography</option>
57
+ <option value="covid_antigen">Rapid COVID-19 antigen test</option>
58
+ <option value="hiv_elisa">HIV ELISA</option>
59
+ <option value="strep_throat">Rapid Streptococcus test</option>
60
+ <option value="xray_pneumonia">Chest X-ray for pneumonia</option>
61
+ <option value="wells_dvt">Wells score (≥2) for DVT</option>
62
+ <option value="heart_acs">HEART score (≥4) for MACE in chest pain</option>
63
+ <option value="curb65">CURB-65 (≥2) for severe community-acquired pneumonia</option>
64
+ <option value="qsofa">qSOFA (≥2) for sepsis screening</option>
65
+ <option value="apgar_5min">APGAR &lt; 7 at 5 min for severe neonatal outcome</option>
66
+ <option value="fast_trauma">FAST ultrasound for blunt abdominal trauma</option>
67
+ </optgroup>
68
+ </select>
69
+ <p id="dataset-help" class="help-text" data-i18n="library.help">
70
+ Pick a preset to load values, or fill in the matrix manually below.
71
+ </p>
72
+ <div id="dataset-reference" class="dataset-reference" style="display: none;"></div>
73
+ </div>
85
74
  <fieldset class="confusion-matrix">
86
- <legend>Confusion matrix</legend>
75
+ <legend data-i18n="form.matrix.legend">Confusion matrix</legend>
87
76
  <div class="grid-container" role="group" aria-label="Diagnostic test results">
88
77
  <span class="empty" aria-hidden="true"></span>
89
- <span class="grid-header grid-header-pos">Disease +</span>
90
- <span class="grid-header grid-header-neg">Disease -</span>
91
- <label class="grid-label grid-label-pos" for="tp">Test +</label>
78
+ <span class="grid-header grid-header-pos" data-i18n="form.matrix.dpos">Disease +</span>
79
+ <span class="grid-header grid-header-neg" data-i18n="form.matrix.dneg">Disease -</span>
80
+ <label class="grid-label grid-label-pos" for="tp" data-i18n="form.matrix.tpos">Test +</label>
92
81
  <div class="grid-item">
93
82
  <input type="number" id="tp" inputmode="numeric" min="0" step="1"
94
83
  placeholder="True positives"
@@ -101,7 +90,7 @@
101
90
  aria-label="False positives (FP): disease absent, test positive"
102
91
  required />
103
92
  </div>
104
- <label class="grid-label grid-label-neg" for="fn">Test -</label>
93
+ <label class="grid-label grid-label-neg" for="fn" data-i18n="form.matrix.tneg">Test -</label>
105
94
  <div class="grid-item">
106
95
  <input type="number" id="fn" inputmode="numeric" min="0" step="1"
107
96
  placeholder="False negatives"
@@ -117,25 +106,36 @@
117
106
  </div>
118
107
  </fieldset>
119
108
  <div class="form-row">
120
- <label for="preTestProb">Pre-test probability (%)</label>
109
+ <label for="preTestProb" data-i18n="form.pre.label">Pre-test probability (%)</label>
121
110
  <input
122
111
  class="preTestProb"
123
- type="number"
112
+ type="text"
124
113
  id="preTestProb"
125
114
  inputmode="decimal"
126
- min="0"
127
- max="100"
128
- step="0.1"
129
115
  placeholder="e.g. 12.5"
130
116
  required
131
117
  />
132
- <p class="help-text">
118
+ <p class="help-text" data-i18n="form.pre.help">
133
119
  Suggestion: estimate the disease prevalence in the study population or the clinical probability before the test.
134
120
  </p>
135
121
  </div>
122
+ <details class="advanced-settings">
123
+ <summary data-i18n="form.advanced.summary">Advanced settings</summary>
124
+ <div class="form-row">
125
+ <label for="continuityModeSelect" data-i18n="form.advanced.continuity">Continuity correction for LR / DOR CIs</label>
126
+ <select id="continuityModeSelect" class="full">
127
+ <option value="auto" selected data-i18n="form.advanced.continuity.auto">Auto — apply +0.5 only when a 2×2 cell is zero</option>
128
+ <option value="always" data-i18n="form.advanced.continuity.always">Always — apply +0.5 to every cell</option>
129
+ <option value="never" data-i18n="form.advanced.continuity.never">Never — leave undefined intervals unavailable</option>
130
+ </select>
131
+ <p class="help-text" data-i18n="form.advanced.continuity.help">
132
+ When any cell of the confusion matrix is zero the log-normal CI for LR+ / LR− / DOR is undefined.
133
+ </p>
134
+ </div>
135
+ </details>
136
136
  <div class="form-actions" role="group">
137
- <button type="submit">Calculate results</button>
138
- <button type="reset" id="resetButton" class="secondary">Clear fields</button>
137
+ <button type="submit" data-i18n="form.calculate">Calculate results</button>
138
+ <button type="reset" id="resetButton" class="secondary" data-i18n="form.clear">Clear fields</button>
139
139
  </div>
140
140
  <p id="feedback" class="feedback" role="alert" aria-live="polite"></p>
141
141
  </form>
@@ -144,59 +144,340 @@
144
144
  <section aria-labelledby="results-title" class="panel panel-results results-section">
145
145
  <div class="panel-heading panel-heading-inline">
146
146
  <div>
147
- <p class="panel-kicker">Output</p>
148
- <h2 id="results-title">Results</h2>
149
- <p class="help-text">
150
- Values are shown with 95% confidence intervals calculated using the Wilson method.
147
+ <p class="panel-kicker" data-i18n="results.kicker">Output</p>
148
+ <h2 id="results-title" data-i18n="results.h">Results</h2>
149
+ <p class="help-text" data-i18n="results.help">
150
+ Values are shown with 95% confidence intervals where applicable.
151
151
  </p>
152
152
  </div>
153
153
  <div class="results-actions" role="group">
154
- <button type="button" id="printButton" class="secondary">Print report</button>
154
+ <button type="button" id="printButton" class="secondary" data-i18n="results.print">Print report</button>
155
155
  </div>
156
156
  </div>
157
- <div id="probabilityChart" class="probability-chart" aria-label="Comparison of pre- and post-test probabilities"></div>
157
+ <div id="biasWarnings" class="bias-warnings" role="status" aria-live="polite"></div>
158
+ <div id="resultsHeadline" class="results-headline" aria-live="polite" tabindex="-1"></div>
159
+ <div id="bayesianUpdate" class="bayesian-update" aria-live="polite"></div>
160
+ <div id="resultsSecondary" class="results-secondary" aria-live="polite"></div>
158
161
  <div id="faganNomogram" class="fagan-nomogram" style="display: none;">
159
- <h3>Fagan Nomogram</h3>
160
- <p class="help-text">Graphical representation of the relationship between pre-test probability, likelihood ratio, and post-test probability.</p>
161
- <canvas id="faganCanvas" width="600" height="400" aria-label="Fagan nomogram"></canvas>
162
+ <h3 data-i18n="fagan.h">Fagan Nomogram</h3>
163
+ <p class="help-text" data-i18n="fagan.help">Calibrated logarithmic axes: pre-test odds × LR = post-test odds. Off-scale or undefined values are shown in the summary.</p>
164
+ <canvas id="faganCanvas" width="600" height="400" role="img" aria-label="Fagan nomogram" aria-describedby="faganSummary"></canvas>
165
+ <dl id="faganSummary" class="fagan-summary">
166
+ <div class="fagan-summary-item">
167
+ <dt data-i18n="fagan.pre">Pre-test</dt>
168
+ <dd id="faganPreValue">—</dd>
169
+ </div>
170
+ <div class="fagan-summary-item">
171
+ <dt data-i18n="fagan.lrpos">LR+</dt>
172
+ <dd id="faganLrPosValue">—</dd>
173
+ </div>
174
+ <div class="fagan-summary-item">
175
+ <dt data-i18n="fagan.lrneg">LR−</dt>
176
+ <dd id="faganLrNegValue">—</dd>
177
+ </div>
178
+ <div class="fagan-summary-item">
179
+ <dt data-i18n="fagan.postpos">Post-test (+)</dt>
180
+ <dd id="faganPostPosValue">—</dd>
181
+ </div>
182
+ <div class="fagan-summary-item">
183
+ <dt data-i18n="fagan.postneg">Post-test (−)</dt>
184
+ <dd id="faganPostNegValue">—</dd>
185
+ </div>
186
+ </dl>
162
187
  </div>
163
- <div id="results" class="results-grid" aria-live="polite"></div>
188
+ <details id="sequentialTest" class="sequential-test" style="display: none;">
189
+ <summary><span class="summary-tag summary-tag--tool" data-i18n="panel.tag.tool">Tool</span><strong>Chain a second test</strong> — feed the post-test probability of this test into a follow-up</summary>
190
+ <p class="help-text">
191
+ Real diagnostic workflows often chain tests (e.g. D-dimer → CT-PA, rapid antigen → PCR, screen → confirm). Choose which result of the first test you are following up on, then enter the second test's confusion matrix. The second test's pre-test probability is set automatically.
192
+ </p>
193
+ <div class="sequential-controls">
194
+ <label for="sequentialFrom">
195
+ <span>Follow up on test 1 result</span>
196
+ <select id="sequentialFrom">
197
+ <option value="positive">Positive (use post-test (+) as pre-test for test 2)</option>
198
+ <option value="negative">Negative (use post-test (−) as pre-test for test 2)</option>
199
+ </select>
200
+ </label>
201
+ <p class="sequential-pretest" id="sequentialPretest"></p>
202
+ </div>
203
+ <fieldset class="confusion-matrix sequential-matrix">
204
+ <legend>Test 2 — confusion matrix</legend>
205
+ <div class="grid-container" role="group" aria-label="Diagnostic test 2 results">
206
+ <span class="empty" aria-hidden="true"></span>
207
+ <span class="grid-header">Disease +</span>
208
+ <span class="grid-header">Disease −</span>
209
+ <label class="grid-label" for="tp2">Test +</label>
210
+ <div class="grid-item">
211
+ <input type="number" id="tp2" inputmode="numeric" min="0" step="1" placeholder="True positives" aria-label="Test 2 true positives" />
212
+ </div>
213
+ <div class="grid-item">
214
+ <input type="number" id="fp2" inputmode="numeric" min="0" step="1" placeholder="False positives" aria-label="Test 2 false positives" />
215
+ </div>
216
+ <label class="grid-label" for="fn2">Test −</label>
217
+ <div class="grid-item">
218
+ <input type="number" id="fn2" inputmode="numeric" min="0" step="1" placeholder="False negatives" aria-label="Test 2 false negatives" />
219
+ </div>
220
+ <div class="grid-item">
221
+ <input type="number" id="tn2" inputmode="numeric" min="0" step="1" placeholder="True negatives" aria-label="Test 2 true negatives" />
222
+ </div>
223
+ </div>
224
+ </fieldset>
225
+ <div class="form-actions">
226
+ <button type="button" id="sequentialCalc">Calculate test 2 result</button>
227
+ <button type="button" id="sequentialClear" class="secondary">Clear test 2</button>
228
+ </div>
229
+ <p id="sequentialFeedback" class="feedback" role="alert" aria-live="polite"></p>
230
+ <div id="sequentialResults" class="results-grid sequential-results" aria-live="polite"></div>
231
+ </details>
232
+ <details id="thresholdPanel" class="threshold-panel" style="display: none;">
233
+ <summary><span class="summary-tag summary-tag--tool" data-i18n="panel.tag.tool">Tool</span><strong>Decision thresholds</strong> — when to test, when to treat without testing</summary>
234
+ <p class="help-text">
235
+ Pauker–Kassirer (1980) framework. Set the treatment threshold (the post-test probability at which you would act). Below the <em>testing</em> threshold even a positive result keeps you below the action level — testing changes nothing. Above the <em>test-treatment</em> threshold even a negative result keeps you above the action level — just treat. The "test is useful" zone is the interval between them.
236
+ </p>
237
+ <div class="threshold-input">
238
+ <label for="treatmentThresholdInput">
239
+ <span>Treatment threshold</span>
240
+ <output id="treatmentThresholdValue" for="treatmentThresholdInput">30%</output>
241
+ </label>
242
+ <input type="range" id="treatmentThresholdInput" min="1" max="99" step="1" value="30" />
243
+ </div>
244
+ <div id="thresholdGauge" class="threshold-gauge" aria-hidden="true">
245
+ <div class="threshold-zone threshold-zone-no"></div>
246
+ <div class="threshold-zone threshold-zone-test"></div>
247
+ <div class="threshold-zone threshold-zone-treat"></div>
248
+ <div class="threshold-marker threshold-marker-pt" id="thresholdMarkerPt"></div>
249
+ <div class="threshold-marker threshold-marker-pre" id="thresholdMarkerPre"></div>
250
+ </div>
251
+ <div class="threshold-legend">
252
+ <span><span class="threshold-swatch threshold-zone-no"></span>Don't test</span>
253
+ <span><span class="threshold-swatch threshold-zone-test"></span>Test useful</span>
254
+ <span><span class="threshold-swatch threshold-zone-treat"></span>Treat without testing</span>
255
+ <span><span class="threshold-swatch threshold-swatch-pt"></span>Pt (treatment threshold)</span>
256
+ <span><span class="threshold-swatch threshold-swatch-pre"></span>Current pre-test</span>
257
+ </div>
258
+ <div id="thresholdReadout" class="threshold-readout"></div>
259
+ </details>
260
+ <details id="rocPanel" class="roc-panel" style="display: none;">
261
+ <summary><span class="summary-tag summary-tag--tool" data-i18n="panel.tag.tool">Tool</span><strong>Build an ROC curve</strong> — combine multiple cutoffs of a continuous test</summary>
262
+ <p class="help-text">
263
+ For a continuous test (e.g. troponin, D-dimer, score), each candidate cutoff defines its own 2×2 table. Add one row per cutoff and the plot draws sensitivity vs (1 − specificity), the trapezoidal AUC, and highlights the cutoff with the largest Youden's J (sens + spec − 1).
264
+ </p>
265
+ <div class="roc-table-wrapper">
266
+ <label for="rocDirection">Positive result direction</label>
267
+ <select id="rocDirection"><option value="higher">Higher scores are positive</option><option value="lower">Lower scores are positive</option></select>
268
+ <table class="roc-table">
269
+ <thead>
270
+ <tr>
271
+ <th scope="col">Cutoff</th>
272
+ <th scope="col">TP</th>
273
+ <th scope="col">FP</th>
274
+ <th scope="col">FN</th>
275
+ <th scope="col">TN</th>
276
+ <th scope="col" aria-label="Remove row"></th>
277
+ </tr>
278
+ </thead>
279
+ <tbody id="rocRows"></tbody>
280
+ </table>
281
+ </div>
282
+ <div class="form-actions">
283
+ <button type="button" id="rocAddRow">+ Add cutoff row</button>
284
+ <button type="button" id="rocFillSynthetic" class="secondary" title="Fit an illustrative equal-variance binormal model and generate seven simulated cutoffs.">+ Fill synthetic cutoffs</button>
285
+ <button type="button" id="rocReset" class="secondary">Reset rows</button>
286
+ </div>
287
+ <canvas id="rocCanvas" width="480" height="480" role="img" aria-label="Receiver operating characteristic curve" aria-describedby="rocReadout"></canvas>
288
+ <div id="rocReadout" class="roc-readout"></div>
289
+ </details>
290
+ <details id="historyPanel" class="history-panel" style="display: none;">
291
+ <summary><span class="summary-tag summary-tag--tool" data-i18n="panel.tag.tool">Tool</span><strong>History</strong> — last 10 calculations on this device</summary>
292
+ <p class="help-text">
293
+ Recent Calculate runs are saved in your browser. Click <em>Reload</em> to drop a case back into the form, tick two or more boxes to compare them side by side, or <em>Clear all</em> to wipe local storage. Nothing leaves this device.
294
+ </p>
295
+ <div id="historyCompare" class="history-compare" aria-live="polite"></div>
296
+ <ul id="historyList" class="history-list" aria-label="Saved calculations"></ul>
297
+ <div class="form-actions">
298
+ <button type="button" id="historyClearAll" class="secondary">Clear all history</button>
299
+ </div>
300
+ </details>
301
+ <details id="prevalenceExplorer" class="prevalence-explorer" style="display: none;">
302
+ <summary><span class="summary-tag summary-tag--tool" data-i18n="panel.tag.tool">Tool</span><strong>Explore prevalence sensitivity</strong> — see how PPV, NPV and post-test probabilities shift when prevalence changes</summary>
303
+ <p class="help-text">
304
+ Sensitivity and specificity are intrinsic to the test and stay fixed. Drag the slider to see how the predictive values and post-test probabilities respond if the disease were more or less common in the population you are testing.
305
+ </p>
306
+ <div class="prevalence-controls">
307
+ <label for="prevalenceSlider">
308
+ <span>Prevalence</span>
309
+ <output id="prevalenceValue" for="prevalenceSlider">10.0%</output>
310
+ </label>
311
+ <input type="range" id="prevalenceSlider" min="0.1" max="100" step="0.1" value="10" aria-describedby="prevalenceValue" />
312
+ </div>
313
+ <div id="prevalenceReadout" class="prevalence-readout"></div>
314
+ <canvas id="prevalenceCanvas" width="600" height="320" aria-label="PPV and NPV as a function of prevalence"></canvas>
315
+ </details>
316
+ <details id="thresholdDemo" class="threshold-demo" style="display: none;">
317
+ <summary><span class="summary-tag summary-tag--tool" data-i18n="panel.tag.tool">Tool</span><strong data-i18n="thresholdDemo.summary">Threshold explorer</strong> — <span data-i18n="thresholdDemo.summary.sub">drag the slicer to see how TP, FP, FN, TN and all metrics shift in real time</span></summary>
318
+ <p class="help-text" data-i18n="thresholdDemo.help">
319
+ A continuous test produces scores for 200 individuals: 80 diseased (higher scores) and 120 healthy (lower scores), with overlap. Move the threshold slider to see how the confusion matrix and all performance metrics change in real time. There is no perfect threshold — only trade-offs.
320
+ </p>
321
+ <canvas id="thresholdDemoCanvas" width="600" height="200" role="img" aria-label="Score distribution with threshold line"></canvas>
322
+ <div class="threshold-demo-controls">
323
+ <label for="thresholdDemoSlider">
324
+ <span data-i18n="thresholdDemo.threshold">Threshold</span>
325
+ <output id="thresholdDemoValue" for="thresholdDemoSlider">0.50</output>
326
+ </label>
327
+ <input type="range" id="thresholdDemoSlider" min="0" max="100" step="1" value="50" aria-describedby="thresholdDemoValue" />
328
+ </div>
329
+ <div id="thresholdDemoStats" class="threshold-demo-stats" aria-live="polite">
330
+ <div class="threshold-demo-stat">
331
+ <div class="threshold-demo-stat-label" data-i18n="thresholdDemo.sens">Sensitivity</div>
332
+ <div class="threshold-demo-stat-value" id="tdSens">—</div>
333
+ <div class="threshold-demo-stat-sub">TP / (TP + FN)</div>
334
+ </div>
335
+ <div class="threshold-demo-stat">
336
+ <div class="threshold-demo-stat-label" data-i18n="thresholdDemo.spec">Specificity</div>
337
+ <div class="threshold-demo-stat-value" id="tdSpec">—</div>
338
+ <div class="threshold-demo-stat-sub">TN / (TN + FP)</div>
339
+ </div>
340
+ <div class="threshold-demo-stat">
341
+ <div class="threshold-demo-stat-label" data-i18n="thresholdDemo.ppv">PPV</div>
342
+ <div class="threshold-demo-stat-value" id="tdPpv">—</div>
343
+ <div class="threshold-demo-stat-sub">TP / (TP + FP)</div>
344
+ </div>
345
+ <div class="threshold-demo-stat">
346
+ <div class="threshold-demo-stat-label" data-i18n="thresholdDemo.npv">NPV</div>
347
+ <div class="threshold-demo-stat-value" id="tdNpv">—</div>
348
+ <div class="threshold-demo-stat-sub">TN / (TN + FN)</div>
349
+ </div>
350
+ </div>
351
+ <div class="threshold-demo-matrix" aria-label="Confusion matrix at current threshold">
352
+ <div class="threshold-demo-cell head"></div>
353
+ <div class="threshold-demo-cell head" data-i18n="thresholdDemo.disease">Disease +</div>
354
+ <div class="threshold-demo-cell head" data-i18n="thresholdDemo.healthy">Disease −</div>
355
+ <div class="threshold-demo-cell head" data-i18n="thresholdDemo.testpos">Test +</div>
356
+ <div class="threshold-demo-cell tp">
357
+ <div class="threshold-demo-cell-n" id="tdTp">0</div>
358
+ <div class="threshold-demo-cell-meta" data-i18n="thresholdDemo.tp">True positives</div>
359
+ </div>
360
+ <div class="threshold-demo-cell fp">
361
+ <div class="threshold-demo-cell-n" id="tdFp">0</div>
362
+ <div class="threshold-demo-cell-meta" data-i18n="thresholdDemo.fp">False positives</div>
363
+ </div>
364
+ <div class="threshold-demo-cell head" data-i18n="thresholdDemo.testneg">Test −</div>
365
+ <div class="threshold-demo-cell fn">
366
+ <div class="threshold-demo-cell-n" id="tdFn">0</div>
367
+ <div class="threshold-demo-cell-meta" data-i18n="thresholdDemo.fn">False negatives</div>
368
+ </div>
369
+ <div class="threshold-demo-cell tn">
370
+ <div class="threshold-demo-cell-n" id="tdTn">0</div>
371
+ <div class="threshold-demo-cell-meta" data-i18n="thresholdDemo.tn">True negatives</div>
372
+ </div>
373
+ </div>
374
+ <p id="thresholdDemoNote" class="threshold-demo-note" aria-live="polite"></p>
375
+ </details>
164
376
  </section>
165
377
  </div>
166
378
 
167
379
  <div class="resource-grid">
380
+ <section aria-labelledby="kappa-title" class="panel panel-docs kappa-panel">
381
+ <div class="panel-heading">
382
+ <p class="panel-kicker">Companion tool</p>
383
+ <h2 id="kappa-title">Inter-rater agreement (Cohen's κ)</h2>
384
+ <p class="help-text">
385
+ Two raters classify the same items as positive / negative. Enter the four counts to get Cohen's κ with 95% asymptotic CI adjusted for estimated rater marginals and a Landis–Koch qualitative label. Useful for imaging or pathology cases where the "test" is a human judgement.
386
+ </p>
387
+ </div>
388
+ <fieldset class="confusion-matrix kappa-matrix">
389
+ <legend>Rater 1 × Rater 2</legend>
390
+ <div class="grid-container" role="group" aria-label="Inter-rater agreement matrix">
391
+ <span class="empty" aria-hidden="true"></span>
392
+ <span class="grid-header">Rater 2 +</span>
393
+ <span class="grid-header">Rater 2 −</span>
394
+ <label class="grid-label" for="kappaBothPos">Rater 1 +</label>
395
+ <div class="grid-item">
396
+ <input type="number" id="kappaBothPos" inputmode="numeric" min="0" step="1" placeholder="Both +" aria-label="Both raters positive" />
397
+ </div>
398
+ <div class="grid-item">
399
+ <input type="number" id="kappaOnly1Pos" inputmode="numeric" min="0" step="1" placeholder="Only rater 1 +" aria-label="Only rater 1 positive" />
400
+ </div>
401
+ <label class="grid-label" for="kappaOnly2Pos">Rater 1 −</label>
402
+ <div class="grid-item">
403
+ <input type="number" id="kappaOnly2Pos" inputmode="numeric" min="0" step="1" placeholder="Only rater 2 +" aria-label="Only rater 2 positive" />
404
+ </div>
405
+ <div class="grid-item">
406
+ <input type="number" id="kappaBothNeg" inputmode="numeric" min="0" step="1" placeholder="Both −" aria-label="Both raters negative" />
407
+ </div>
408
+ </div>
409
+ </fieldset>
410
+ <div class="form-actions">
411
+ <button type="button" id="kappaCalc">Compute κ</button>
412
+ <button type="button" id="kappaClear" class="secondary">Clear</button>
413
+ </div>
414
+ <div id="kappaResult" class="kappa-result" aria-live="polite"></div>
415
+ </section>
168
416
  <section aria-labelledby="interpretation-title" class="interpretation panel panel-docs">
169
417
  <h2 id="interpretation-title">Interpretation guide</h2>
170
418
  <details>
171
- <summary>Sensitivity and specificity</summary>
419
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>Sensitivity and specificity</summary>
172
420
  <p>
173
421
  Quantify the test's ability to correctly identify people with and without the condition. Useful for understanding the intrinsic performance of the test. Learn more at <a href="https://en.wikipedia.org/wiki/Sensitivity_and_specificity" target="_blank" rel="noopener noreferrer">Wikipedia</a>.
174
422
  </p>
175
423
  </details>
176
424
  <details>
177
- <summary>Predictive values</summary>
425
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>Predictive values</summary>
178
426
  <p>
179
427
  Depend on prevalence (pre-test probability). Use them to estimate the probability that a person has or does not have the disease after a positive or negative result. See <a href="https://en.wikipedia.org/wiki/Positive_and_negative_predictive_values" target="_blank" rel="noopener noreferrer">Wikipedia</a> for further reading.
180
428
  </p>
181
429
  </details>
182
430
  <details>
183
- <summary>Likelihood ratios</summary>
431
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>Likelihood ratios</summary>
184
432
  <p>
185
433
  Relate the probability of a result in diseased vs. non-diseased individuals and can be applied directly in Bayes' formula to update the probability of disease. Further reference at <a href="https://en.wikipedia.org/wiki/Likelihood_ratio" target="_blank" rel="noopener noreferrer">Wikipedia</a>.
186
434
  </p>
187
435
  </details>
188
436
  <details>
189
- <summary>Post-test probabilities</summary>
437
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>Post-test probabilities</summary>
190
438
  <p>
191
439
  Result from applying the LR to pre-test odds. They are crucial for clinical decisions such as initiating treatment or ordering additional tests. See also <a href="https://en.wikipedia.org/wiki/Posterior_probability" target="_blank" rel="noopener noreferrer">Wikipedia</a>.
192
440
  </p>
193
441
  </details>
194
442
  </section>
195
443
 
444
+ <section aria-labelledby="stard-title" class="panel panel-docs stard-panel">
445
+ <div class="panel-heading">
446
+ <p class="panel-kicker">Critical appraisal</p>
447
+ <h2 id="stard-title">STARD essentials</h2>
448
+ <p class="help-text">
449
+ Numbers from a 2×2 only mean what the underlying study allows them to mean. The <a href="https://www.equator-network.org/reporting-guidelines/stard/" target="_blank" rel="noopener noreferrer">STARD 2015 checklist</a> has 30 items; six of them dominate the interpretation of sens / spec / PPV / NPV.
450
+ </p>
451
+ </div>
452
+ <details>
453
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>1. Reference standard — what counted as "truth"?</summary>
454
+ <p>If the gold standard is imperfect (e.g. expert consensus instead of biopsy), sensitivity and specificity are biased. Worse: if the reference standard incorporates the index test result, agreement is inflated (<em>incorporation bias</em>). Ask which standard was used and whether the index test fed into it.</p>
455
+ </details>
456
+ <details>
457
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>2. Spectrum of patients — who got tested?</summary>
458
+ <p>Performance estimated in obvious cases ("sick" vs healthy volunteers) almost always overestimates real-world accuracy. Check whether enrolment was <em>consecutive</em> or <em>random</em>, and whether it spanned the clinical spectrum you'll apply the test to (mild, atypical, comorbid).</p>
459
+ </details>
460
+ <details>
461
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>3. Verification — was the reference standard applied to everyone?</summary>
462
+ <p><em>Partial verification bias</em> arises when positive-index-test patients are more likely to receive the gold standard than negative ones — inflates sensitivity, deflates specificity. Look for differential or post-hoc verification protocols.</p>
463
+ </details>
464
+ <details>
465
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>4. Blinding — were readers independent?</summary>
466
+ <p>If the person interpreting the index test also knew the reference result (or vice versa), expect <em>review bias</em>: estimates drift toward perfect agreement. Imaging and pathology studies should explicitly report blinding between readers and from clinical context.</p>
467
+ </details>
468
+ <details>
469
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>5. Indeterminate results — where did the "?"s go?</summary>
470
+ <p>If indeterminate, missing, or excluded results were silently dropped, the 2×2 misrepresents real practice. Ask how many results fell outside the binary scheme and how they were handled (re-tested, excluded, counted as negative…).</p>
471
+ </details>
472
+ <details>
473
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>6. Subgroups — does one number describe everyone?</summary>
474
+ <p>Sensitivity / specificity often vary by age, severity, prior testing, or setting. A single 2×2 hides those differences. Check whether the authors pre-specified clinically meaningful subgroups and reported them separately.</p>
475
+ </details>
476
+ </section>
196
477
  <section aria-labelledby="references-title" class="references panel panel-docs">
197
478
  <h2 id="references-title">Resources and references</h2>
198
479
  <details>
199
- <summary>Supporting literature</summary>
480
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>Supporting literature</summary>
200
481
  <ul class="reference-list">
201
482
  <li>
202
483
  <strong>Akobeng AK.</strong> Understanding diagnostic tests 1-3: sensitivity, specificity, and predictive values.
@@ -216,7 +497,7 @@
216
497
  </ul>
217
498
  </details>
218
499
  <details>
219
- <summary>Online resources</summary>
500
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>Online resources</summary>
220
501
  <ul class="reference-list">
221
502
  <li>
222
503
  <strong>The Centre for Evidence-Based Medicine (CEBM):</strong>
@@ -234,7 +515,7 @@
234
515
  </ul>
235
516
  </details>
236
517
  <details>
237
- <summary>Complementary calculators</summary>
518
+ <summary><span class="summary-tag summary-tag--ref" data-i18n="panel.tag.ref">Reference</span>Complementary calculators</summary>
238
519
  <ul class="reference-list">
239
520
  <li>
240
521
  <strong>MDCalc:</strong>
@@ -247,6 +528,29 @@
247
528
  </ul>
248
529
  </details>
249
530
  </section>
531
+ <section aria-labelledby="about-title" class="panel panel-docs about-panel">
532
+ <div class="panel-heading">
533
+ <p class="panel-kicker" data-i18n="intro.tag">Educational tool</p>
534
+ <h2 id="about-title" data-i18n="about.h">About DIAGCALC</h2>
535
+ <p data-i18n="intro.lead">
536
+ Interactive calculator for exploring diagnostic test performance measures and applying Bayesian reasoning in clinical practice.
537
+ </p>
538
+ </div>
539
+ <div class="about-grid">
540
+ <article class="about-card">
541
+ <h3 data-i18n="intro.highlight1.h">Key measures</h3>
542
+ <p data-i18n="intro.highlight1.p">Sensitivity, specificity, PPV, NPV, and likelihood ratios with 95% CI.</p>
543
+ </article>
544
+ <article class="about-card">
545
+ <h3 data-i18n="intro.highlight2.h">Post-test probability</h3>
546
+ <p data-i18n="intro.highlight2.p">Convert pre-test probability into positive and negative post-test probability.</p>
547
+ </article>
548
+ <article class="about-card">
549
+ <h3 data-i18n="intro.highlight3.h">Guided interpretation</h3>
550
+ <p data-i18n="intro.highlight3.p">Results explained to support self-directed learning.</p>
551
+ </article>
552
+ </div>
553
+ </section>
250
554
  </div>
251
555
  </main>
252
556
  <footer>
@@ -256,12 +560,23 @@
256
560
  <p>
257
561
  Any analyses, results and conclusions may not be used, interpreted or generalised beyond this specific context.
258
562
  </p>
563
+ <p class="privacy-note">
564
+ 🔒 All calculations run locally in your browser. No inputs, datasets, or results leave this device.
565
+ </p>
259
566
  <p>
260
567
  &copy; 2025 — MIT Licence — Developed by <a href="https://tiagojct.eu">Tiago Jacinto</a>. <a href="mailto:tiagojacinto@med.up.pt">Contact</a>
261
568
  </p>
262
569
  </footer>
570
+ <script src="lib/diagcalc-i18n.js"></script>
263
571
  <script src="lib/diagcalc-datasets.js"></script>
572
+ <script src="lib/diagcalc-presentation.js"></script>
264
573
  <script src="lib/diagcalc-core.js"></script>
574
+ <script src="lib/diagcalc-meta.js"></script>
575
+ <script src="lib/diagcalc-case.js"></script>
576
+ <script src="lib/diagcalc-storage.js"></script>
577
+ <script src="lib/diagcalc-geometry.js"></script>
578
+ <script src="web/results.js"></script>
579
+ <script src="web/charts.js"></script>
265
580
  <script src="script.js"></script>
266
581
  </body>
267
582
  </html>