65 lines
2.5 KiB
JavaScript
65 lines
2.5 KiB
JavaScript
// 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';
|
|
});
|
|
}
|