bindsight 0.3.5__py3-none-any.whl

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.
Files changed (106) hide show
  1. bindsight/README.md +44 -0
  2. bindsight/__init__.py +15 -0
  3. bindsight/benchmark/__init__.py +71 -0
  4. bindsight/benchmark/core.py +497 -0
  5. bindsight/benchmark/defaults.py +28 -0
  6. bindsight/benchmark/designer_bench.py +899 -0
  7. bindsight/benchmark/outcomes.py +508 -0
  8. bindsight/benchmark/panel.py +621 -0
  9. bindsight/benchmark/rediscovery.py +1153 -0
  10. bindsight/benchmark/statistics.py +622 -0
  11. bindsight/benchmark/study.py +1153 -0
  12. bindsight/benchmark/study_figures.py +164 -0
  13. bindsight/benchmark/study_report.py +607 -0
  14. bindsight/cli.py +2429 -0
  15. bindsight/config.py +451 -0
  16. bindsight/cost.py +258 -0
  17. bindsight/deg/__init__.py +7 -0
  18. bindsight/deg/pydeseq2_runner.py +285 -0
  19. bindsight/design/__init__.py +23 -0
  20. bindsight/design/_common.py +501 -0
  21. bindsight/design/bindcraft.py +78 -0
  22. bindsight/design/boltzgen.py +79 -0
  23. bindsight/design/developability.py +140 -0
  24. bindsight/design/embeddings.py +165 -0
  25. bindsight/design/prescreen.py +169 -0
  26. bindsight/design/protocol.py +111 -0
  27. bindsight/design/rfdiff_mpnn.py +93 -0
  28. bindsight/epitopes/__init__.py +7 -0
  29. bindsight/epitopes/surface_bind.py +134 -0
  30. bindsight/export/__init__.py +7 -0
  31. bindsight/export/ro_crate.py +595 -0
  32. bindsight/io/__init__.py +19 -0
  33. bindsight/io/cbioportal.py +118 -0
  34. bindsight/io/gdc.py +343 -0
  35. bindsight/io/paths.py +124 -0
  36. bindsight/pipelines/__init__.py +9 -0
  37. bindsight/pipelines/caveats.py +60 -0
  38. bindsight/pipelines/discover.py +1443 -0
  39. bindsight/pipelines/full_run.py +410 -0
  40. bindsight/plugins.py +335 -0
  41. bindsight/provenance/__init__.py +34 -0
  42. bindsight/provenance/append.py +199 -0
  43. bindsight/provenance/fragments.py +216 -0
  44. bindsight/provenance/manifest.py +506 -0
  45. bindsight/rank/__init__.py +7 -0
  46. bindsight/rank/scoring.py +355 -0
  47. bindsight/report/__init__.py +39 -0
  48. bindsight/report/format.py +100 -0
  49. bindsight/report/html.py +645 -0
  50. bindsight/report/showcase.py +653 -0
  51. bindsight/report/templates/report.css +67 -0
  52. bindsight/report/templates/report.html.j2 +420 -0
  53. bindsight/report/theme.py +231 -0
  54. bindsight/report/web/__init__.py +35 -0
  55. bindsight/report/web/app.py +624 -0
  56. bindsight/report/web/static/binder_viewer.js +245 -0
  57. bindsight/report/web/static/bindsight.css +455 -0
  58. bindsight/report/web/static/charts.js +425 -0
  59. bindsight/report/web/static/vendor/3Dmol-min.js +2 -0
  60. bindsight/report/web/static/vendor/3Dmol.LICENSE +90 -0
  61. bindsight/report/web/static/your_data_check.js +370 -0
  62. bindsight/report/web/templates/base.html.j2 +48 -0
  63. bindsight/report/web/templates/evidence.html.j2 +341 -0
  64. bindsight/report/web/templates/overview.html.j2 +140 -0
  65. bindsight/report/web/templates/runs.html.j2 +125 -0
  66. bindsight/report/web/templates/try.html.j2 +155 -0
  67. bindsight/report/web/templates/your_data.html.j2 +46 -0
  68. bindsight/runners/__init__.py +7 -0
  69. bindsight/runners/colab.py +158 -0
  70. bindsight/runners/job_exec.py +1110 -0
  71. bindsight/runners/kaggle.py +425 -0
  72. bindsight/runners/kaggle_kernel.py +489 -0
  73. bindsight/runners/local_docker.py +154 -0
  74. bindsight/runners/mock.py +167 -0
  75. bindsight/runners/modal_runner.py +199 -0
  76. bindsight/runners/notebook.py +116 -0
  77. bindsight/runners/notebook_content.py +217 -0
  78. bindsight/runners/protocol.py +81 -0
  79. bindsight/runners/source_wheel.py +104 -0
  80. bindsight/runners/tools.py +997 -0
  81. bindsight/structures/__init__.py +7 -0
  82. bindsight/structures/alphafolddb.py +93 -0
  83. bindsight/structures/plddt.py +92 -0
  84. bindsight/structures/topology.py +227 -0
  85. bindsight/surfaceome/__init__.py +42 -0
  86. bindsight/surfaceome/data/surfaceome_extended.tsv +4814 -0
  87. bindsight/surfaceome/data/surfy_offline.txt +17 -0
  88. bindsight/surfaceome/data/surfy_v1.ensembl.tsv +3380 -0
  89. bindsight/surfaceome/data/surfy_v1.uniprot.txt +2901 -0
  90. bindsight/surfaceome/surfy.py +441 -0
  91. bindsight/targets/__init__.py +7 -0
  92. bindsight/targets/ensembl_uniprot.py +56 -0
  93. bindsight/targets/gtex.py +250 -0
  94. bindsight/targets/open_targets.py +239 -0
  95. bindsight/validate/__init__.py +7 -0
  96. bindsight/validate/af2_ig.py +49 -0
  97. bindsight/validate/boltz2.py +256 -0
  98. bindsight/validate/chai1r.py +42 -0
  99. bindsight/validate/protocol.py +188 -0
  100. bindsight-0.3.5.data/data/bindsight_demo/config.yaml +54 -0
  101. bindsight-0.3.5.dist-info/METADATA +495 -0
  102. bindsight-0.3.5.dist-info/RECORD +106 -0
  103. bindsight-0.3.5.dist-info/WHEEL +4 -0
  104. bindsight-0.3.5.dist-info/entry_points.txt +20 -0
  105. bindsight-0.3.5.dist-info/licenses/COPYRIGHT +24 -0
  106. bindsight-0.3.5.dist-info/licenses/LICENSE +681 -0
