Change tooltip stickiness
Browse files- index.html +56 -3
index.html
CHANGED
|
@@ -105,7 +105,7 @@
|
|
| 105 |
padding: 12px 14px; min-width: 248px; max-width: 320px;
|
| 106 |
border-radius: 4px;
|
| 107 |
opacity: 0; visibility: hidden; transform: translateY(2px);
|
| 108 |
-
transition: opacity 0.
|
| 109 |
pointer-events: none;
|
| 110 |
}
|
| 111 |
.point-card.visible { opacity: 1; visibility: visible; transform: none; pointer-events: auto; }
|
|
@@ -482,6 +482,50 @@ function renderChart() {
|
|
| 482 |
const pointCard = document.getElementById('pointCard');
|
| 483 |
let pcHideTimer = null;
|
| 484 |
let pcSticky = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
function buildPointCardHtml(e) {
|
| 487 |
const info = AGENTS[e.agent] || null;
|
|
@@ -512,7 +556,11 @@ function buildPointCardHtml(e) {
|
|
| 512 |
|
| 513 |
const linkBtns = [];
|
| 514 |
if (e.filename) linkBtns.push(`<a href="${escapeHtml(submissionHref(e.filename))}" target="_blank" rel="noopener noreferrer">Submission β</a>`);
|
| 515 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
|
| 517 |
return `${head}<div class="pc-score"><span class="big">${fmt2(e.score)}</span> tok/s ${verified}</div><div class="pc-rows">${rowsHtml}</div>${note}<div class="pc-links">${linkBtns.join('')}</div>`;
|
| 518 |
}
|
|
@@ -532,6 +580,7 @@ function showPointCard(entry, c, element) {
|
|
| 532 |
const rect = c.canvas.getBoundingClientRect();
|
| 533 |
const px = rect.left + element.x;
|
| 534 |
const py = rect.top + element.y;
|
|
|
|
| 535 |
const w = pointCard.offsetWidth, h = pointCard.offsetHeight;
|
| 536 |
let left = px + 14;
|
| 537 |
if (left + w > window.innerWidth - 8) left = px - w - 14;
|
|
@@ -544,12 +593,16 @@ function showPointCard(entry, c, element) {
|
|
| 544 |
}
|
| 545 |
function hidePointCard() {
|
| 546 |
pcSticky = false;
|
|
|
|
| 547 |
pointCard.classList.remove('visible');
|
| 548 |
pointCard.setAttribute('aria-hidden', 'true');
|
| 549 |
}
|
| 550 |
-
function scheduleHide() { if (
|
| 551 |
|
| 552 |
function handleChartHover(evt, elements, c) {
|
|
|
|
|
|
|
|
|
|
| 553 |
if (elements && elements.length) {
|
| 554 |
const entry = entryFromElement(elements[0], c);
|
| 555 |
if (entry) { pcSticky = false; showPointCard(entry, c, elements[0].element); return; }
|
|
|
|
| 105 |
padding: 12px 14px; min-width: 248px; max-width: 320px;
|
| 106 |
border-radius: 4px;
|
| 107 |
opacity: 0; visibility: hidden; transform: translateY(2px);
|
| 108 |
+
transition: opacity 0.16s ease-out, transform 0.16s ease-out;
|
| 109 |
pointer-events: none;
|
| 110 |
}
|
| 111 |
.point-card.visible { opacity: 1; visibility: visible; transform: none; pointer-events: auto; }
|
|
|
|
| 482 |
const pointCard = document.getElementById('pointCard');
|
| 483 |
let pcHideTimer = null;
|
| 484 |
let pcSticky = false;
|
| 485 |
+
let cardAnchor = null; // screen coords of the dot the card points at
|
| 486 |
+
const cursor = { x: -1, y: -1 };
|
| 487 |
+
// Track the pointer everywhere (capture phase, so it updates before Chart's
|
| 488 |
+
// own hover handler runs) β the "safe corridor" test below needs to know
|
| 489 |
+
// whether the cursor is travelling from the dot toward the card.
|
| 490 |
+
document.addEventListener('mousemove', e => { cursor.x = e.clientX; cursor.y = e.clientY; }, true);
|
| 491 |
+
|
| 492 |
+
// Convex hull (monotone chain) + point-in-polygon. Together they form a
|
| 493 |
+
// forgiving hover corridor between the anchored dot and the card: while the
|
| 494 |
+
// cursor is inside it, the card stays put and other dots can't hijack it β so
|
| 495 |
+
// the user can actually reach the card and click its links.
|
| 496 |
+
function convexHull(pts) {
|
| 497 |
+
const p = pts.slice().sort((a, b) => a.x - b.x || a.y - b.y);
|
| 498 |
+
if (p.length < 3) return p;
|
| 499 |
+
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
| 500 |
+
const lower = [];
|
| 501 |
+
for (const pt of p) { while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], pt) <= 0) lower.pop(); lower.push(pt); }
|
| 502 |
+
const upper = [];
|
| 503 |
+
for (let i = p.length - 1; i >= 0; i--) { const pt = p[i]; while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pt) <= 0) upper.pop(); upper.push(pt); }
|
| 504 |
+
lower.pop(); upper.pop();
|
| 505 |
+
return lower.concat(upper);
|
| 506 |
+
}
|
| 507 |
+
function pointInPolygon(pt, poly) {
|
| 508 |
+
let inside = false;
|
| 509 |
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
| 510 |
+
const xi = poly[i].x, yi = poly[i].y, xj = poly[j].x, yj = poly[j].y;
|
| 511 |
+
if (((yi > pt.y) !== (yj > pt.y)) && (pt.x < (xj - xi) * (pt.y - yi) / (yj - yi) + xi)) inside = !inside;
|
| 512 |
+
}
|
| 513 |
+
return inside;
|
| 514 |
+
}
|
| 515 |
+
function cardVisible() { return pointCard.classList.contains('visible'); }
|
| 516 |
+
// True while the cursor is over the (padded) card or inside the hull spanning
|
| 517 |
+
// from the anchored dot to it β i.e. plausibly on its way to the card.
|
| 518 |
+
function inSafeCorridor() {
|
| 519 |
+
if (!cardVisible() || !cardAnchor) return false;
|
| 520 |
+
const r = pointCard.getBoundingClientRect();
|
| 521 |
+
const pad = 14;
|
| 522 |
+
if (cursor.x >= r.left - pad && cursor.x <= r.right + pad && cursor.y >= r.top - pad && cursor.y <= r.bottom + pad) return true;
|
| 523 |
+
const corners = [
|
| 524 |
+
{ x: r.left - pad, y: r.top - pad }, { x: r.right + pad, y: r.top - pad },
|
| 525 |
+
{ x: r.right + pad, y: r.bottom + pad }, { x: r.left - pad, y: r.bottom + pad },
|
| 526 |
+
];
|
| 527 |
+
return pointInPolygon(cursor, convexHull([cardAnchor, ...corners]));
|
| 528 |
+
}
|
| 529 |
|
| 530 |
function buildPointCardHtml(e) {
|
| 531 |
const info = AGENTS[e.agent] || null;
|
|
|
|
| 556 |
|
| 557 |
const linkBtns = [];
|
| 558 |
if (e.filename) linkBtns.push(`<a href="${escapeHtml(submissionHref(e.filename))}" target="_blank" rel="noopener noreferrer">Submission β</a>`);
|
| 559 |
+
// The artifacts dir holds the run's code, so surface it as "Code" for clarity.
|
| 560 |
+
(e.links || []).forEach(l => {
|
| 561 |
+
const label = l.label.replace(/^Artifacts/i, 'Code');
|
| 562 |
+
linkBtns.push(`<a href="${escapeHtml(l.href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)} β</a>`);
|
| 563 |
+
});
|
| 564 |
|
| 565 |
return `${head}<div class="pc-score"><span class="big">${fmt2(e.score)}</span> tok/s ${verified}</div><div class="pc-rows">${rowsHtml}</div>${note}<div class="pc-links">${linkBtns.join('')}</div>`;
|
| 566 |
}
|
|
|
|
| 580 |
const rect = c.canvas.getBoundingClientRect();
|
| 581 |
const px = rect.left + element.x;
|
| 582 |
const py = rect.top + element.y;
|
| 583 |
+
cardAnchor = { x: px, y: py };
|
| 584 |
const w = pointCard.offsetWidth, h = pointCard.offsetHeight;
|
| 585 |
let left = px + 14;
|
| 586 |
if (left + w > window.innerWidth - 8) left = px - w - 14;
|
|
|
|
| 593 |
}
|
| 594 |
function hidePointCard() {
|
| 595 |
pcSticky = false;
|
| 596 |
+
cardAnchor = null;
|
| 597 |
pointCard.classList.remove('visible');
|
| 598 |
pointCard.setAttribute('aria-hidden', 'true');
|
| 599 |
}
|
| 600 |
+
function scheduleHide() { if (pcSticky) return; clearTimeout(pcHideTimer); pcHideTimer = setTimeout(hidePointCard, 300); }
|
| 601 |
|
| 602 |
function handleChartHover(evt, elements, c) {
|
| 603 |
+
// Cursor is over the card or travelling toward it β keep the current card,
|
| 604 |
+
// and don't let an intervening dot swap it out from under the user.
|
| 605 |
+
if (inSafeCorridor()) { clearTimeout(pcHideTimer); return; }
|
| 606 |
if (elements && elements.length) {
|
| 607 |
const entry = entryFromElement(elements[0], c);
|
| 608 |
if (entry) { pcSticky = false; showPointCard(entry, c, elements[0].element); return; }
|