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.
- bindsight/README.md +44 -0
- bindsight/__init__.py +15 -0
- bindsight/benchmark/__init__.py +71 -0
- bindsight/benchmark/core.py +497 -0
- bindsight/benchmark/defaults.py +28 -0
- bindsight/benchmark/designer_bench.py +899 -0
- bindsight/benchmark/outcomes.py +508 -0
- bindsight/benchmark/panel.py +621 -0
- bindsight/benchmark/rediscovery.py +1153 -0
- bindsight/benchmark/statistics.py +622 -0
- bindsight/benchmark/study.py +1153 -0
- bindsight/benchmark/study_figures.py +164 -0
- bindsight/benchmark/study_report.py +607 -0
- bindsight/cli.py +2429 -0
- bindsight/config.py +451 -0
- bindsight/cost.py +258 -0
- bindsight/deg/__init__.py +7 -0
- bindsight/deg/pydeseq2_runner.py +285 -0
- bindsight/design/__init__.py +23 -0
- bindsight/design/_common.py +501 -0
- bindsight/design/bindcraft.py +78 -0
- bindsight/design/boltzgen.py +79 -0
- bindsight/design/developability.py +140 -0
- bindsight/design/embeddings.py +165 -0
- bindsight/design/prescreen.py +169 -0
- bindsight/design/protocol.py +111 -0
- bindsight/design/rfdiff_mpnn.py +93 -0
- bindsight/epitopes/__init__.py +7 -0
- bindsight/epitopes/surface_bind.py +134 -0
- bindsight/export/__init__.py +7 -0
- bindsight/export/ro_crate.py +595 -0
- bindsight/io/__init__.py +19 -0
- bindsight/io/cbioportal.py +118 -0
- bindsight/io/gdc.py +343 -0
- bindsight/io/paths.py +124 -0
- bindsight/pipelines/__init__.py +9 -0
- bindsight/pipelines/caveats.py +60 -0
- bindsight/pipelines/discover.py +1443 -0
- bindsight/pipelines/full_run.py +410 -0
- bindsight/plugins.py +335 -0
- bindsight/provenance/__init__.py +34 -0
- bindsight/provenance/append.py +199 -0
- bindsight/provenance/fragments.py +216 -0
- bindsight/provenance/manifest.py +506 -0
- bindsight/rank/__init__.py +7 -0
- bindsight/rank/scoring.py +355 -0
- bindsight/report/__init__.py +39 -0
- bindsight/report/format.py +100 -0
- bindsight/report/html.py +645 -0
- bindsight/report/showcase.py +653 -0
- bindsight/report/templates/report.css +67 -0
- bindsight/report/templates/report.html.j2 +420 -0
- bindsight/report/theme.py +231 -0
- bindsight/report/web/__init__.py +35 -0
- bindsight/report/web/app.py +624 -0
- bindsight/report/web/static/binder_viewer.js +245 -0
- bindsight/report/web/static/bindsight.css +455 -0
- bindsight/report/web/static/charts.js +425 -0
- bindsight/report/web/static/vendor/3Dmol-min.js +2 -0
- bindsight/report/web/static/vendor/3Dmol.LICENSE +90 -0
- bindsight/report/web/static/your_data_check.js +370 -0
- bindsight/report/web/templates/base.html.j2 +48 -0
- bindsight/report/web/templates/evidence.html.j2 +341 -0
- bindsight/report/web/templates/overview.html.j2 +140 -0
- bindsight/report/web/templates/runs.html.j2 +125 -0
- bindsight/report/web/templates/try.html.j2 +155 -0
- bindsight/report/web/templates/your_data.html.j2 +46 -0
- bindsight/runners/__init__.py +7 -0
- bindsight/runners/colab.py +158 -0
- bindsight/runners/job_exec.py +1110 -0
- bindsight/runners/kaggle.py +425 -0
- bindsight/runners/kaggle_kernel.py +489 -0
- bindsight/runners/local_docker.py +154 -0
- bindsight/runners/mock.py +167 -0
- bindsight/runners/modal_runner.py +199 -0
- bindsight/runners/notebook.py +116 -0
- bindsight/runners/notebook_content.py +217 -0
- bindsight/runners/protocol.py +81 -0
- bindsight/runners/source_wheel.py +104 -0
- bindsight/runners/tools.py +997 -0
- bindsight/structures/__init__.py +7 -0
- bindsight/structures/alphafolddb.py +93 -0
- bindsight/structures/plddt.py +92 -0
- bindsight/structures/topology.py +227 -0
- bindsight/surfaceome/__init__.py +42 -0
- bindsight/surfaceome/data/surfaceome_extended.tsv +4814 -0
- bindsight/surfaceome/data/surfy_offline.txt +17 -0
- bindsight/surfaceome/data/surfy_v1.ensembl.tsv +3380 -0
- bindsight/surfaceome/data/surfy_v1.uniprot.txt +2901 -0
- bindsight/surfaceome/surfy.py +441 -0
- bindsight/targets/__init__.py +7 -0
- bindsight/targets/ensembl_uniprot.py +56 -0
- bindsight/targets/gtex.py +250 -0
- bindsight/targets/open_targets.py +239 -0
- bindsight/validate/__init__.py +7 -0
- bindsight/validate/af2_ig.py +49 -0
- bindsight/validate/boltz2.py +256 -0
- bindsight/validate/chai1r.py +42 -0
- bindsight/validate/protocol.py +188 -0
- bindsight-0.3.5.data/data/bindsight_demo/config.yaml +54 -0
- bindsight-0.3.5.dist-info/METADATA +495 -0
- bindsight-0.3.5.dist-info/RECORD +106 -0
- bindsight-0.3.5.dist-info/WHEEL +4 -0
- bindsight-0.3.5.dist-info/entry_points.txt +20 -0
- bindsight-0.3.5.dist-info/licenses/COPYRIGHT +24 -0
- 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
|
+
})();
|