@@ -0,0 +1,425 @@
1
+ // SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ //
4
+ // Charts for bindsight, drawn as SVG with no dependencies.
5
+ //
6
+ // Written rather than vendored. The chart types this project needs are few and
7
+ // simple -- a strip of paired values, a forest of intervals, a bar row, a
8
+ // scatter -- and a hand-written renderer costs nothing, ships nothing, needs no
9
+ // CDN, adds no licence to the inventory, and takes its colours from the same
10
+ // custom properties as everything else.
11
+ //
12
+ // Two rules, both of which are about honesty rather than looks:
13
+ //
14
+ // * A chart never carries information the surrounding text does not. Hover is
15
+ // for detail, never for the finding: a tooltip does not print, does not
16
+ // exist on a touch screen, and is absent from a photograph of the screen.
17
+ // * Every series is distinguishable without colour. Shape and position do the
18
+ // work; hue is a reinforcement.
19
+
20
+ (function () {
21
+ "use strict";
22
+
23
+ const NS = "http://www.w3.org/2000/svg";
24
+
25
+ function el(name, attrs, parent) {
26
+ const node = document.createElementNS(NS, name);
27
+ for (const key in attrs || {}) {
28
+ if (attrs[key] !== null && attrs[key] !== undefined) {
29
+ node.setAttribute(key, String(attrs[key]));
30
+ }
31
+ }
32
+ if (parent) parent.appendChild(node);
33
+ return node;
34
+ }
35
+
36
+ function cssVar(name, fallback) {
37
+ const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
38
+ return v || fallback;
39
+ }
40
+
41
+ // ------------------------------------------------------------- tooltip
42
+ let tip = null;
43
+ function tooltip() {
44
+ if (!tip) {
45
+ tip = document.createElement("div");
46
+ tip.className = "tooltip";
47
+ tip.hidden = true;
48
+ document.body.appendChild(tip);
49
+ }
50
+ return tip;
51
+ }
52
+
53
+ // The tooltip is a bold label and some lines under it -- never richer than
54
+ // that, and never markup. It took `html` and assigned it to innerHTML, and
55
+ // every value in it (`r.label`, `p.label`, `p.tip`, `p.id`) comes from run
56
+ // data: gene symbols and binder ids that trace back to the reader's own
57
+ // counts matrix and config. Composed as nodes, that is text; concatenated
58
+ // into a string, it was markup that happened to look like text so far.
59
+ function fillTip(t, label, lines) {
60
+ t.innerHTML = "";
61
+ if (label) {
62
+ const bold = document.createElement("strong");
63
+ bold.textContent = String(label);
64
+ t.appendChild(bold);
65
+ }
66
+ (lines || []).forEach(function (line, i) {
67
+ if (label || i) t.appendChild(document.createElement("br"));
68
+ t.appendChild(document.createTextNode(String(line)));
69
+ });
70
+ }
71
+
72
+ function bindTip(node, label, lines) {
73
+ node.addEventListener("pointerenter", function (ev) {
74
+ const t = tooltip();
75
+ fillTip(t, label, lines);
76
+ t.hidden = false;
77
+ move(ev);
78
+ });
79
+ node.addEventListener("pointermove", move);
80
+ node.addEventListener("pointerleave", function () {
81
+ tooltip().hidden = true;
82
+ });
83
+ // Keyboard reach: a chart whose detail is mouse-only is detail some
84
+ // readers simply do not have.
85
+ //
86
+ // The name has to come with the tab stop. Each mark was reachable and
87
+ // anonymous: a screen reader announced "graphic" and moved on, so the
88
+ // keyboard route existed and carried none of the detail it was added for.
89
+ // The tooltip already holds that detail, so it is what gets announced.
90
+ node.setAttribute("role", "img");
91
+ node.setAttribute(
92
+ "aria-label",
93
+ [label]
94
+ .concat(lines || [])
95
+ .filter(Boolean)
96
+ .map(String)
97
+ .join(", "),
98
+ );
99
+ node.setAttribute("tabindex", "0");
100
+ node.addEventListener("focus", function () {
101
+ const t = tooltip();
102
+ fillTip(t, label, lines);
103
+ t.hidden = false;
104
+ const r = node.getBoundingClientRect();
105
+ t.style.left = r.right + 8 + "px";
106
+ t.style.top = r.top + "px";
107
+ });
108
+ node.addEventListener("blur", function () {
109
+ tooltip().hidden = true;
110
+ });
111
+
112
+ function move(ev) {
113
+ const t = tooltip();
114
+ const pad = 14;
115
+ let x = ev.clientX + pad;
116
+ let y = ev.clientY + pad;
117
+ const r = t.getBoundingClientRect();
118
+ if (x + r.width > window.innerWidth - 8) x = ev.clientX - r.width - pad;
119
+ if (y + r.height > window.innerHeight - 8) y = ev.clientY - r.height - pad;
120
+ t.style.left = x + "px";
121
+ t.style.top = y + "px";
122
+ }
123
+ }
124
+
125
+ // --------------------------------------------------------------- scales
126
+ function linear(domain, range) {
127
+ const [d0, d1] = domain;
128
+ const [r0, r1] = range;
129
+ const span = d1 - d0 || 1;
130
+ const f = function (v) {
131
+ return r0 + ((v - d0) / span) * (r1 - r0);
132
+ };
133
+ f.invert = function (p) {
134
+ return d0 + ((p - r0) / (r1 - r0)) * span;
135
+ };
136
+ f.domain = domain;
137
+ return f;
138
+ }
139
+
140
+ function ticks(lo, hi, count) {
141
+ const span = hi - lo;
142
+ if (!(span > 0)) return [lo];
143
+ const raw = span / Math.max(1, count);
144
+ const mag = Math.pow(10, Math.floor(Math.log10(raw)));
145
+ const norm = raw / mag;
146
+ const step = (norm >= 5 ? 10 : norm >= 2 ? 5 : norm >= 1 ? 2 : 1) * mag;
147
+ const out = [];
148
+ for (let v = Math.ceil(lo / step) * step; v <= hi + 1e-9; v += step) {
149
+ out.push(Number(v.toFixed(10)));
150
+ }
151
+ return out;
152
+ }
153
+
154
+ function frame(host, height) {
155
+ host.innerHTML = "";
156
+ const width = Math.max(320, host.clientWidth || 640);
157
+ const svg = el("svg", {
158
+ class: "chart",
159
+ viewBox: `0 0 ${width} ${height}`,
160
+ width: "100%",
161
+ height: height,
162
+ role: "img",
163
+ }, host);
164
+ return { svg: svg, width: width, height: height };
165
+ }
166
+
167
+ // ------------------------------------------------- paired strip (designs
168
+ // vs their own shuffles). The comparison the calibration actually makes: a
169
+ // line per pair, so a reader sees the pairing rather than two clouds.
170
+ function pairedStrip(host, spec) {
171
+ const { svg, width, height } = frame(host, spec.height || 300);
172
+ const m = { top: 24, right: 24, bottom: 36, left: 52 };
173
+ const iw = width - m.left - m.right;
174
+ const ih = height - m.top - m.bottom;
175
+ const g = el("g", { transform: `translate(${m.left},${m.top})` }, svg);
176
+
177
+ const all = spec.pairs.flatMap((p) => [p.a, p.b]);
178
+ const lo = Math.min.apply(null, all.concat([spec.threshold ?? Infinity]));
179
+ const hi = Math.max.apply(null, all.concat([spec.threshold ?? -Infinity]));
180
+ const pad = (hi - lo) * 0.08 || 0.05;
181
+ const y = linear([lo - pad, hi + pad], [ih, 0]);
182
+ const xa = iw * 0.3;
183
+ const xb = iw * 0.7;
184
+
185
+ ticks(lo - pad, hi + pad, 5).forEach(function (t) {
186
+ el("line", { class: "gridline", x1: 0, x2: iw, y1: y(t), y2: y(t) }, g);
187
+ el("text", { x: -8, y: y(t) + 4, "text-anchor": "end" }, g).textContent = t.toFixed(2);
188
+ });
189
+
190
+ if (spec.threshold !== undefined && spec.threshold !== null) {
191
+ el("line", {
192
+ x1: 0, x2: iw, y1: y(spec.threshold), y2: y(spec.threshold),
193
+ stroke: cssVar("--err", "#a32020"), "stroke-width": 1.5, "stroke-dasharray": "5 4",
194
+ }, g);
195
+ el("text", {
196
+ x: iw, y: y(spec.threshold) - 6, "text-anchor": "end",
197
+ fill: cssVar("--err", "#a32020"), "font-weight": 600,
198
+ }, g).textContent = spec.thresholdLabel || `threshold ${spec.threshold}`;
199
+ }
200
+
201
+ [[xa, spec.labelA || "A"], [xb, spec.labelB || "B"]].forEach(function (d) {
202
+ el("text", { x: d[0], y: ih + 24, "text-anchor": "middle", "font-weight": 600 }, g)
203
+ .textContent = d[1];
204
+ });
205
+
206
+ const navy = cssVar("--navy", "#0b5394");
207
+ const teal = cssVar("--teal", "#0e766d");
208
+
209
+ spec.pairs.forEach(function (p) {
210
+ // The connecting line is the point of this chart: it shows which member
211
+ // of each pair won, which a pair of box plots hides entirely.
212
+ const up = p.b > p.a;
213
+ el("line", {
214
+ x1: xa, y1: y(p.a), x2: xb, y2: y(p.b),
215
+ stroke: up ? cssVar("--err", "#a32020") : cssVar("--ok", "#1f6f35"),
216
+ "stroke-width": 1, opacity: 0.5,
217
+ }, g);
218
+
219
+ // Circle for A, square for B: the series are separable in greyscale.
220
+ const ca = el("circle", { class: "mark", cx: xa, cy: y(p.a), r: 4.5, fill: navy }, g);
221
+ const cb = el("rect", {
222
+ class: "mark", x: xb - 4, y: y(p.b) - 4, width: 8, height: 8, fill: teal,
223
+ }, g);
224
+ const lines = [
225
+ `${spec.labelA}: ${p.a.toFixed(3)}`,
226
+ `${spec.labelB}: ${p.b.toFixed(3)}`,
227
+ `difference: ${(p.b - p.a).toFixed(3)}`,
228
+ ];
229
+ bindTip(ca, p.id, lines);
230
+ bindTip(cb, p.id, lines);
231
+ });
232
+
233
+ if (spec.title) svg.setAttribute("aria-label", spec.title);
234
+ return svg;
235
+ }
236
+
237
+ // ------------------------------------------------------ forest of intervals
238
+ function forest(host, spec) {
239
+ const rows = spec.rows;
240
+ const rowH = 30;
241
+ const { svg, width } = frame(host, rows.length * rowH + 56);
242
+ const m = { top: 16, right: 28, bottom: 32, left: spec.labelWidth || 150 };
243
+ const iw = width - m.left - m.right;
244
+ const g = el("g", { transform: `translate(${m.left},${m.top})` }, svg);
245
+
246
+ const lows = rows.map((r) => r.low);
247
+ const highs = rows.map((r) => r.high);
248
+ let lo = Math.min.apply(null, lows.concat([spec.reference ?? Infinity]));
249
+ let hi = Math.max.apply(null, highs.concat([spec.reference ?? -Infinity]));
250
+ const pad = (hi - lo) * 0.1 || 0.1;
251
+ lo -= pad; hi += pad;
252
+ const x = linear([lo, hi], [0, iw]);
253
+ const ih = rows.length * rowH;
254
+
255
+ ticks(lo, hi, 5).forEach(function (t) {
256
+ el("line", { class: "gridline", x1: x(t), x2: x(t), y1: 0, y2: ih }, g);
257
+ el("text", { x: x(t), y: ih + 20, "text-anchor": "middle" }, g).textContent = t.toFixed(2);
258
+ });
259
+
260
+ if (spec.reference !== undefined && spec.reference !== null) {
261
+ el("line", {
262
+ x1: x(spec.reference), x2: x(spec.reference), y1: 0, y2: ih,
263
+ stroke: cssVar("--ink-faint", "#68727f"), "stroke-width": 1.5, "stroke-dasharray": "4 3",
264
+ }, g);
265
+ }
266
+
267
+ rows.forEach(function (r, i) {
268
+ const cy = i * rowH + rowH / 2;
269
+ const excludes =
270
+ spec.reference === undefined || spec.reference === null
271
+ ? null
272
+ : r.low > spec.reference || r.high < spec.reference;
273
+ const colour = excludes === null
274
+ ? cssVar("--navy", "#0b5394")
275
+ : excludes ? cssVar("--ok", "#1f6f35") : cssVar("--ink-soft", "#55606d");
276
+
277
+ el("text", { x: -m.left + 4, y: cy + 4, "font-weight": 600 }, g).textContent = r.label;
278
+ el("line", { x1: x(r.low), x2: x(r.high), y1: cy, y2: cy, stroke: colour, "stroke-width": 2 }, g);
279
+ [r.low, r.high].forEach(function (v) {
280
+ el("line", { x1: x(v), x2: x(v), y1: cy - 5, y2: cy + 5, stroke: colour, "stroke-width": 2 }, g);
281
+ });
282
+ const dot = el("circle", { class: "mark", cx: x(r.point), cy: cy, r: 5, fill: colour }, g);
283
+
284
+ // The glyph, not the colour, states the conclusion.
285
+ if (excludes !== null) {
286
+ el("text", {
287
+ x: iw + 6, y: cy + 4, "font-weight": 700, fill: colour,
288
+ }, g).textContent = excludes ? "✓" : "–";
289
+ }
290
+
291
+ bindTip(
292
+ dot,
293
+ r.label,
294
+ [
295
+ `${r.point.toFixed(3)} (${r.ciLabel || "CI"} ${r.low.toFixed(3)} to ${r.high.toFixed(3)})`,
296
+ ]
297
+ .concat(r.n ? [`n = ${r.n}`] : [])
298
+ .concat(
299
+ excludes === null ? [] : [`${excludes ? "excludes" : "includes"} ${spec.reference}`]
300
+ )
301
+ );
302
+ });
303
+
304
+ if (spec.title) svg.setAttribute("aria-label", spec.title);
305
+ return svg;
306
+ }
307
+
308
+ // ------------------------------------------------------------------ bars
309
+ function bars(host, spec) {
310
+ const rows = spec.rows;
311
+ const rowH = 30;
312
+ const { svg, width } = frame(host, rows.length * rowH + 40);
313
+ const m = { top: 12, right: 90, bottom: 24, left: spec.labelWidth || 120 };
314
+ const iw = width - m.left - m.right;
315
+ const g = el("g", { transform: `translate(${m.left},${m.top})` }, svg);
316
+ const hi = Math.max.apply(null, rows.map((r) => r.value).concat([spec.max || 0])) || 1;
317
+ const x = linear([0, hi], [0, iw]);
318
+
319
+ rows.forEach(function (r, i) {
320
+ const y0 = i * rowH + 4;
321
+ el("text", { x: -8, y: y0 + 14, "text-anchor": "end", "font-weight": 600 }, g)
322
+ .textContent = r.label;
323
+ el("rect", { x: 0, y: y0, width: iw, height: rowH - 10, fill: cssVar("--rule", "#e3e6ea"), rx: 3 }, g);
324
+ const bar = el("rect", {
325
+ class: "mark", x: 0, y: y0, width: Math.max(1, x(r.value)), height: rowH - 10,
326
+ fill: r.colour || cssVar("--navy", "#0b5394"), rx: 3,
327
+ }, g);
328
+ el("text", { x: iw + 8, y: y0 + 14, "font-weight": 600 }, g).textContent = r.display;
329
+ bindTip(bar, r.label, [r.tip || r.display]);
330
+ });
331
+
332
+ if (spec.title) svg.setAttribute("aria-label", spec.title);
333
+ return svg;
334
+ }
335
+
336
+ // --------------------------------------------------------------- scatter
337
+ function scatter(host, spec) {
338
+ const { svg, width, height } = frame(host, spec.height || 340);
339
+ const m = { top: 20, right: 24, bottom: 44, left: 56 };
340
+ const iw = width - m.left - m.right;
341
+ const ih = height - m.top - m.bottom;
342
+ const g = el("g", { transform: `translate(${m.left},${m.top})` }, svg);
343
+
344
+ const xs = spec.points.map((p) => p.x);
345
+ const ys = spec.points.map((p) => p.y);
346
+ const x = linear([Math.min.apply(null, xs), Math.max.apply(null, xs)], [0, iw]);
347
+ const y = linear([Math.min.apply(null, ys), Math.max.apply(null, ys)], [ih, 0]);
348
+
349
+ ticks(x.domain[0], x.domain[1], 6).forEach(function (t) {
350
+ el("line", { class: "gridline", x1: x(t), x2: x(t), y1: 0, y2: ih }, g);
351
+ el("text", { x: x(t), y: ih + 18, "text-anchor": "middle" }, g).textContent = String(t);
352
+ });
353
+ ticks(y.domain[0], y.domain[1], 5).forEach(function (t) {
354
+ el("line", { class: "gridline", x1: 0, x2: iw, y1: y(t), y2: y(t) }, g);
355
+ el("text", { x: -8, y: y(t) + 4, "text-anchor": "end" }, g).textContent = String(t);
356
+ });
357
+
358
+ if (spec.xLabel) {
359
+ el("text", { x: iw / 2, y: ih + 38, "text-anchor": "middle", "font-weight": 600 }, g)
360
+ .textContent = spec.xLabel;
361
+ }
362
+ if (spec.yLabel) {
363
+ el("text", {
364
+ x: -ih / 2, y: -40, "text-anchor": "middle", "font-weight": 600,
365
+ transform: `rotate(-90 ${-ih / 2} ${-40})`,
366
+ }, g).textContent = spec.yLabel;
367
+ }
368
+
369
+ spec.points.forEach(function (p) {
370
+ const node = el("circle", {
371
+ class: "mark", cx: x(p.x), cy: y(p.y), r: p.r || 5,
372
+ fill: p.colour || cssVar("--navy", "#0b5394"), opacity: 0.85,
373
+ }, g);
374
+ // `p.tip`, when the spec supplies one, replaced the label as well --
375
+ // kept, so what a reader sees does not change.
376
+ // The label is emphasised, which the string this replaced did not do.
377
+ // Deliberate: every other tooltip here bolds its label, and scatter was
378
+ // the one that did not. Keeping the old plain text would preserve an
379
+ // inconsistency in a chart type no template in this repository emits.
380
+ bindTip(node, p.tip ? "" : p.label || "", [p.tip || `${p.x}, ${p.y}`]);
381
+ });
382
+
383
+ if (spec.title) svg.setAttribute("aria-label", spec.title);
384
+ return svg;
385
+ }
386
+
387
+ // ------------------------------------------------------------ public API
388
+ const api = { pairedStrip: pairedStrip, forest: forest, bars: bars, scatter: scatter };
389
+
390
+ function drawAll() {
391
+ document.querySelectorAll("[data-chart]").forEach(function (host) {
392
+ const kind = host.getAttribute("data-chart");
393
+ const fn = api[kind];
394
+ if (!fn) return;
395
+ let spec;
396
+ try {
397
+ spec = JSON.parse(host.getAttribute("data-spec") || "{}");
398
+ } catch (e) {
399
+ return;
400
+ }
401
+ try {
402
+ fn(host, spec);
403
+ } catch (e) {
404
+ // A chart that cannot draw must not take the page with it: the numbers
405
+ // it illustrates are already in the text beside it.
406
+ host.innerHTML = "";
407
+ const note = document.createElement("p");
408
+ note.className = "small muted";
409
+ note.textContent =
410
+ "This chart could not be drawn. The figures it shows are stated in " +
411
+ "the text above.";
412
+ host.appendChild(note);
413
+ }
414
+ });
415
+ }
416
+
417
+ window.bindsightCharts = api;
418
+ document.addEventListener("DOMContentLoaded", drawAll);
419
+
420
+ let resizeTimer = null;
421
+ window.addEventListener("resize", function () {
422
+ clearTimeout(resizeTimer);
423
+ resizeTimer = setTimeout(drawAll, 150);
424
+ });
425
+ })();