/* 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(() => {}); })();