// Fitness dashboard chart hover. The range dropdowns and "absolute" // checkboxes are htmx (assets/fitness/main.html): they swap in only their // chart. // Boosted navigation re-runs this script on every visit; the listeners // are delegated to document, so register them once. if (!window.fitnessWired) { window.fitnessWired = true; // Hover tooltip: the pointer snaps to the data point nearest in time. Dots // are sorted by x, so a binary search finds it; the tooltip sits above the // point and is clamped to the chart panel. function fitnessNearest(dots, x) { var lo = 0, hi = dots.length - 1; while (lo < hi) { var mid = (lo + hi) >> 1; if (dots[mid].cx.baseVal.value < x) lo = mid + 1; else hi = mid; } if (lo > 0 && x - dots[lo - 1].cx.baseVal.value < dots[lo].cx.baseVal.value - x) lo--; return dots[lo]; } function fitnessHide(svg) { var chart = svg.closest('.fitness-chart'); svg.classList.remove('is-hover'); chart.querySelector('.fitness-tip').hidden = true; } document.addEventListener('pointermove', function (e) { var svg = e.target.closest && e.target.closest('.fitness-svg'); document.querySelectorAll('.fitness-svg').forEach(function (other) { if (other !== svg) fitnessHide(other); }); if (!svg) return; var dots = svg.querySelectorAll('.chart-dot'); if (!dots.length) return; var ctm = svg.getScreenCTM(); if (!ctm) return; var pt = new DOMPoint(e.clientX, e.clientY).matrixTransform(ctm.inverse()); var dot = fitnessNearest(dots, pt.x); var cx = dot.cx.baseVal.value, cy = dot.cy.baseVal.value; var cursor = svg.querySelector('.chart-cursor'); cursor.setAttribute('x1', cx); cursor.setAttribute('x2', cx); var focus = svg.querySelector('.chart-focus'); focus.setAttribute('cx', cx); focus.setAttribute('cy', cy); svg.classList.add('is-hover'); var chart = svg.closest('.fitness-chart'); var tip = chart.querySelector('.fitness-tip'); tip.firstElementChild.textContent = dot.dataset.date; tip.lastElementChild.textContent = dot.dataset.value; tip.hidden = false; var box = chart.getBoundingClientRect(); var at = new DOMPoint(cx, cy).matrixTransform(ctm); var left = at.x - box.left - tip.offsetWidth / 2; left = Math.max(0, Math.min(left, box.width - tip.offsetWidth)); var top = at.y - box.top - tip.offsetHeight - 10; if (top < 0) top = at.y - box.top + 10; tip.style.left = left + 'px'; tip.style.top = top + 'px'; }); }