function renderTooltip(label, data) {
const d = typeof data === 'string' ? JSON.parse(data) : data;
return `<div class="tt-title">${label}<span class="tt-count">${d.count} nodes</span></div>
<table class="tt-tbl">
<thead><tr><th>pct</th><th>value</th></tr></thead>
<tbody>
<tr><td>p1</td><td>${fmtNum(d.p1)}</td></tr>
<tr><td>p10</td><td>${fmtNum(d.p10)}</td></tr>
<tr><td>p50</td><td>${fmtNum(d.p50)}</td></tr>
<tr><td>p90</td><td>${fmtNum(d.p90)}</td></tr>
<tr><td>p99</td><td>${fmtNum(d.p99)}</td></tr>
</tbody></table>`;
}
function renderDescTooltip(label, desc, formula, calc) {
const f = formula ? `<div class="tt-formula">${escHtml(formula)}</div>` : '';
const c = calc ? `<div class="tt-formula tt-calc">${escHtml(calc)}</div>` : '';
const descHtml = escHtml(desc)
.replace(/<br\s*\/?>/gi, '<br>')
.replace(/`([^`]+)`/g, '<code class="tt-code">$1</code>');
return `<div class="tt-title">${escHtml(label)}</div>${f}${c}<div class="tt-desc">${descHtml}</div>`;
}
function renderNodeTooltip(node, level) {
const rows = [];
const path = (node.path || node.id || '').replace(/^\{[^}]+\}\//, '');
if (path) rows.push(['path', path]);
const gk = levelUi(level).grouping?.key;
if (gk) {
const gv = nodeAttr(node, gk);
if (gv != null && gv !== '') rows.push([(attrLabel(level, gk) || gk).toLowerCase(), String(gv)]);
}
for (const k of ['hk', 'sloc']) {
const v = nodeAttr(node, k);
if (v != null) rows.push([k, String(v)]);
}
const body = rows.map(([k, v]) => `<b>${escHtml(k)}:</b> ${escHtml(v)}`).join('<br>');
return `<div class="tt-title">${escHtml(node.name || node.id)}</div>` +
(body ? `<div class="tt-desc">${body}</div>` : '');
}
function renderGroupTooltip(stats) {
const rows = [
['files', String(stats.files)],
['sloc', stats.sloc > 0 ? fmtMetricShort(stats.sloc) : null],
['hk', stats.hk > 0 ? fmtMetricShort(stats.hk) : null],
].filter(([, v]) => v !== null);
const body = rows.map(([k, v]) => `<b>${k}:</b> ${escHtml(v)}`).join('<br>');
return `<div class="tt-title">${escHtml(stats.name)}</div>` +
(body ? `<div class="tt-desc">${body}</div>` : '');
}
function setupTooltip() {
const tt = document.getElementById('tt');
let current = null;
let showTimer = null;
let lastX = 0, lastY = 0;
const SHOW_DELAY = 300;
const titleOf = (el, lv) => {
if (el.dataset.tipTitle) return el.dataset.tipTitle;
const col = el.dataset.col;
if (col && lv) return attrName(lv, col) || col;
return el.textContent.trim();
};
const position = () => {
const pad = 14;
const tw = tt.offsetWidth, th = tt.offsetHeight;
let x = lastX + pad, y = lastY + pad;
if (x + tw > window.innerWidth - 8) x = lastX - tw - pad;
if (y + th > window.innerHeight - 8) y = lastY - th - pad;
tt.style.left = x + 'px';
tt.style.top = y + 'px';
};
const contentFor = e => {
if (e.target.closest('g.node[data-group-id]')) return null;
if (e.target.closest('g.node[data-node-id]')) return null;
const cellTt = e.target.closest('[data-tt]');
const cellTip = e.target.closest('[data-tip]');
const cellNum = e.target.closest('td[data-col]');
if (cellTt && cellTt.dataset.tt) {
const label = cellTt.dataset.tipTitle
|| cellTt.closest('tr')?.querySelector('td:first-child')?.textContent || '';
return { el: cellTt, html: renderTooltip(label, cellTt.dataset.tt) };
}
if (cellTip && cellTip.dataset.tip) {
return { el: cellTip, html: renderDescTooltip(titleOf(cellTip, null), cellTip.dataset.tip, cellTip.dataset.tipFormula, cellTip.dataset.tipCalc) };
}
if (cellNum) {
const id = cellNum.dataset.col;
const lv = cellNum.closest('[data-view]')?.dataset.view || 'files';
const desc = attrDesc(lv, id);
if (!desc) return null; const formula = attrFormula(lv, id);
const nid = cellNum.closest('tr[data-node-id]')?.dataset.nodeId;
const node = nid ? activeGraph(lv).nodes.find(n => n.id === nid) : null;
const calc = node ? calcDisplay(lv, id, node) : '';
return { el: cellNum, html: renderDescTooltip(titleOf(cellNum, lv), desc, formula, calc) };
}
return null;
};
const cancelShow = () => { if (showTimer) { clearTimeout(showTimer); showTimer = null; } };
document.addEventListener('mouseover', e => {
const r = contentFor(e);
if (!r || r.el === current) return; cancelShow();
current = r.el; showTimer = setTimeout(() => {
showTimer = null;
tt.innerHTML = r.html;
tt.removeAttribute('hidden');
position();
}, SHOW_DELAY);
});
document.addEventListener('mouseout', e => {
if (!current) return;
if (e.target !== current && !current.contains(e.target)) return;
if (e.relatedTarget && current.contains(e.relatedTarget)) return;
hide();
});
document.addEventListener('mousemove', e => {
lastX = e.clientX; lastY = e.clientY;
if (!tt.hasAttribute('hidden')) position();
});
const hide = () => { cancelShow(); tt.setAttribute('hidden', ''); current = null; };
window.hideMetricTooltip = hide;
document.addEventListener('click', hide, true);
}