diagcalc 3.2.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/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/diagcalc.js +224 -0
- package/index.html +266 -0
- package/lib/diagcalc-core.js +305 -0
- package/lib/diagcalc-datasets.js +174 -0
- package/package.json +40 -0
- package/script.js +497 -0
- package/styles.css +789 -0
- package/tui/index.js +988 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tiago Jacinto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# DIAGCALC
|
|
2
|
+
|
|
3
|
+
DIAGCALC is a diagnostic test calculator with two interfaces:
|
|
4
|
+
|
|
5
|
+
- a web app for teaching and interactive use
|
|
6
|
+
- a terminal app with both TUI and plain CLI modes
|
|
7
|
+
|
|
8
|
+
Both interfaces use the same calculation engine.
|
|
9
|
+
|
|
10
|
+
## Repository
|
|
11
|
+
|
|
12
|
+
- GitHub: `https://github.com/tiagojct/diagcalc`
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- confusion matrix input: TP, FP, FN, TN
|
|
17
|
+
- pre-test probability input
|
|
18
|
+
- sensitivity, specificity, PPV, NPV
|
|
19
|
+
- LR+ and LR-
|
|
20
|
+
- positive and negative post-test probability
|
|
21
|
+
- 95% confidence intervals with the Wilson method
|
|
22
|
+
- preset study scenarios
|
|
23
|
+
- Fagan nomogram in the web app
|
|
24
|
+
- TUI, text CLI, and JSON CLI output in the terminal app
|
|
25
|
+
|
|
26
|
+
## Web App
|
|
27
|
+
|
|
28
|
+
The web app is static. It does not need a build step.
|
|
29
|
+
|
|
30
|
+
### Run locally
|
|
31
|
+
|
|
32
|
+
Open `index.html` directly in a browser, or serve the folder locally:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python3 -m http.server 8080
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then open:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
http://localhost:8080
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Use
|
|
45
|
+
|
|
46
|
+
1. Load a preset scenario, or leave the selector empty.
|
|
47
|
+
2. Enter TP, FP, FN, TN.
|
|
48
|
+
3. Enter pre-test probability.
|
|
49
|
+
4. Click `Calculate results`.
|
|
50
|
+
5. Review the probability bars, result cards, and Fagan nomogram.
|
|
51
|
+
|
|
52
|
+
## Terminal App
|
|
53
|
+
|
|
54
|
+
The terminal app requires Node.js 18 or newer.
|
|
55
|
+
|
|
56
|
+
### Install locally
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### TUI mode
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
node bin/diagcalc.js --tui
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If you want the short command:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm link
|
|
72
|
+
diag --tui
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### CLI mode
|
|
76
|
+
|
|
77
|
+
List datasets:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
diag --list-datasets
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run a preset case:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
diag --dataset hiv_elisa
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run an ad hoc case:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
diag --tp 42 --fp 8 --fn 3 --tn 120 --pre 15
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Get JSON output:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
diag --dataset ddimer --format json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### TUI controls
|
|
102
|
+
|
|
103
|
+
- `Tab` / `Ctrl-N`: next panel
|
|
104
|
+
- `Shift-Tab` / `Ctrl-P`: previous panel
|
|
105
|
+
- arrows: move selection
|
|
106
|
+
- type digits directly in the input editor
|
|
107
|
+
- `Backspace`: delete one character from the selected field
|
|
108
|
+
- `Delete` or `Ctrl-U`: clear the selected field
|
|
109
|
+
- `Enter`: open the selected field in prompt mode
|
|
110
|
+
- `n`: start a blank ad hoc case
|
|
111
|
+
- `x`: export current case to plain text
|
|
112
|
+
- `m`: export current case to Markdown
|
|
113
|
+
- `r`: reset current case
|
|
114
|
+
- `q`: quit
|
|
115
|
+
|
|
116
|
+
## Deployment
|
|
117
|
+
|
|
118
|
+
### GitHub Pages
|
|
119
|
+
|
|
120
|
+
This repository includes a GitHub Pages workflow at `.github/workflows/deploy-pages.yml`.
|
|
121
|
+
|
|
122
|
+
To publish the web app:
|
|
123
|
+
|
|
124
|
+
1. Push to the `main` branch.
|
|
125
|
+
2. In GitHub, open `Settings -> Pages`.
|
|
126
|
+
3. Set the source to `GitHub Actions`.
|
|
127
|
+
4. The workflow will publish the static site automatically.
|
|
128
|
+
|
|
129
|
+
### npm
|
|
130
|
+
|
|
131
|
+
This package is ready for npm publishing.
|
|
132
|
+
|
|
133
|
+
Publish steps:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm login
|
|
137
|
+
npm publish --access public
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
After publishing, users can install it with:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm install -g diagcalc
|
|
144
|
+
diag --tui
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Project Structure
|
|
148
|
+
|
|
149
|
+
- `index.html` - web app markup
|
|
150
|
+
- `styles.css` - web app styles
|
|
151
|
+
- `script.js` - web app logic and Fagan nomogram rendering
|
|
152
|
+
- `lib/diagcalc-core.js` - shared calculations and validation
|
|
153
|
+
- `lib/diagcalc-datasets.js` - shared preset datasets
|
|
154
|
+
- `tui/index.js` - terminal UI
|
|
155
|
+
- `bin/diagcalc.js` - CLI and TUI entrypoint
|
|
156
|
+
- `.github/workflows/deploy-pages.yml` - GitHub Pages deployment workflow
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
package/bin/diagcalc.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const core = require("../lib/diagcalc-core");
|
|
4
|
+
const datasetStore = require("../lib/diagcalc-datasets");
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const args = {
|
|
8
|
+
raw: [],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const token = argv[index];
|
|
13
|
+
if (!token.startsWith("--")) {
|
|
14
|
+
args.raw.push(token);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const key = token.slice(2);
|
|
19
|
+
const next = argv[index + 1];
|
|
20
|
+
const isFlag = !next || next.startsWith("--");
|
|
21
|
+
|
|
22
|
+
if (isFlag) {
|
|
23
|
+
args[key] = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
args[key] = next;
|
|
28
|
+
index += 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printHelp() {
|
|
35
|
+
process.stdout.write([
|
|
36
|
+
"DIAGCALC terminal tools",
|
|
37
|
+
"",
|
|
38
|
+
"Usage:",
|
|
39
|
+
" diag --tui",
|
|
40
|
+
" diag --dataset hiv_elisa",
|
|
41
|
+
" diag --tp 199 --fp 1 --fn 1 --tn 9799 --pre 2",
|
|
42
|
+
" diag --dataset ddimer --pre 18",
|
|
43
|
+
" diag --list-datasets",
|
|
44
|
+
"",
|
|
45
|
+
"Options:",
|
|
46
|
+
" --tui Launch the interactive terminal UI",
|
|
47
|
+
" --dataset <key> Load a predefined scenario",
|
|
48
|
+
" --tp <n> True positives",
|
|
49
|
+
" --fp <n> False positives",
|
|
50
|
+
" --fn <n> False negatives",
|
|
51
|
+
" --tn <n> True negatives",
|
|
52
|
+
" --pre <n> Pre-test probability (%)",
|
|
53
|
+
" --format <type> Output format: text or json",
|
|
54
|
+
" --list-datasets Show available dataset keys",
|
|
55
|
+
" --help Show this help message",
|
|
56
|
+
"",
|
|
57
|
+
"Tip:",
|
|
58
|
+
" Run `npm link` in this repo to use `diag` globally.",
|
|
59
|
+
"",
|
|
60
|
+
].join("\n"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function serialiseMetric(metric) {
|
|
64
|
+
return {
|
|
65
|
+
label: metric.label,
|
|
66
|
+
value: metric.value,
|
|
67
|
+
formatted: core.formatValue(metric.value, metric.formatter),
|
|
68
|
+
ci: metric.ci || null,
|
|
69
|
+
note: metric.note || null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printJsonReport(dataset, input, metrics) {
|
|
74
|
+
const payload = {
|
|
75
|
+
case: dataset ? dataset.name : "Ad hoc case",
|
|
76
|
+
datasetKey: dataset ? dataset.key || null : null,
|
|
77
|
+
input,
|
|
78
|
+
metrics: Object.fromEntries(Object.entries(metrics).map(([key, metric]) => [key, serialiseMetric(metric)])),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printDatasets() {
|
|
85
|
+
const rows = datasetStore.listDatasets().map((dataset) => `${dataset.key.padEnd(16)} ${dataset.name}`);
|
|
86
|
+
process.stdout.write(`Available datasets\n\n${rows.join("\n")}\n`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildInputFromArgs(args) {
|
|
90
|
+
const dataset = args.dataset ? datasetStore.getDataset(args.dataset) : null;
|
|
91
|
+
if (args.dataset && !dataset) {
|
|
92
|
+
return {
|
|
93
|
+
error: `Unknown dataset: ${args.dataset}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const merged = {
|
|
98
|
+
tp: dataset ? dataset.tp : NaN,
|
|
99
|
+
fp: dataset ? dataset.fp : NaN,
|
|
100
|
+
fn: dataset ? dataset.fn : NaN,
|
|
101
|
+
tn: dataset ? dataset.tn : NaN,
|
|
102
|
+
preTestProb: dataset ? dataset.preTestProb : NaN,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (typeof args.tp === "string") {
|
|
106
|
+
merged.tp = core.safeParseInt(args.tp);
|
|
107
|
+
}
|
|
108
|
+
if (typeof args.fp === "string") {
|
|
109
|
+
merged.fp = core.safeParseInt(args.fp);
|
|
110
|
+
}
|
|
111
|
+
if (typeof args.fn === "string") {
|
|
112
|
+
merged.fn = core.safeParseInt(args.fn);
|
|
113
|
+
}
|
|
114
|
+
if (typeof args.tn === "string") {
|
|
115
|
+
merged.tn = core.safeParseInt(args.tn);
|
|
116
|
+
}
|
|
117
|
+
if (typeof args.pre === "string") {
|
|
118
|
+
const normalised = core.normaliseDecimal(args.pre);
|
|
119
|
+
merged.preTestProb = normalised === "" ? NaN : parseFloat(normalised);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
dataset: dataset ? { key: args.dataset, ...dataset } : null,
|
|
124
|
+
input: merged,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderMetricLine(metric) {
|
|
129
|
+
const value = core.formatValue(metric.value, metric.formatter);
|
|
130
|
+
const ci = metric.ci
|
|
131
|
+
? ` | 95% CI ${core.formatPercentage(metric.ci.lower)} to ${core.formatPercentage(metric.ci.upper)}`
|
|
132
|
+
: "";
|
|
133
|
+
return `${metric.label}: ${value}${ci}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function printReport(dataset, metrics) {
|
|
137
|
+
const entries = [
|
|
138
|
+
metrics.sensitivity,
|
|
139
|
+
metrics.specificity,
|
|
140
|
+
metrics.ppv,
|
|
141
|
+
metrics.npv,
|
|
142
|
+
metrics.lrPositive,
|
|
143
|
+
metrics.lrNegative,
|
|
144
|
+
metrics.preTestProbability,
|
|
145
|
+
metrics.postTestPositive,
|
|
146
|
+
metrics.postTestNegative,
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const lines = [];
|
|
150
|
+
lines.push("DIAGCALC");
|
|
151
|
+
if (dataset) {
|
|
152
|
+
lines.push(dataset.name);
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
155
|
+
entries.forEach((metric) => {
|
|
156
|
+
lines.push(renderMetricLine(metric));
|
|
157
|
+
});
|
|
158
|
+
lines.push("");
|
|
159
|
+
lines.push(`Pre-test bar [${core.buildProbabilityBar(metrics.preTestProbability.value, 24)}] ${core.formatPercentage(metrics.preTestProbability.value)}`);
|
|
160
|
+
lines.push(`Post-test (+) bar [${core.buildProbabilityBar(metrics.postTestPositive.value, 24)}] ${core.formatPercentage(metrics.postTestPositive.value)}`);
|
|
161
|
+
lines.push(`Post-test (-) bar [${core.buildProbabilityBar(metrics.postTestNegative.value, 24)}] ${core.formatPercentage(metrics.postTestNegative.value)}`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push(`Interpretation LR+: ${metrics.lrPositive.note}`);
|
|
164
|
+
lines.push(`Interpretation LR-: ${metrics.lrNegative.note}`);
|
|
165
|
+
lines.push("");
|
|
166
|
+
|
|
167
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function main() {
|
|
171
|
+
const args = parseArgs(process.argv.slice(2));
|
|
172
|
+
|
|
173
|
+
if (args.help) {
|
|
174
|
+
printHelp();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (args["list-datasets"]) {
|
|
179
|
+
printDatasets();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (args.tui || (process.argv.length === 2 && process.stdout.isTTY && process.stdin.isTTY)) {
|
|
184
|
+
const { runTui } = require("../tui/index");
|
|
185
|
+
runTui();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (process.argv.length === 2) {
|
|
190
|
+
printHelp();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const built = buildInputFromArgs(args);
|
|
195
|
+
if (built.error) {
|
|
196
|
+
process.stderr.write(`${built.error}\n`);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const validation = core.validateInputs(built.input);
|
|
202
|
+
if (!validation.valid) {
|
|
203
|
+
process.stderr.write(`${validation.message}\n`);
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const metrics = core.calculateMetrics(built.input);
|
|
209
|
+
const format = typeof args.format === "string" ? args.format.toLowerCase() : "text";
|
|
210
|
+
if (format === "json") {
|
|
211
|
+
printJsonReport(built.dataset, built.input, metrics);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (format !== "text") {
|
|
216
|
+
process.stderr.write("Unsupported format. Use --format text or --format json.\n");
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
printReport(built.dataset, metrics);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main();
|
package/index.html
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>DIAG-CALC</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
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" />
|
|
10
|
+
<link rel="stylesheet" href="styles.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<header class="intro" role="banner">
|
|
14
|
+
<div class="container">
|
|
15
|
+
<div class="header-controls">
|
|
16
|
+
<button id="themeToggle" class="theme-toggle" aria-label="Toggle dark mode" type="button">
|
|
17
|
+
<span class="theme-icon">◐</span>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
<p class="tag">Educational tool</p>
|
|
21
|
+
<h1>DIAGCALC</h1>
|
|
22
|
+
<p class="lead">
|
|
23
|
+
Interactive calculator for exploring diagnostic test performance measures
|
|
24
|
+
and applying Bayesian reasoning in clinical practice.
|
|
25
|
+
</p>
|
|
26
|
+
<div class="intro-highlights" role="list">
|
|
27
|
+
<article class="highlight" role="listitem">
|
|
28
|
+
<h2>Key measures</h2>
|
|
29
|
+
<p>Sensitivity, specificity, PPV, NPV, and likelihood ratios with 95% CI.</p>
|
|
30
|
+
</article>
|
|
31
|
+
<article class="highlight" role="listitem">
|
|
32
|
+
<h2>Post-test probability</h2>
|
|
33
|
+
<p>Convert pre-test probability into positive and negative post-test probability.</p>
|
|
34
|
+
</article>
|
|
35
|
+
<article class="highlight" role="listitem">
|
|
36
|
+
<h2>Guided interpretation</h2>
|
|
37
|
+
<p>Results explained to support self-directed learning.</p>
|
|
38
|
+
</article>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</header>
|
|
42
|
+
<main class="container" id="main" role="main">
|
|
43
|
+
<div class="workspace">
|
|
44
|
+
<section aria-labelledby="library-title" class="panel panel-library">
|
|
45
|
+
<div class="panel-heading">
|
|
46
|
+
<p class="panel-kicker">Library</p>
|
|
47
|
+
<h2 id="library-title">Load a study scenario</h2>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<label class="full" for="datasetSelect">Scenario</label>
|
|
51
|
+
<select id="datasetSelect" class="full" aria-describedby="dataset-help">
|
|
52
|
+
<option value="">— Select —</option>
|
|
53
|
+
<optgroup label="Generic scenarios">
|
|
54
|
+
<option value="screening">Screening programme (low prevalence)</option>
|
|
55
|
+
<option value="caseControl">Case-control study</option>
|
|
56
|
+
<option value="clinic">Specialist clinic</option>
|
|
57
|
+
</optgroup>
|
|
58
|
+
<optgroup label="Medical literature cases">
|
|
59
|
+
<option value="ddimer">D-dimer for pulmonary embolism</option>
|
|
60
|
+
<option value="troponin">High-sensitivity troponin for AMI</option>
|
|
61
|
+
<option value="mammography">Screening mammography</option>
|
|
62
|
+
<option value="covid_antigen">Rapid COVID-19 antigen test</option>
|
|
63
|
+
<option value="hiv_elisa">HIV ELISA</option>
|
|
64
|
+
<option value="strep_throat">Rapid Streptococcus test</option>
|
|
65
|
+
<option value="xray_pneumonia">Chest X-ray for pneumonia</option>
|
|
66
|
+
</optgroup>
|
|
67
|
+
</select>
|
|
68
|
+
<p id="dataset-help" class="help-text">
|
|
69
|
+
Values are filled in automatically and can be adjusted afterwards.
|
|
70
|
+
</p>
|
|
71
|
+
<div id="dataset-reference" class="dataset-reference" style="display: none;"></div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
|
|
75
|
+
<section aria-labelledby="form-title" class="panel panel-inputs">
|
|
76
|
+
<div class="panel-heading">
|
|
77
|
+
<p class="panel-kicker">Editor</p>
|
|
78
|
+
<h2 id="form-title">Build the confusion matrix</h2>
|
|
79
|
+
<p>
|
|
80
|
+
Use non-negative integers and a pre-test probability under 100%.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
<form id="inputForm" novalidate>
|
|
84
|
+
<fieldset class="confusion-matrix">
|
|
85
|
+
<legend>Confusion matrix</legend>
|
|
86
|
+
<div class="grid-container" role="group" aria-label="Diagnostic test results">
|
|
87
|
+
<span class="empty" aria-hidden="true"></span>
|
|
88
|
+
<span class="grid-header grid-header-pos">Disease +</span>
|
|
89
|
+
<span class="grid-header grid-header-neg">Disease -</span>
|
|
90
|
+
<label class="grid-label grid-label-pos" for="tp">Test +</label>
|
|
91
|
+
<div class="grid-item">
|
|
92
|
+
<input type="number" id="tp" inputmode="numeric" min="0" step="1"
|
|
93
|
+
placeholder="True positives"
|
|
94
|
+
aria-label="True positives (TP): disease present, test positive"
|
|
95
|
+
required />
|
|
96
|
+
</div>
|
|
97
|
+
<div class="grid-item">
|
|
98
|
+
<input type="number" id="fp" inputmode="numeric" min="0" step="1"
|
|
99
|
+
placeholder="False positives"
|
|
100
|
+
aria-label="False positives (FP): disease absent, test positive"
|
|
101
|
+
required />
|
|
102
|
+
</div>
|
|
103
|
+
<label class="grid-label grid-label-neg" for="fn">Test -</label>
|
|
104
|
+
<div class="grid-item">
|
|
105
|
+
<input type="number" id="fn" inputmode="numeric" min="0" step="1"
|
|
106
|
+
placeholder="False negatives"
|
|
107
|
+
aria-label="False negatives (FN): disease present, test negative"
|
|
108
|
+
required />
|
|
109
|
+
</div>
|
|
110
|
+
<div class="grid-item">
|
|
111
|
+
<input type="number" id="tn" inputmode="numeric" min="0" step="1"
|
|
112
|
+
placeholder="True negatives"
|
|
113
|
+
aria-label="True negatives (TN): disease absent, test negative"
|
|
114
|
+
required />
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</fieldset>
|
|
118
|
+
<div class="form-row">
|
|
119
|
+
<label for="preTestProb">Pre-test probability (%)</label>
|
|
120
|
+
<input
|
|
121
|
+
class="preTestProb"
|
|
122
|
+
type="number"
|
|
123
|
+
id="preTestProb"
|
|
124
|
+
inputmode="decimal"
|
|
125
|
+
min="0"
|
|
126
|
+
max="100"
|
|
127
|
+
step="0.1"
|
|
128
|
+
placeholder="e.g. 12.5"
|
|
129
|
+
required
|
|
130
|
+
/>
|
|
131
|
+
<p class="help-text">
|
|
132
|
+
Suggestion: estimate the disease prevalence in the study population or the clinical probability before the test.
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="form-actions" role="group">
|
|
136
|
+
<button type="submit">Calculate results</button>
|
|
137
|
+
<button type="reset" id="resetButton" class="secondary">Clear fields</button>
|
|
138
|
+
</div>
|
|
139
|
+
<p id="feedback" class="feedback" role="alert" aria-live="polite"></p>
|
|
140
|
+
</form>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<section aria-labelledby="results-title" class="panel panel-results results-section">
|
|
144
|
+
<div class="panel-heading panel-heading-inline">
|
|
145
|
+
<div>
|
|
146
|
+
<p class="panel-kicker">Output</p>
|
|
147
|
+
<h2 id="results-title">Results</h2>
|
|
148
|
+
<p class="help-text">
|
|
149
|
+
Values are shown with 95% confidence intervals calculated using the Wilson method.
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="results-actions" role="group">
|
|
153
|
+
<button type="button" id="printButton" class="secondary">Print report</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div id="probabilityChart" class="probability-chart" aria-label="Comparison of pre- and post-test probabilities"></div>
|
|
157
|
+
<div id="faganNomogram" class="fagan-nomogram" style="display: none;">
|
|
158
|
+
<h3>Fagan Nomogram</h3>
|
|
159
|
+
<p class="help-text">Graphical representation of the relationship between pre-test probability, likelihood ratio, and post-test probability.</p>
|
|
160
|
+
<canvas id="faganCanvas" width="600" height="400" aria-label="Fagan nomogram"></canvas>
|
|
161
|
+
</div>
|
|
162
|
+
<div id="results" class="results-grid" aria-live="polite"></div>
|
|
163
|
+
</section>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="resource-grid">
|
|
167
|
+
<section aria-labelledby="interpretation-title" class="interpretation panel panel-docs">
|
|
168
|
+
<h2 id="interpretation-title">Interpretation guide</h2>
|
|
169
|
+
<details>
|
|
170
|
+
<summary>Sensitivity and specificity</summary>
|
|
171
|
+
<p>
|
|
172
|
+
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>.
|
|
173
|
+
</p>
|
|
174
|
+
</details>
|
|
175
|
+
<details>
|
|
176
|
+
<summary>Predictive values</summary>
|
|
177
|
+
<p>
|
|
178
|
+
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.
|
|
179
|
+
</p>
|
|
180
|
+
</details>
|
|
181
|
+
<details>
|
|
182
|
+
<summary>Likelihood ratios</summary>
|
|
183
|
+
<p>
|
|
184
|
+
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>.
|
|
185
|
+
</p>
|
|
186
|
+
</details>
|
|
187
|
+
<details>
|
|
188
|
+
<summary>Post-test probabilities</summary>
|
|
189
|
+
<p>
|
|
190
|
+
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>.
|
|
191
|
+
</p>
|
|
192
|
+
</details>
|
|
193
|
+
</section>
|
|
194
|
+
|
|
195
|
+
<section aria-labelledby="references-title" class="references panel panel-docs">
|
|
196
|
+
<h2 id="references-title">Resources and references</h2>
|
|
197
|
+
<details>
|
|
198
|
+
<summary>Supporting literature</summary>
|
|
199
|
+
<ul class="reference-list">
|
|
200
|
+
<li>
|
|
201
|
+
<strong>Akobeng AK.</strong> Understanding diagnostic tests 1-3: sensitivity, specificity, and predictive values.
|
|
202
|
+
<em>Acta Paediatr.</em> 2007;96(3):338-341.
|
|
203
|
+
<a href="https://doi.org/10.1111/j.1651-2227.2006.00180.x" target="_blank" rel="noopener noreferrer">DOI: 10.1111/j.1651-2227.2006.00180.x</a>
|
|
204
|
+
</li>
|
|
205
|
+
<li>
|
|
206
|
+
<strong>McGee S.</strong> Simplifying likelihood ratios.
|
|
207
|
+
<em>J Gen Intern Med.</em> 2002;17(8):646-649.
|
|
208
|
+
<a href="https://doi.org/10.1046/j.1525-1497.2002.10750.x" target="_blank" rel="noopener noreferrer">DOI: 10.1046/j.1525-1497.2002.10750.x</a>
|
|
209
|
+
</li>
|
|
210
|
+
<li>
|
|
211
|
+
<strong>Fagan TJ.</strong> Nomogram for Bayes theorem.
|
|
212
|
+
<em>N Engl J Med.</em> 1975;293(5):257.
|
|
213
|
+
<a href="https://doi.org/10.1056/NEJM197507312930513" target="_blank" rel="noopener noreferrer">DOI: 10.1056/NEJM197507312930513</a>
|
|
214
|
+
</li>
|
|
215
|
+
</ul>
|
|
216
|
+
</details>
|
|
217
|
+
<details>
|
|
218
|
+
<summary>Online resources</summary>
|
|
219
|
+
<ul class="reference-list">
|
|
220
|
+
<li>
|
|
221
|
+
<strong>The Centre for Evidence-Based Medicine (CEBM):</strong>
|
|
222
|
+
<a href="https://www.cebm.ox.ac.uk/resources/ebm-tools" target="_blank" rel="noopener noreferrer">EBM Tools and Resources</a>
|
|
223
|
+
</li>
|
|
224
|
+
<li>
|
|
225
|
+
<strong>StatPearls:</strong>
|
|
226
|
+
<a href="https://www.ncbi.nlm.nih.gov/books/NBK557491/" target="_blank" rel="noopener noreferrer">Sensitivity and Specificity</a>
|
|
227
|
+
</li>
|
|
228
|
+
<li>
|
|
229
|
+
<strong>Deeks JJ, Altman DG.</strong> Diagnostic tests 4: likelihood ratios.
|
|
230
|
+
<em>BMJ.</em> 2004;329(7458):168-169.
|
|
231
|
+
<a href="https://www.bmj.com/content/329/7458/168" target="_blank" rel="noopener noreferrer">Open access</a>
|
|
232
|
+
</li>
|
|
233
|
+
</ul>
|
|
234
|
+
</details>
|
|
235
|
+
<details>
|
|
236
|
+
<summary>Complementary calculators</summary>
|
|
237
|
+
<ul class="reference-list">
|
|
238
|
+
<li>
|
|
239
|
+
<strong>MDCalc:</strong>
|
|
240
|
+
<a href="https://www.mdcalc.com/" target="_blank" rel="noopener noreferrer">Medical calculators</a>
|
|
241
|
+
</li>
|
|
242
|
+
<li>
|
|
243
|
+
<strong>ClinCalc:</strong>
|
|
244
|
+
<a href="https://clincalc.com/Stats/SampleSize.aspx" target="_blank" rel="noopener noreferrer">Diagnostic Test Calculator</a>
|
|
245
|
+
</li>
|
|
246
|
+
</ul>
|
|
247
|
+
</details>
|
|
248
|
+
</section>
|
|
249
|
+
</div>
|
|
250
|
+
</main>
|
|
251
|
+
<footer>
|
|
252
|
+
<p>
|
|
253
|
+
DIAGCALC is a diagnostic test performance calculator created for educational purposes and training of healthcare students.
|
|
254
|
+
</p>
|
|
255
|
+
<p>
|
|
256
|
+
Any analyses, results and conclusions may not be used, interpreted or generalised beyond this specific context.
|
|
257
|
+
</p>
|
|
258
|
+
<p>
|
|
259
|
+
© 2025 — MIT Licence — Developed by <a href="https://tiagojct.eu">Tiago Jacinto</a>. <a href="mailto:tiagojacinto@med.up.pt">Contact</a>
|
|
260
|
+
</p>
|
|
261
|
+
</footer>
|
|
262
|
+
<script src="lib/diagcalc-datasets.js"></script>
|
|
263
|
+
<script src="lib/diagcalc-core.js"></script>
|
|
264
|
+
<script src="script.js"></script>
|
|
265
|
+
</body>
|
|
266
|
+
</html>
|