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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 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.