- Data now comes from a calendar - Hovering in charts shows day info - Optical enhancements to chart rendering - Allow setting/showing goal weight - Allow toggeling between absolute and zoomed in weight axis
75 lines
2.9 KiB
JavaScript
75 lines
2.9 KiB
JavaScript
// Fitness dashboard range dropdowns and "absolute" checkboxes: changing one
|
|
// reloads the page with that chart's query parameter updated. Plain GET
|
|
// navigation — each view is distinct and bookmarkable, so no
|
|
// postReplace/history handling is needed.
|
|
document.addEventListener('change', function (e) {
|
|
var url = new URL(window.location.href);
|
|
var sel = e.target.closest('[data-fitness-range]');
|
|
var abs = e.target.closest('[data-fitness-absolute]');
|
|
if (sel) {
|
|
url.searchParams.set(sel.dataset.fitnessRange, sel.value);
|
|
} else if (abs) {
|
|
if (abs.checked) url.searchParams.set(abs.dataset.fitnessAbsolute, '1');
|
|
else url.searchParams.delete(abs.dataset.fitnessAbsolute);
|
|
} else {
|
|
return;
|
|
}
|
|
window.location.href = url.toString();
|
|
});
|
|
|
|
// 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';
|
|
});
|