/* Compact interactive figures for the Overview, using the report's data files. */ (function () { if (typeof d3 === 'undefined') return; const esc = value => String(value).replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])); const pct = value => d3.format('.3~g')(value * 100) + '%'; function frame(host, controls) { host.hidden = false; host.innerHTML = `
${controls}
`; return { host, canvas: host.querySelector('.plot-canvas'), reading: host.querySelector('.plot-reading'), svg(width, height, label) { this.canvas.replaceChildren(); const svg = d3.create('svg').attr('viewBox', `0 0 ${width} ${height}`).attr('role', 'group').attr('aria-label', label); svg.append('title').text(label); this.canvas.appendChild(svg.node()); return svg; } }; } function responsive(host, draw) { let width = 0; const redraw = () => { const next = Math.round(host.clientWidth); if (next > 0 && next !== width) { width = next; draw(Math.max(270, width)); } }; new ResizeObserver(redraw).observe(host); redraw(); return () => draw(Math.max(270, width || 440)); } // One tab stop per chart; arrow keys move between marks. Taps retain the readout. function inspect(marks, describe, onSelect) { const nodes = marks.nodes(); const select = (node, datum) => { marks.classed('is-selected', function () { return this === node; }) .attr('tabindex', function () { return this === node ? 0 : -1; }) .attr('aria-pressed', function () { return String(this === node); }); onSelect(datum); }; marks.attr('class', function () { return (this.getAttribute('class') || '') + ' plot-mark'; }) .attr('role', 'button').attr('tabindex', (_, i) => i === 0 ? 0 : -1) .attr('aria-label', describe).attr('aria-pressed', 'false') .on('pointerenter.inspect', function (_, d) { select(this, d); }) .on('click.inspect', function (_, d) { select(this, d); }) .on('focus.inspect', function (_, d) { select(this, d); }) .on('keydown.inspect', function (event, d) { const i = nodes.indexOf(this); const steps = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }; let next = i; if (event.key in steps) next = (i + steps[event.key] + nodes.length) % nodes.length; else if (event.key === 'Home') next = 0; else if (event.key === 'End') next = nodes.length - 1; else if (!['Enter', ' '].includes(event.key)) return; event.preventDefault(); if (next !== i) nodes[next].focus(); else select(this, d); }); return datum => { const node = nodes.find(node => d3.select(node).datum() === datum); if (node) select(node, datum); }; } function horizontalGrid(svg, y, width, left, right, ticks, format = d3.format('.1f')) { const grid = svg.append('g').attr('class', 'plot-grid'); ticks.forEach(value => { grid.append('line').attr('x1', left).attr('x2', width - right).attr('y1', y(value)).attr('y2', y(value)); grid.append('text').attr('x', left - 7).attr('y', y(value) + 3).attr('text-anchor', 'end').text(format(value)); }); } function benchmark(host, data) { const f = frame(host, 'Selected referencesSbaltest · axis starts at 0.5 · whiskers 95% CI'); const references = new Set(['PlantCAD2-L', 'GPN', 'CARBON-8B', 'NTv3-pre', 'Evo2-7b', 'AgroNT']); let selected = 'botanic1-XL'; responsive(host, W => { const rows = data.models.filter(m => m.ours || references.has(m.id)).slice().sort((a, b) => b.s_bal - a.s_bal); const L = W < 350 ? 132 : 146, R = 45, T = 30, H = 252, rowH = (H - T - 6) / rows.length; const x = d3.scaleLinear().domain([.5, .8]).range([L, W - R]); const svg = f.svg(W, H, 'Aggregate test scores. Select a model; arrow keys move between models.'); const grid = svg.append('g').attr('class', 'plot-grid'); [.5, .6, .7, .8].forEach(v => { grid.append('line').attr('x1', x(v)).attr('x2', x(v)).attr('y1', T - 10).attr('y2', H - 5); grid.append('text').attr('x', x(v)).attr('y', 12).attr('text-anchor', 'middle').text(v.toFixed(1)); }); const marks = svg.selectAll('g.benchmark-model').data(rows).join('g').attr('class', 'benchmark-model').attr('transform', (_, i) => `translate(0,${T + i * rowH})`); marks.append('rect').attr('class', 'mark-hit').attr('x', 0).attr('y', -12).attr('width', W).attr('height', rowH - 1).attr('rx', 3); marks.append('circle').attr('cx', 13).attr('cy', 0).attr('r', 10).attr('fill', '#fff').attr('stroke', '#d1d9cf'); marks.append('image').attr('href', d => 'assets/logos/' + d.logo).attr('x', 5).attr('y', -8).attr('width', 16).attr('height', 16).attr('preserveAspectRatio', 'xMidYMid meet'); marks.append('text').attr('class', 'model-name').attr('x', 29).attr('y', 4).text(d => d.name); marks.append('rect').attr('class', 'benchmark-bar').attr('x', L).attr('y', -6).attr('width', d => x(d.s_bal) - L).attr('height', 12).attr('rx', 2) .attr('fill', d => d.ours ? 'var(--lime)' : 'var(--plot-reference)') .attr('stroke', d => d.ours ? 'var(--accent)' : 'var(--rule-2)').attr('stroke-width', .6); marks.filter(d => d.s_bal_ci).append('line').attr('class', 'benchmark-ci').attr('x1', d => x(d.s_bal_ci[0])).attr('x2', d => x(d.s_bal_ci[1])).attr('y1', 0).attr('y2', 0); marks.append('text').attr('class', 'plot-value').attr('x', d => x(d.s_bal_ci ? d.s_bal_ci[1] : d.s_bal) + 5).attr('y', 4).text(d => d.s_bal.toFixed(4)); const select = inspect(marks, d => `${d.name}, ${d.params_str} parameters, aggregate test score ${d.s_bal.toFixed(4)}`, d => { selected = d.id; f.reading.innerHTML = `${esc(d.name)} · ${esc(d.params_str)} parameters · score ${d.s_bal.toFixed(4)}${d.s_bal_ci ? ` · 95% CI [${d.s_bal_ci[0].toFixed(4)}, ${d.s_bal_ci[1].toFixed(4)}]` : ''}`; }); select(rows.find(d => d.id === selected) || rows[0]); }); } function corpus(host, data) { const families = d3.rollups(data.species, rows => ({ species: rows.length, windows: d3.sum(rows, d => d.windows) }), d => d.family) .map(([name, counts]) => ({ name, ...counts })).sort((a, b) => b.windows - a.windows); const options = families.slice().sort((a, b) => a.name.localeCompare(b.name)).map(d => ``).join(''); const f = frame(host, `Training windows`); const family = host.querySelector('#overview-corpus-family'); family.value = families[0].name; let selectFamily; responsive(host, W => { const H = 252; const root = d3.hierarchy({ children: families }).sum(d => d.windows || 0).sort((a, b) => b.value - a.value); d3.treemap().size([W, H]).padding(2).round(true)(root); const svg = f.svg(W, H, 'Training corpus by family. Tile area is proportional to training windows. Select a family or use arrow keys.'); const marks = svg.selectAll('g.corpus-family').data(root.leaves()).join('g').attr('class', 'corpus-family').attr('transform', d => `translate(${d.x0},${d.y0})`); const palette = ['#c8d9bd', '#a5c09b', '#d6e4ce', '#8fae83', '#b8cdae']; marks.append('rect').attr('class', 'family-tile').attr('width', d => Math.max(0, d.x1 - d.x0)).attr('height', d => Math.max(0, d.y1 - d.y0)).attr('rx', 2).attr('fill', (_, i) => palette[i % palette.length]); marks.filter(d => d.x1 - d.x0 > d.data.name.length * 6.1 + 14 && d.y1 - d.y0 > 30).append('text').attr('class', 'family-name').attr('x', 7).attr('y', 17).text(d => d.data.name); marks.filter(d => d.x1 - d.x0 > d.data.name.length * 6.1 + 14 && d.y1 - d.y0 > 47).append('text').attr('class', 'family-count').attr('x', 7).attr('y', 33).text(d => `${d.data.species} species`); const select = inspect(marks, d => `${d.data.name}, ${d.data.species} species, ${d.data.windows.toLocaleString()} windows`, d => { family.value = d.data.name; f.reading.innerHTML = `${esc(d.data.name)} · ${d.data.species} species · ${pct(d.data.windows / data.totals.windows)} of training windows`; }); selectFamily = () => select(root.leaves().find(d => d.data.name === family.value)); selectFamily(); }); family.addEventListener('change', () => selectFamily()); } function causal(host, data) { const keys = ['XL315', 'L', 'M315', 'S315', 'PlantCAD2', 'Evo2-1b', 'Carbon-8B', 'NTv3-650M-pre', 'AgroNT']; const colours = ['var(--accent)', 'var(--ours)', 'var(--ours-light)', 'var(--tree-2)', 'var(--plantcad)', 'var(--evo2)', 'var(--carbon)', 'var(--ntv3)', 'var(--agront)']; const models = keys.map(key => data.models.find(m => m.key === key)); // Same tie-aware recall as the full benchmark, over each model's covered studies. const recall = (model, alpha) => { let sum = 0, count = 0; model.n_variants.forEach((n, i) => { if (n < 0) return; sum += Math.min(Math.max(Math.floor(alpha * n) - model.n_better[i], 0), model.n_tie[i]) / model.n_tie[i]; count++; }); return sum / count; }; const f = frame(host, ''); f.reading.classList.add('plot-legend'); f.reading.setAttribute('aria-live', 'off'); f.reading.innerHTML = models.map((m, i) => `${esc(m.name)} `).join(''); const range = host.querySelector('input'), output = host.querySelector('output'); const alphas = d3.range(121).map(i => 10 ** (-4 + 4 * i / 120)); const series = models.map(model => ({ model, values: alphas.map(alpha => ({ alpha, recall: recall(model, alpha) })) })); let alpha = .01, update; responsive(host, W => { const H = 252, L = 38, R = 12, T = 20, B = 43; const x = d3.scaleLog().domain([.0001, 1]).range([L, W - R]); const y = d3.scaleLinear().domain([0, 1]).range([H - B, T]); const svg = f.svg(W, H, 'Causal-variant recall against shortlist size. Use the Shortlist slider or move across the plot.'); horizontalGrid(svg, y, W, L, R, [0, .25, .5, .75, 1], d => `${d * 100}%`); [.0001, .001, .01, .1, 1].forEach(v => svg.append('text').attr('class', 'plot-tick').attr('x', x(v)).attr('y', H - B + 16).attr('text-anchor', v === 1 ? 'end' : 'middle').text(pct(v))); svg.append('text').attr('class', 'plot-unit-label').attr('x', L).attr('y', 10).text('Studies recovering the causal variant'); svg.append('text').attr('class', 'plot-unit-label').attr('x', (L + W - R) / 2).attr('y', H - 4).attr('text-anchor', 'middle').text('Candidate variants shortlisted (log scale)'); const line = d3.line().x(d => x(d.alpha)).y(d => y(d.recall)); svg.append('path').attr('d', line(alphas.map(a => ({ alpha: a, recall: a })))).attr('class', 'chance-line'); series.forEach((s, i) => svg.append('path').datum(s).attr('class', 'recall-curve').attr('d', line(s.values)).attr('stroke', colours[i]).attr('stroke-width', i === 0 ? 2.5 : 1.5)); const guide = svg.append('line').attr('class', 'shortlist-guide').attr('y1', T).attr('y2', H - B); const points = svg.selectAll('circle.recall-point').data(models).join('circle').attr('class', 'recall-point').attr('r', 3.2).attr('fill', (_, i) => colours[i]); update = value => { alpha = value; range.value = Math.log10(value); output.textContent = 'Top ' + pct(value); range.setAttribute('aria-valuetext', output.textContent + ' of candidate variants'); guide.attr('x1', x(value)).attr('x2', x(value)); points.attr('cx', x(value)).attr('cy', d => y(recall(d, value))); models.forEach(model => { host.querySelector(`[data-recall="${model.key}"]`).textContent = pct(recall(model, value)); }); }; svg.append('rect').attr('class', 'recall-hit').attr('x', L).attr('y', T).attr('width', W - L - R).attr('height', H - T - B) .on('pointermove', event => { if (event.pointerType !== 'touch') update(x.invert(Math.max(L, Math.min(W - R, d3.pointer(event, svg.node())[0])))); }).on('click', event => update(x.invert(Math.max(L, Math.min(W - R, d3.pointer(event, svg.node())[0]))))); update(alpha); }); range.addEventListener('input', () => update(10 ** Number(range.value))); } function finetune(host, [atac, peft, tf]) { const methods = ['Full fine-tuning', 'LoRA', 'IA3']; const models = ['Botanic1-S', 'AgroNT', 'PlantCAD2-L']; const colour = name => name.startsWith('Botanic1') ? 'var(--lime)' : name === 'AgroNT' ? 'var(--agront)' : name === 'PlantCAD2-L' ? 'var(--plantcad)' : name.startsWith('NTv3') ? 'var(--ntv3)' : 'var(--plot-reference)'; const species = ['Arabidopsis thaliana', 'Zea mays']; const names = { 'Arabidopsis thaliana': 'A. thaliana', 'Zea mays': 'Z. mays' }; const experiments = []; const add = (group, label, config) => experiments.push({ id: `ft-${experiments.length}`, group, label, ...config }); const atacArms = [ ['Botanic1-S pre-trained', 'Botanic1-S (318M) · pre-trained', 'var(--lime)'], ['Botanic1-S from scratch', 'Botanic1-S (318M) · random init', 'var(--ours-pale)'], ['ChromBPNet', 'ChromBPNet', 'var(--plot-reference)'] ]; [ ['count_r', 'Read-count correlation', 'Pearson r', 1, false], ['raw_jsd', 'Profile error', 'Raw JSD', .6, true] ].forEach(([key, label, unit, max, lower]) => add('ATAC-seq', `ATAC-seq · ${label}`, { unit, max, lower, groups: species.map(sp => ({ key: sp, name: names[sp] })), series: atacArms.map((a, i) => ({ key: String(i), name: a[1], colour: a[2] })), rows: species.flatMap(sp => atacArms.map(([backbone, name, fill], i) => { const candidates = atac.rows.filter(d => d.species === sp && d.backbone === backbone); const d = candidates.reduce((best, next) => (lower ? next[key] < best[key] : next[key] > best[key]) ? next : best); return { id: `${sp}-${i}`, group: sp, series: String(i), name, colour: fill, value: d[key], ci: d[key + '_ci'], detail: `${names[sp]} · ${d.head}` }; })), context: 'Best head per species, metric and initialisation · whiskers: 95% bootstrap intervals' })); const taskNames = { terminator: 'Terminator strength', promoter: 'Promoter strength', poly_a: 'poly(A) sites', lncrna: 'lncRNA classification' }; ['terminator', 'promoter', 'poly_a', 'lncrna'].forEach(task => { const regression = task === 'terminator' || task === 'promoter'; const metrics = regression ? (task === 'terminator' ? ['merged', 'tobacco', 'maize'] : ['tobacco', 'maize']).flatMap(sub => ['pearson', 'r2'].map(metric => ({ key: `${metric}_${sub}`, unit: metric === 'pearson' ? 'Pearson r' : 'R²', sub, metric }))) : [{ key: 'auroc_mean', unit: 'Mean AUROC', sub: 'global tuning' }, { key: 'auroc_mean_per_species', unit: 'Mean AUROC', sub: 'per-species tuning' }]; metrics.forEach(({ key, unit, sub, metric }) => { const subLabel = sub === 'merged' ? 'tobacco + maize' : sub; const base = regression && peft.baselines.find(d => d.task === task && d.metric === metric && d.subtask.toLowerCase() === sub); add(taskNames[task], `${taskNames[task]} · ${subLabel}${regression ? ` · ${unit}` : ''}`, { unit, max: 1, groups: methods.map(method => ({ key: method, name: method === 'Full fine-tuning' ? 'Full FT' : method === 'IA3' ? 'IA³' : method })), series: models.map(model => ({ key: model, name: model, colour: colour(model) })), rows: methods.flatMap(method => models.map(model => { const d = peft.cells.find(d => d.task === task && d.method === method && d.model === model); return { id: `${method}-${model}`, group: method, series: model, name: model, colour: colour(model), value: d[key], detail: method === 'IA3' ? 'IA³' : method }; })), baseline: base ? { value: base.value, name: task === 'terminator' ? 'DenseNet from scratch' : 'CNN from scratch' } : null, context: `Held-out test sets · ${regression ? subLabel : `${sub} · mean over six species`}` }); }); }); ['test', 'validation'].forEach(split => add('TF-family binding', `TF-family binding · ${split === 'test' ? 'chromosome 5 test' : 'chromosome 4 validation'}`, { unit: 'Macro average precision', max: 1, horizontal: true, rows: tf[split].filter(d => d.model !== 'Botanic1-M').map(d => ({ id: `${d.model}-${d.initialization}`, name: d.model + (split === 'validation' && d.initialization === 'random' ? ' · random init' : ''), colour: d.initialization === 'random' ? 'var(--plot-reference)' : colour(d.model), value: d.mean, seeds: d.seeds, detail: `${d.initialization === 'random' ? 'Random initialisation' : 'Pre-trained'}${d.input_bp ? ` · ${d.input_bp.toLocaleString()} bp input` : ''}` })), context: `Chromosome ${split === 'test' ? '5 test' : '4 validation'} · dots: three seeds · Botanic1-S uses the context-extended backbone` })); const options = d3.groups(experiments, d => d.group).map(([group, views]) => `${views.map(d => ``).join('')}`).join(''); const f = frame(host, ``); const legend = document.createElement('div'); legend.className = 'plot-legend ft-legend'; f.canvas.after(legend); const choice = host.querySelector('select'); const selected = new Map(); const redraw = responsive(host, W => { const view = experiments.find(d => d.id === choice.value); const H = 252, L = view.horizontal ? (W < 350 ? 109 : 128) : 37, R = view.horizontal ? 46 : 12, T = 29, B = 30; const svg = f.svg(W, H, `${view.label}. ${view.unit}, ${view.lower ? 'lower' : 'higher'} is better. ${view.context}. Select a result or use arrow keys.`); svg.append('text').attr('class', 'plot-unit-label').attr('x', view.horizontal ? 10 : L).attr('y', 13).text(`${view.unit} · ${view.lower ? 'lower' : 'higher'} is better`); legend.hidden = !!view.horizontal; legend.innerHTML = (view.series || []).map(a => `${esc(a.name)}`).join('') + (view.baseline ? `${esc(view.baseline.name)} ${view.baseline.value.toFixed(3)}` : ''); const marks = svg.selectAll('g.ft-arm').data(view.rows).join('g').attr('class', 'ft-arm'); if (view.horizontal) { const x = d3.scaleLinear().domain([0, 1]).range([L, W - R]); const y = d3.scaleBand().domain(view.rows.map(d => d.id)).range([T + 12, H - 10]).padding(.28); [0, .25, .5, .75, 1].forEach(value => { svg.append('line').attr('class', 'ft-grid-line').attr('x1', x(value)).attr('x2', x(value)).attr('y1', T + 5).attr('y2', H - 8); svg.append('text').attr('class', 'plot-tick').attr('x', x(value)).attr('y', T).attr('text-anchor', 'middle').text(value); }); marks.attr('transform', d => `translate(0,${y(d.id) + y.bandwidth() / 2})`); marks.append('rect').attr('class', 'mark-hit').attr('width', W).attr('y', -y.step() / 2).attr('height', y.step()); marks.append('text').attr('class', 'model-name').attr('x', 8).attr('y', 4).each(function (d) { const [name, init] = d.name.split(' · '), label = d3.select(this); label.text(name + (init ? ' ' : '')); if (init) { label.attr('y', -3); label.append('tspan').attr('x', 8).attr('dy', 13).text('· ' + init); } }); marks.append('rect').attr('class', 'ft-bar').attr('x', L).attr('y', -7).attr('width', d => x(d.value) - L).attr('height', 14).attr('rx', 2).attr('fill', d => d.colour); marks.append('text').attr('class', 'plot-value').attr('x', d => x(d.value) + 7).attr('y', 4).text(d => d.value.toFixed(3)); marks.each(function (d) { d3.select(this).selectAll('circle').data(d.seeds).join('circle').attr('class', 'ft-seed').attr('cx', value => x(value)).attr('cy', (_, i) => (i - 1) * 4).attr('r', 2.2); }); } else { const x = d3.scaleBand().domain(view.groups.map(d => d.key)).range([L, W - R]).padding(.22); const inner = d3.scaleBand().domain(view.series.map(d => d.key)).range([0, x.bandwidth()]).padding(.15); const y = d3.scaleLinear().domain([0, view.max]).range([H - B, T]); horizontalGrid(svg, y, W, L, R, y.ticks(4)); view.groups.forEach(group => svg.append('text').attr('class', 'plot-tick').attr('x', x(group.key) + x.bandwidth() / 2).attr('y', H - 8).attr('text-anchor', 'middle').text(group.name)); if (view.baseline) svg.append('line').attr('class', 'ft-baseline').attr('x1', L).attr('x2', W - R).attr('y1', y(view.baseline.value)).attr('y2', y(view.baseline.value)); marks.attr('transform', d => `translate(${x(d.group) + inner(d.series)},0)`); marks.append('rect').attr('class', 'mark-hit').attr('x', -2).attr('y', T).attr('width', inner.bandwidth() + 4).attr('height', H - T - B); marks.append('rect').attr('class', 'ft-bar').attr('y', d => y(d.value)).attr('width', inner.bandwidth()).attr('height', d => y(0) - y(d.value)).attr('rx', 2).attr('fill', d => d.colour).attr('stroke', 'var(--rule-2)').attr('stroke-width', .6); const intervals = marks.filter(d => d.ci); intervals.append('line').attr('class', 'confidence').attr('x1', inner.bandwidth() / 2).attr('x2', inner.bandwidth() / 2).attr('y1', d => y(d.ci[0])).attr('y2', d => y(d.ci[1])); [0, 1].forEach(end => intervals.append('line').attr('class', 'confidence').attr('x1', inner.bandwidth() / 2 - 3).attr('x2', inner.bandwidth() / 2 + 3).attr('y1', d => y(d.ci[end])).attr('y2', d => y(d.ci[end]))); if (inner.bandwidth() >= 32) marks.append('text').attr('class', 'plot-value').attr('x', inner.bandwidth() / 2).attr('y', d => y(d.ci ? d.ci[1] : d.value) - 6).attr('text-anchor', 'middle').text(d => d.value.toFixed(3)); } svg.selectAll('.ft-grid-line, .plot-grid').lower(); const uncertainty = d => d.ci ? ` · 95% CI ${d.ci.map(v => v.toFixed(3)).join(' to ')}` : d.seeds ? ` · seeds ${d.seeds.map(v => v.toFixed(3)).join(', ')}` : ''; const select = inspect(marks, d => `${d.name}, ${d.detail}, ${view.unit} ${d.value.toFixed(3)}${uncertainty(d)}`, d => { selected.set(view.id, d.id); f.reading.innerHTML = `${esc(d.name)} · ${esc(d.detail)} · ${view.unit} ${d.value.toFixed(3)}${uncertainty(d)}${esc(view.context)}`; }); select(view.rows.find(d => d.id === selected.get(view.id)) || view.rows[0]); }); choice.addEventListener('change', redraw); } const plots = [ ['overview-benchmark', 'leaderboard.json', benchmark], ['overview-corpus', 'corpus.json', corpus], ['overview-causal', 'causal.json', causal], ['overview-finetune', ['atac.json', 'finetune.json', 'tf.json'], finetune] ]; plots.forEach(async ([id, file, draw]) => { const host = document.getElementById(id); if (!host) return; try { const data = await Promise.all([file].flat().map(async name => { const response = await fetch('data/' + name); if (!response.ok) throw new Error('Plot data unavailable'); return response.json(); })); draw(host, Array.isArray(file) ? data : data[0]); } catch (_) { host.hidden = false; host.innerHTML = '

Plot unavailable. The full results are linked below.

'; } }); })();