/* Hero: the corpus's family tree drawn as spreading roots (the medallion),
and the masked token in every wordmark. Needs d3 when a medallion is present. */
(function () {
// Private Hugging Face Spaces authorise requests through the __sign token of the
// document URL (and the cookie it sets). Carry it, and __theme, across every
// same-origin navigation and data fetch so in-iframe navigation keeps working.
const carried = new URLSearchParams(location.search);
const keep = ['__sign', '__theme'].filter(k => carried.has(k));
if (keep.length) {
const decorate = (href) => { try { const u = new URL(href, location.href); if (u.origin !== location.origin) return href; keep.forEach(k => u.searchParams.set(k, carried.get(k))); return u.toString(); } catch (e) { return href; } };
document.addEventListener('click', (e) => { const a = e.target.closest('a[href]'); if (!a) return; const h = a.getAttribute('href'); if (!h || h.startsWith('#') || h.startsWith('mailto:')) return; a.href = decorate(a.href); }, true);
const f = window.fetch; window.fetch = (input, init) => f(typeof input === 'string' ? decorate(input) : input, init);
}
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
// Wordmarks: the last cell is a masked token that gets predicted.
document.querySelectorAll('.wordmark .tok').forEach((tok, i) => {
const reveal = () => { tok.lastChild.textContent = '1'; tok.dataset.state = 'predicted'; };
if (reduced) reveal(); else setTimeout(reveal, 1300 + i * 150);
});
const el = document.getElementById('hero-art');
if (!el || typeof d3 === 'undefined') return;
const tip = document.getElementById('tip');
const showTip = (html, ev) => { if (!tip) return; tip.innerHTML = html; tip.style.display = 'block'; const pad = 14; let x = ev.clientX + pad, y = ev.clientY + pad; const r = tip.getBoundingClientRect(); if (x + r.width > innerWidth - 8) x = ev.clientX - r.width - pad; if (y + r.height > innerHeight - 8) y = ev.clientY - r.height - pad; tip.style.left = x + 'px'; tip.style.top = y + 'px'; };
const hideTip = () => { if (tip) tip.style.display = 'none'; };
fetch('data/tree.json').then(r => r.json()).then(data => {
const size = 780, c = size / 2, R = c - 40;
// Polytomies make the outer child's link sweep across its siblings' subtrees, so
// every node with more than two children is resolved into a ladder of zero-length
// binary splits, the larger and deeper subtree first at each step.
const prep = (n) => {
if (!n.children || !n.children.length) { n._h = 0; n._c = 1; return n; }
n.children.forEach(prep);
n.children.sort((a, b) => (b._h - a._h) || (b._c - a._c));
while (n.children.length > 2) {
const rest = n.children.slice(1);
const w = { name: '', length: 0, children: rest, _h: Math.max(...rest.map(r => r._h)) + 1, _c: rest.reduce((t, r) => t + r._c, 0) };
n.children = [n.children[0], w];
}
n._h = Math.max(...n.children.map(k => k._h)) + 1; n._c = n.children.reduce((t, k) => t + k._c, 0);
return n;
};
const root = d3.hierarchy(prep(data.tree));
root.each(d => { d.age = (d.parent ? d.parent.age : 0) + (d.data.length || 0); });
const maxAge = d3.max(root.leaves(), d => d.age);
// Ladderise: at every node the deeper, larger subtree comes first, so the fan is
// regular and far fewer links have to swing across their neighbours.
const nLeaves = root.leaves().length;
d3.cluster().size([2 * Math.PI * (1 - 1 / nLeaves), R]).separation(() => 1)(root);
// Put every split at the boundary between its two children's fans rather than at
// the mean of their angles: a small branch next to a wide clade otherwise gets a
// node inside the clade's fan and a link that swings across it.
root.eachAfter(d => {
if (!d.children) return;
const spans = d.children.map(k => d3.extent(k.leaves(), l => l.x)).sort((p, q) => p[0] - q[0]);
d.x = spans.length === 1 ? d.children[0].x : (spans[0][1] + spans[1][0]) / 2;
});
// Keep the family relationships and ordering, with an uneven outer silhouette.
// This is an illustration of the tree, so no time rings or age scale are shown.
const seed = name => Array.from(name).reduce((value, ch) => (value * 31 + ch.charCodeAt(0)) >>> 0, 17);
root.eachBefore(d => {
const reach = R - 12 + 12 * Math.sin(d.x * 3 + .6) + 7 * Math.sin(d.x * 7 - .8);
const tipOffset = d.children ? 0 : (seed(d.data.name) % 25) - 12;
d.y = Math.max(d.parent ? d.parent.y : 0, Math.pow(d.age / maxAge, 1.45) * reach + tipOffset);
d.width = 1 + 7 * Math.pow(d.data._c / nLeaves, .65);
});
const polar = (angle, radius) => [Math.sin(angle) * radius, -Math.cos(angle) * radius];
const rootPath = ({ source, target }) => {
const middle = (source.y + target.y) / 2;
const controls = [polar(source.x, source.y), polar(source.x, middle), polar(target.x, middle), polar(target.x, target.y)];
const distance = controls.slice(1).reduce((sum, point, i) => sum + Math.hypot(point[0] - controls[i][0], point[1] - controls[i][1]), 0);
const phase = (seed(target.data.name || String(target.x)) % 628) / 100;
const amplitude = Math.min(3.5, distance * .025);
const steps = Math.max(12, Math.ceil(distance / 5));
const points = d3.range(steps + 1).map(i => {
const t = i / steps, u = 1 - t;
const weights = [u ** 3, 3 * u * u * t, 3 * u * t * t, t ** 3];
const point = [0, 1].map(axis => controls.reduce((sum, p, j) => sum + weights[j] * p[axis], 0));
const tangent = [0, 1].map(axis => 3 * u * u * (controls[1][axis] - controls[0][axis]) + 6 * u * t * (controls[2][axis] - controls[1][axis]) + 3 * t * t * (controls[3][axis] - controls[2][axis]));
const length = Math.hypot(...tangent) || 1;
// The bend fades at each end so adjoining branches meet smoothly.
const bend = amplitude * Math.sin(Math.PI * t) ** 2 * Math.sin(2 * Math.PI * t + phase);
return [point[0] - tangent[1] / length * bend, point[1] + tangent[0] / length * bend];
});
const sides = [[], []];
const startWidth = Math.min(source.width, target.width * 1.65);
const endWidth = target.children ? target.width : .08;
points.forEach((point, i) => {
const t = i / steps;
const before = points[Math.max(0, i - 1)], after = points[Math.min(steps, i + 1)];
const dx = after[0] - before[0], dy = after[1] - before[1], length = Math.hypot(dx, dy) || 1;
const width = (startWidth * (1 - t) + endWidth * t) * (1 + .06 * Math.sin(3 * Math.PI * t + phase) * Math.sin(Math.PI * t)) / 2;
sides[0].push([point[0] - dy / length * width, point[1] + dx / length * width]);
sides[1].push([point[0] + dy / length * width, point[1] - dx / length * width]);
});
return d3.line()(sides[0].concat(sides[1].reverse())) + 'Z';
};
const svg = d3.create('svg').attr('viewBox', `0 0 ${size} ${size}`).attr('aria-hidden', 'true');
// Rotation is driven from JavaScript on a nested group: a CSS transform would
// replace the SVG translate attribute and spin the tree about the corner.
const centre = svg.append('g').attr('transform', `translate(${c},${c})`);
const g = centre.append('g').attr('class', 'spin');
if (!reduced) { const period = 480000; let t0 = null; const tick = (ts) => { if (t0 === null) t0 = ts; g.attr('transform', `rotate(${((ts - t0) / period) * 360})`); requestAnimationFrame(tick); }; requestAnimationFrame(tick); }
const branches = g.selectAll('path.root-branch').data(root.links()).join('path').attr('class', 'root-branch').attr('d', rootPath);
// Each family has a visible root-tip marker and a larger hover target.
const tips = g.selectAll('g.root-tip').data(root.leaves()).join('g').attr('class', 'root-tip')
.attr('transform', d => `translate(${polar(d.x, d.y).join(',')})`);
tips.append('circle').attr('class', 'root-tip-marker').attr('r', 3.2);
tips.append('circle').attr('class', 'root-tip-hit').attr('r', 8);
tips.on('mousemove', (ev, d) => {
const ancestors = new Set(d.ancestors());
branches.classed('highlighted', link => ancestors.has(link.target));
showTip(`${d.data.name} · ${d.data.order}
${d.data.n_species} species · ${d.data.windows.toLocaleString()} windows`, ev);
}).on('mouseleave', () => { branches.classed('highlighted', false); hideTip(); });
const lab = svg.append('g').attr('transform', `translate(${c},${c})`);
lab.append('text').attr('class', 'tree-label').attr('x', 0).attr('y', -R - 16).attr('text-anchor', 'middle').text(`${nLeaves} families · ${data.tree.n_species} species`);
el.appendChild(svg.node());
}).catch(() => {});
})();