PageSourceSearch

https://m-colley.github.io/dagitty/js/local-tests.js?v=2026-09-22.3

js m-colley.github.io collected 2026-10-03 09:33:08 UTC 25,430 bytes, 490 lines download raw bytes

1/* DAGitty — Test the implications against data
2 *
3 * The Testable-implications panel lists the conditional independencies a
4 * diagram predicts, and the methods statement tells the user to go and run
5 * localTests() in R. This closes that loop in the browser: upload the study
6 * data, match its columns to the diagram's variables, and every implied
7 * independence X ⊥ Y | Z is tested with a partial-correlation test using
8 * Fisher's z transformation — the "cis" test of dagitty::localTests(), which
9 * is also the test used by the discovery tool in causal-discovery.js.
10 *
11 * For each test the tool reports the partial correlation, its 95 % confidence
12 * interval, the p-value and a Holm-adjusted p-value, and it produces a
13 * ready-to-paste sentence for the methods statement.
14 *
15 * Limits are stated in the dialog: the test is linear and treats every
16 * variable as numeric; binary / ordinal codes are used as they are.
17 */
18
19(function () {
20    'use strict';
21
22    var Z975 = 1.959963984540054;   // Φ⁻¹(0.975)
23    var IMPLICATION_CAP = 1000;
24
25    function ids(vs) { return _.pluck(vs || [], 'id'); }
26    function esc(s) {
27        return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
28    }
29
30    // ── Statistics ────────────────────────────────────────────────────────
31
32    function atanhClamped(r) {
33        r = Math.max(-0.999999, Math.min(0.999999, r));
34        return 0.5 * Math.log((1 + r) / (1 - r));
35    }
36
37    /**
38     * Fisher z test of H0: (partial) correlation = 0.
39     * @param {number} r  observed (partial) correlation
40     * @param {number} n  complete cases used
41     * @param {number} k  size of the conditioning set
42     * @returns {null|{estimate,z,se,lo,hi,p,n}}  null if n is too small
43     */
44    function fisherTest(r, n, k) {
45        var df = n - k - 3;
46        if (!(df > 0)) return null;
47        var z = atanhClamped(r), se = 1 / Math.sqrt(df);
48        var stat = z / se;
49        var p = 2 * (1 - CausalDiscovery.normalCDF(Math.abs(stat)));
50        return {
51            estimate: r, z: stat, se: se,
52            lo: Math.tanh(z - Z975 * se), hi: Math.tanh(z + Z975 * se),
53            p: Math.max(0, Math.min(1, p)), n: n
54        };
55    }
56
57    /** Holm step-down adjusted p-values (same order as the input). */
58    function holm(ps) {
59        var m = ps.length, adj = new Array(m);
60        var order = ps.map(function (p, i) { return i; }).sort(function (a, b) { return ps[a] - ps[b]; });
61        var running = 0;
62        order.forEach(function (i, rank) {
63            running = Math.max(running, Math.min(1, (m - rank) * ps[i]));
64            adj[i] = running;
65        });
66        return adj;
67    }
68
69    /**
70     * Partial correlation of columns[0] and columns[1] given the rest, from the
71     * rows complete for all of them.
72     * @returns {{r:number,n:number}|{error:string}}
73     */
74    function partialFromColumns(columns) {
75        var m = columns.length, n = columns[0].length, rows = [];
76        for (var i = 0; i < n; i++) {
77            var ok = true;
78            for (var c = 0; c < m; c++) if (!isFinite(columns[c][i])) { ok = false; break; }
79            if (ok) rows.push(i);
80        }
81        if (rows.length < m + 4) return { error: 'too few complete rows (' + rows.length + ')' };
82
83        var sub = columns.map(function (col) { return rows.map(function (i) { return col[i]; }); });
84        var C = [];
85        for (var a = 0; a < m; a++) {
86            C.push([]);
87            for (var b = 0; b < m; b++) C[a].push(a === b ? 1 : CausalDiscovery.pearsonR(sub[a], sub[b]));
88        }
89        if (m === 2) return { r: C[0][1], n: rows.length };
90        var inv = CausalDiscovery.invertMatrix(C);
91        if (!inv) return { error: 'variables are collinear' };
92        var denom = Math.sqrt(Math.abs(inv[0][0] * inv[1][1]));
93        if (denom < 1e-12) return { error: 'variables are collinear' };
94        return { r: -inv[0][1] / denom, n: rows.length };
95    }
96
97    // ── Implications of the diagram ───────────────────────────────────────
98
99    /**
100     * Flatten GraphAnalyzer.listMinimalImplications() into {x, y, z[]} records.
101     * Implications involving a selection variable are dropped: everyone in the
102     * data is selected, so it is constant and cannot be tested.
103     */
104    function implicationsOf(g) {
105        var raw = GraphAnalyzer.listMinimalImplications(g, IMPLICATION_CAP);
106        var selected = ids(g.getSelectedNodes());
107        var out = [], droppedSelection = 0, total = 0;
108        raw.forEach(function (t) {
109            t[2].forEach(function (s) {
110                total++;
111                var z = ids(s).sort();
112                var involvesSel = [t[0], t[1]].concat(z).some(function (v) { return selected.indexOf(v) !== -1; });
113                if (involvesSel) { droppedSelection++; return; }
114                out.push({ x: t[0], y: t[1], z: z });
115            });
116        });
117        return { list: out, capped: total >= IMPLICATION_CAP, droppedSelection: droppedSelection };
118    }
119
120    function implText(imp) {
121        return imp.x + ' ⊥ ' + imp.y + (imp.z.length ? ' | ' + imp.z.join(', ') : '');
122    }
123
124    // ── Matching diagram variables to data columns ─────────────────────────
125
126    function norm(s) { return String(s).toLowerCase().replace(/[^a-z0-9]/g, ''); }
127
128    /** Best-effort column for each variable: exact, then case-insensitive, then punctuation-insensitive. */
129    function autoMap(vars, headers) {
130        var map = {};
131        var lower = {}, normed = {};
132        headers.forEach(function (h) {
133            if (!(h.toLowerCase() in lower)) lower[h.toLowerCase()] = h;
134            if (!(norm(h) in normed)) normed[norm(h)] = h;
135        });
136        vars.forEach(function (v) {
137            if (headers.indexOf(v) !== -1) map[v] = v;
138            else if (v.toLowerCase() in lower) map[v] = lower[v.toLowerCase()];
139            else if (norm(v) in normed && norm(v) !== '') map[v] = normed[norm(v)];
140            else map[v] = null;
141        });
142        return map;
143    }
144
145    // ── Running the tests ─────────────────────────────────────────────────
146
147    /**
148     * @param {Graph}  g
149     * @param {{columns: Object<string,number[]>}} table  from CausalDiscovery.parseTable
150     * @param {Object<string,string|null>} mapping        variable → column
151     * @param {number} alpha
152     */
153    function run(g, table, mapping, alpha) {
154        alpha = alpha || 0.05;
155        var imps = implicationsOf(g);
156        var tests = [];
157        imps.list.forEach(function (imp) {
158            var names = [imp.x, imp.y].concat(imp.z);
159            var missing = names.filter(function (v) { return !mapping[v] || !table.columns[mapping[v]]; });
160            if (missing.length) { tests.push({ imp: imp, skipped: 'not in the data: ' + missing.join(', ') }); return; }
161            var pc = partialFromColumns(names.map(function (v) { return table.columns[mapping[v]]; }));
162            if (pc.error) { tests.push({ imp: imp, skipped: pc.error }); return; }
163            var ft = fisherTest(pc.r, pc.n, imp.z.length);
164            if (!ft) { tests.push({ imp: imp, skipped: 'too few complete rows (' + pc.n + ')' }); return; }
165            tests.push({ imp: imp, estimate: ft.estimate, lo: ft.lo, hi: ft.hi, p: ft.p, z: ft.z, n: ft.n });
166        });
167
168        var tested = tests.filter(function (t) { return !t.skipped; });
169        var adj = holm(tested.map(function (t) { return t.p; }));
170        tested.forEach(function (t, i) {
171            t.pHolm = adj[i];
172            t.violated = t.p < alpha;
173            t.violatedHolm = t.pHolm < alpha;
174        });
175        tests.sort(function (a, b) {
176            if (!!a.skipped !== !!b.skipped) return a.skipped ? 1 : -1;
177            if (a.skipped) return 0;
178            return a.p - b.p;
179        });
180
181        var ns = tested.map(function (t) { return t.n; });
182        return {
183            tests: tests,
184            summary: {
185                nImplied: imps.list.length, capped: imps.capped, droppedSelection: imps.droppedSelection,
186                tested: tested.length, skipped: tests.length - tested.length,
187                violated: tested.filter(function (t) { return t.violated; }).length,
188                violatedHolm: tested.filter(function (t) { return t.violatedHolm; }).length,
189                alpha: alpha,
190                nMin: ns.length ? Math.min.apply(null, ns) : 0,
191                nMax: ns.length ? Math.max.apply(null, ns) : 0
192            }
193        };
194    }
195
196    function formatP(p) {
197        if (p < 0.001) return 'p < .001';
198        return 'p = ' + p.toFixed(3).replace(/^0\./, '.');
199    }
200    function f2(x) { return (x >= 0 ? '' : '−') + Math.abs(x).toFixed(2); }
201
202    /** One paragraph for the methods statement. */
203    function methodsSentence(res) {
204        var s = res.summary;
205        var nText = s.nMin === s.nMax ? 'n = ' + s.nMin : 'n = ' + s.nMin + '–' + s.nMax + ' complete cases per test';
206        var out = 'We tested ' + (s.tested === s.nImplied ? 'all ' : '') + s.tested + ' of the ' +
207            (s.capped ? 'at least ' : '') + s.nImplied + ' implied conditional independencies against our data (' +
208            nText + ') using partial-correlation tests with Fisher\'s z transformation (α = ' + s.alph
208a +
209            '; equivalent to localTests(type = "cis") in the dagitty R package). ';
210        if (s.tested === 0) return out + 'No implication could be tested with the available columns.';
211        if (s.violated === 0) {
212            out += 'None was rejected, so the data are consistent with the independencies the diagram implies.';
213        } else {
214            var v = res.tests.filter(function (t) { return t.violated; });
215            var shown = v.slice(0, 5).map(function (t) {
216                return implText(t.imp) + ' (r = ' + f2(t.estimate) + ', 95% CI ' + f2(t.lo) + ' to ' + f2(t.hi) + ', ' + formatP(t.p) + ')';
217            });
218            out += s.violated + ' ' + (s.violated === 1 ? 'was' : 'were') + ' rejected: ' + shown.join('; ') +
219                (v.length > 5 ? '; and ' + (v.length - 5) + ' more' : '') + '. After Holm correction for multiple testing ' +
220                (s.violatedHolm ? s.violatedHolm + ' remained significant.' : 'none remained significant.') +
221                ' A rejected independence suggests the diagram omits a relationship between the variables involved, ' +
222                'or that a linear test is inappropriate for them.';
223        }
224        if (s.skipped) {
225            out += ' ' + s.skipped + ' implication' + (s.skipped !== 1 ? 's' : '') +
226                ' could not be tested (variables absent from the data or too few complete cases).';
227        }
228        return out;
229    }
230
231    /** Tab-separated table of the results, for pasting into a spreadsheet. */
232    function toTSV(res) {
233        var rows = [['implication', 'estimate', 'ci_low', 'ci_high', 'p', 'p_holm', 'n', 'verdict']];
234        res.tests.forEach(function (t) {
235            rows.push(t.skipped
236                ? [implText(t.imp), '', '', '', '', '', '', 'not tested: ' + t.skipped]
237                : [implText(t.imp), t.estimate.toFixed(4), t.lo.toFixed(4), t.hi.toFixed(4), t.p.toExponential(3),
238                   t.pHolm.toExponential(3), t.n, t.violated ? 'violated' : 'consistent']);
239        });
240        return rows.map(function (r) { return r.join('\t'); }).join('\n');
241    }
242
243    // ── Dialog ────────────────────────────────────────────────────────────
244
245    var root = null, state = null;
246
247    function ensureRoot() {
248        if (root) return;
249        root = document.createElement('div');
250        root.id = 'lt-modal';
251        root.className = 'modal-overlay';
252        root.setAttribute('role', 'dialog');
253        root.setAttribute('aria-modal', 'true');
254        root.setAttribute('aria-labelledby', 'lt-title');
255        root.style.display = 'none';
256        root.innerHTML =
257            '<div class="modal-dialog modal-dialog-wide">' +
258              '<div class="modal-header">' +
259                '<h2 id="lt-title">Test the implications against your data</h2>' +
260                '<button type="button" class="modal-close" aria-label="Close" id="lt-close">✕</button>' +
261              '</div>' +
262              '<div class="modal-body">' +
263                '<p class="modal-intro">Your diagram predicts that certain pairs of variables are independent once ' +
264                  'others are held fixed. Upload the data you collected and each prediction is tested with a ' +
265                  'partial-correlation test (Fisher\'s z) — the same test as <code>localTests()</code> in the dagitty R package. ' +
266                  'A rejected prediction means the diagram is missing a relationship between those variables.</p>' +
267                '<div class="modal-callout">These tests are linear and treat every column as numeric ' +
268                  '(binary or ordinal codes are used as they are). Treat the results as a check on the diagram, ' +
269                  'not as proof of it: passing does not make the arrows correct.</div>' +
270                '<div class="modal-field">' +
271                  '<label for="lt-file" class="modal-label">Data file ' +
272                    '<span class="modal-label-hint">— CSV, TSV, Excel or plain text; first row = column names</span></label>' +
273                  '<input type="file" id="lt-file" accept=".csv,.tsv,.txt,.xlsx,.xls,.xlsb,.ods,text/csv,text/tab-separated-values">' +
274                '</div>' +
275                '<div id="lt-map-field" class="modal-field" style="display:none">
275' +
276                  '<span class="modal-label">Match diagram variables to data columns ' +
277                    '<span class="modal-label-hint">— matched automatically where the names agree; fix any that are wrong</span></span>' +
278                  '<table class="lt-map" id="lt-map"><thead><tr><th scope="col">Variable in diagram</th>' +
279                    '<th scope="col">Column in data</th></tr></thead><tbody></tbody></table>' +
280                '</div>' +
281                '<div class="modal-field">' +
282                  '<label for="lt-alpha" class="modal-label">Significance level</label>' +
283                  '<select id="lt-alpha">' +
284                    '<option value="0.01">0.01</option><option value="0.05" selected>0.05</option><option value="0.10">0.10</option>' +
285                  '</select>' +
286                '</div>' +
287                '<div id="lt-status" class="modal-status" aria-live="polite" aria-atomic="true"></div>' +
288                '<div id="lt-results" style="display:none">' +
289                  '<p id="lt-summary" class="lt-summary"></p>' +
290                  '<div class="lt-table-wrap"><table class="lt-table" id="lt-table"><thead><tr>' +
291                    '<th scope="col">Implied independence</th><th scope="col">r</th><th scope="col">95% CI</th>' +
292                    '<th scope="col">p</th><th scope="col">p (Holm)</th><th scope="col">n</th><th scope="col">Verdict</th>' +
293                  '</tr></thead><tbody></tbody></table></div>' +
294                '</div>' +
295              '</div>' +
296              '<div class="modal-footer">' +
297                '<button type="button" class="btn-secondary" id="lt-cancel">Close</button>' +
298                '<button type="button" class="btn-secondary" id="lt-copy" disabled>Copy table</button>' +
299                '<button type="button" class="btn-secondary" id="lt-use" disabled>Use in methods statement</button>' +
300                '<button type="button" id="lt-run" disabled>Run tests</button>' +
301              '</div>' +
302            '</div>';
303        document.body.appendChild(root);
304
305        root.querySelector('#lt-close').addEventListener('click', close);
306        root.querySelector('#lt-cancel').addEventListener('click', close);
307        root.addEventListener('click', function (e) { if (e.target === root) close(); });
308        document.addEventListener('keydown', function (e) {
309            if (root.style.display === 'flex' && e.key === 'Escape') { e.preventDefault(); close(); }
310        });
311        root.querySelector('#lt-file').addEventListener('change', function () {
312            if (this.files && this.files[0]) loadFile(this.files[0]);
313        });
314        root.querySelector('#lt-run').addEventListener('click', runFromDialog);
315        root.querySelector('#lt-use').addEventListener('click', useInMethods);
316        root.querySelector('#lt-copy').addEventListener('click', copyTable);
317    }
318
319    function setStatus(msg, type) {
320        var el = root.querySelector('#lt-status');
321        el.textContent = msg || '';
322        el.className = 'modal-status' + (type ? ' status-' + type : '');
323    }
324
325    function loadFile(file) {
326        state.table = null;
327        root.querySelector('#lt-run').disabled = true;
328        root.querySelector('#lt-results').style.display = 'none';
329        TabularFile.read(file, function (err, res) {
330            if (err) { setStatus(err.message, 'error'); return; }
331            try {
332                state.table = CausalDiscovery.parseTable(res.text);
333            } catch (e) { setStatus('Could not read ' + res.label + ': ' + e.message, 'error'); return; }
334            state.label = res.label;
335            renderMapping();
336            var msg = 'Loaded ' + res.label + ' — ' + state.table.nRows + ' rows, ' + state.table.headers.length +
337                ' numeric column' + (state.table.headers.length !== 1 ? 's' : '') + '.';
338            if (state.table.dropped.length) msg += ' Skipped non-numeric: ' + state.table.dropped.slice(0, 6).join(', ') +
339                (state.table.dropped.length > 6 ? '…' : '') + '.';
340            setStatus(msg + ' Check the matching, then run.', 'success');
341            root.querySelector('#lt-run').disabled = false;
342        }, function (s) { setStatus(s, 'running'); });
343    }
344
345    function diagramVariables(g) {
346        var latent = ids(g.getLatentNodes()), selected = ids(g.getSelectedNodes());
347        return ids(g.getVertices()).filter(function (v) { return latent.indexOf(v) === -1 && selected.indexOf(v) === -1; }).sort();
348    }
349
350    function renderMapping() {
351        var g = Model.dag, vars = diagramVariables(g);
352        var field = root.querySelector('#lt-map-field');
353        var tbody = root.querySelector('#lt-map tbody');
354        tbody.innerHTML = '';
355        if (!state.table) { field.style.display = 'none'; return; }
356        var auto = autoMap(vars, state.table.headers);
357        state.mapping = state.mapping || {};
358        vars.forEach(function (v) {
359            if (!(v in state.mapping) || (state.mapping[v] && state.table.headers.indexOf(state.mapping[v]) === -1)) {
360                state.mapping[v] = auto[v];
361            }
362            var tr = document.createElement('tr');
363            var th = document.createElement('th');
364            th.scope = 'row'; th.textContent = v;
365            tr.appendChild(th);
366            var td = document.createElement('td');
367            var sel = document.createElement('select');
368            sel.setAttribute('aria-label', 'Data column for ' + v);
369            var none = document.createElement('option');
370            none.value = ''; none.textContent = '— not in the data —';
371            sel.appendChild(none);
372            state.table.headers.forEach(function (h) {
373                var o = document.createElement('option');
374                o.value = h; o.textContent = h;
375                if (state.mapping[v] === h) o.selected = true;
376                sel.appendChild(o);
377            });
378            sel.addEventListener('change', function () { state.mapping[v] = sel.value || null; });
379            td.appendChild(sel);
380            tr.appendChild(td);
381            tbody.appendChild(tr);
382        });
383        field.style.display = vars.length ? '' : 'none';
384    }
385
386    function runFromDialog() {
387        if (!state.table) return;
388        var g = Model.dag;
389        var alpha = parseFloat(root.querySelector('#lt-alpha').value) || 0.05;
390        setStatus('Testing…', 'running');
391        var res;
392        try { res = run(g, state.table, state.mapping || {}, alpha); }
393        catch (e) { setStatus('Could not run the tests: ' + e.message, 'error'); return; }
394        res.modelCode = g.toString();
395        res.dataLabel = state.label;
396        window.LocalTests.last = res;
397        renderResults(res);
398        var s = res.summary;
399        setStatus(s.tested ? 'Tested ' + s.tested + ' implication' + (s.tested !== 1 ? 's' : '') + '.' :
400            'Nothing could be tested — match at least two diagram variables to data columns.', s.tested ? 'success' : 'error');
401        root.querySelector('#lt-use').disabled = !s.tested;
402        root.querySelector('#lt-copy').disabled = !res.tests.length;
403    }
404
405    function renderResults(res) {
406        var s = res.summary;
407        var wrap = root.querySelector('#lt-results');
408        var sum = root.querySelector('#lt-summary');
409        var tbody = root.querySelector('#lt-table tbody');
410        tbody.innerHTML = '';
411
412        var txt;
413        if (!s.tested) txt = 'No implication could be tested.';
414        else if (!s.violated) txt = 'All ' + s.tested + ' testable implications are consistent with the data at α = ' + s.alpha + '.';
415        else txt = s.violated + ' of ' + s.tested + ' implications ' + (s.violated === 1 ? 'is' : 'are') + ' violated at α = ' +
416            s.alpha + (s.violatedHolm ? ' (' + s.violatedHolm + ' after Holm correction)' : ' (none after Holm correction)') + '.';
417        if (s.skipped) txt += ' ' + s.skipped + ' not tested.';
418        if (s.droppedSelection) txt += ' ' + s.droppedSelection + ' involving the selection variable were left out.';
419        sum.textContent = txt;
420        sum.className = 'lt-summary ' + (s.violated ? 'lt-bad' : 'lt-good');
421
422        res.tests.forEach(function (t) {
423            var tr = document.createElement('tr');
424            tr.className = t.skipped ? 'lt-skipped' : (t.violated ? 'lt-violated' : 'lt-ok');
425            var pCell = function (p) { return p < 0.001 ? '< .001' : p.toFixed(3).replace(/^0\./, '.'); };
426            var cells = t.skipped
427                ? [implText(t.imp), '', '', '', '', '', 'not tested — ' + t.skipped]
428                : [implText(t.imp), f2(t.estimate), f2(t.lo) + ' to ' + f2(t.hi), pCell(t.p),
429                   pCell(t.pHolm), String(t.n), t.violated ? 'violated' : 'consistent'];
430            cells.forEach(function (c, i) {
431                var td = document.createElement(i === 0 ? 'th' : 'td');
432                if (i === 0) td.scope = 'row';
433                td.textContent = c;
434                tr.appendChild(td);
435            });
436            tbody.appendChild(tr);
437        });
438        wrap.style.display = '';
439    }
440
441    function useInMethods() {
442        var res = window.LocalTests.last;
443        if (!res || !window.MethodsExport) return;
444        MethodsExport.setLocalTestResults(methodsSentence(res));
445        setStatus('Added to "Assumptions for your paper" — the placeholder now reports these results.', 'success');
446        if (window.Workspace && Workspace.toast) Workspace.toast('Methods statement updated with the test results.', 'ok');
447    }
448
449    function copyTable() {
450        var res = window.LocalTests.last;
451        if (!res) return;
452        var tsv = toTSV(res);
453        var done = function (ok) { setStatus(ok ? 'Table copied (tab-separated).' : 'Could not copy automatically.', ok ? 'success' : 'error'); };
454        if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(tsv).then(function () { done(true); }, function () { done(false); });
455        else done(false);
456    }
457
458    function open() {
459        if (!window.Model || !Model.dag) return;
460        ensureRoot();
461        state = state || {};
462        root.style.display = 'flex';
463        var g = Model.dag;
464        var imps = implicationsOf(g);
465        if (!imps.list.length) {
466            setStatus('This diagram implies no testable independencies' +
467                (imps.droppedSelection ? ' apart from ones involving the selection variable' : '') +
468                ', so there is nothing to test.', 'warning');
469        } else if (!state.table) {
470            setStatus(imps.list.length + ' implied independenc' + (imps.list.length !== 1 ? 'ies' : 'y') + ' to test. Choose a data file.', '');
471        }
472        renderMapping();
473        if (state.table) root.querySelector('#lt-run').disabled = false;
474        setTimeout(function () { var f = root.querySelector('#lt-file'); if (f) f.focus(); }, 40);
475    }
476    function close() { if (root) root.style.display = 'none'; }
477
478    window.LocalTests = {
479        open: open, close: close, run: run, last: null,
480        fisherTest: fisherTest, holm: holm, autoMap: autoMap, implicationsOf: implicationsOf,
481        partialFromColumns: partialFromColumns, methodsSentence: methodsSentence, toTSV: toTSV, implText: implText
482    };
483
484    window.addEventListener('load', function () {
485        if (window.DAGittyControl) DAGittyControl.observe('graphchange', function () {
486            if (root && root.style.display === 'flex') renderMapping();
487        });
488    });
489
490})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.