Files
landing_page/qdrant-landing/themes/qdrant-2024/assets/js/viz.js
T

170 lines
6.7 KiB
JavaScript

/*
* viz.js — progressive enhancement for build-time chart SVGs.
*
* The SVG is generated at build time and is complete without this file: every
* number is already in the markup, so scrapers, feed readers and no-JS visitors
* lose nothing. This only adds hover affordances on top.
*
* Interaction model borrowed from the reference charts we benchmarked against:
* - full-height invisible hit zones, so you hover the COLUMN, not the bar
* - one shared cursor-following tooltip, positioned imperatively
* - "highlight, don't reorder": nothing dims, nothing moves. The hovered
* series just gains an outline. Dimming the rest makes a chart feel like it
* is hiding data from you.
* - the same key highlights across every panel, so a config you hover in the
* throughput chart also lights up in the latency chart
*/
(function () {
var figures = document.querySelectorAll('[data-viz]');
if (!figures.length) return;
var tip = document.createElement('div');
tip.className = 'viz-tip';
tip.setAttribute('role', 'status');
tip.setAttribute('aria-live', 'polite');
document.body.appendChild(tip);
function move(e) {
var pad = 14;
var w = tip.offsetWidth;
var h = tip.offsetHeight;
var x = e.clientX + pad;
var y = e.clientY + pad;
if (x + w > window.innerWidth - 8) x = e.clientX - w - pad;
if (y + h > window.innerHeight - 8) y = e.clientY - h - pad;
tip.style.left = Math.max(8, x) + 'px';
tip.style.top = Math.max(8, y) + 'px';
}
function show(e, title, rows, fig) {
if (fig.closest('.qdrant-blog-post')) tip.setAttribute('data-viz-theme', 'light');
else tip.removeAttribute('data-viz-theme');
var html = '<div class="viz-tip__title">' + title + '</div>';
for (var i = 0; i < rows.length; i++) {
html += '<div class="viz-tip__row">'
+ (rows[i].c ? '<span class="viz-tip__dot" style="background:' + rows[i].c + '"></span>' : '')
+ '<span class="viz-tip__k">' + rows[i].k + '</span>'
+ '<b class="viz-tip__v">' + rows[i].v + '</b></div>';
}
tip.innerHTML = html;
tip.classList.add('is-on');
move(e);
}
function hide() { tip.classList.remove('is-on'); }
// Each figure is wired in isolation. Without this, a throw while wiring one
// figure aborts the whole forEach and silently kills hover on every figure
// after it — the failure would be invisible, since the SVG still renders.
//
// No load timeout here, unlike the island loader: nothing in this file is
// async. The SVG is already in the document and this only attaches listeners,
// so there is no pending state a timeout could rescue.
Array.prototype.forEach.call(figures, function (fig) {
try {
wireFigure(fig);
} catch (err) {
// Leave the chart exactly as rendered: static, correct, just not hoverable.
if (window.console && console.warn) console.warn('viz: figure not wired', err);
}
});
function wireFigure(fig) {
wireToggle(fig);
var zones = fig.querySelectorAll('[data-viz-zone]');
// Line charts carry a dashed vertical rule that snaps to the hovered
// x-column, so the eye can read every series at the same x.
function setCrosshair(zone) {
var lines = fig.querySelectorAll('[data-viz-crosshair]');
for (var i = 0; i < lines.length; i++) lines[i].setAttribute('opacity', '0');
if (!zone) return;
var x = zone.getAttribute('data-viz-x');
var panel = zone.getAttribute('data-viz-panel');
if (x === null || panel === null) return;
var line = fig.querySelector('[data-viz-crosshair="' + panel + '"]');
if (!line) return;
line.setAttribute('x1', x);
line.setAttribute('x2', x);
line.setAttribute('opacity', '1');
}
function setActive(key) {
// Zones carry a key too, but they are invisible hit targets — ringing
// them would draw a tall box around the whole column.
var marks = fig.querySelectorAll('[data-viz-key]:not([data-viz-zone])');
Array.prototype.forEach.call(marks, function (m) {
// Highlight across every panel in this figure, not just the hovered one.
if (key !== null && m.getAttribute('data-viz-key') === key) {
m.setAttribute('data-viz-active', '');
} else {
m.removeAttribute('data-viz-active');
}
});
}
Array.prototype.forEach.call(zones, function (z) {
try {
wireZone(z);
} catch (err) {
if (window.console && console.warn) console.warn('viz: zone not wired', err);
}
});
function wireZone(z) {
var key = z.getAttribute('data-viz-key');
var title = z.getAttribute('data-viz-title') || '';
var rows;
try { rows = JSON.parse(z.getAttribute('data-viz-rows') || '[]'); } catch (err) { rows = []; }
function enter(e) { setActive(key); setCrosshair(z); show(e, title, rows, fig); }
z.addEventListener('pointerenter', enter);
z.addEventListener('pointermove', move);
z.addEventListener('pointerleave', function () { setActive(null); setCrosshair(null); hide(); });
// Keyboard parity: the zones are focusable, so tabbing reads the same rows.
z.addEventListener('focus', function () {
setActive(key);
setCrosshair(z);
var r = z.getBoundingClientRect();
show({ clientX: r.left + r.width / 2, clientY: r.top }, title, rows, fig);
});
z.addEventListener('blur', function () { setActive(null); setCrosshair(null); hide(); });
}
fig.addEventListener('pointerleave', function () { setActive(null); setCrosshair(null); hide(); });
}
function wireToggle(fig) {
var sw = fig.querySelector('[data-viz-toggle]');
if (!sw) return;
var btns = sw.querySelectorAll('[data-viz-toggle-btn]');
var views = fig.querySelectorAll('[data-viz-view]');
if (!btns.length || !views.length) return;
var cap = fig.querySelector('[data-viz-captions]');
var captions = null;
if (cap) {
try { captions = JSON.parse(cap.getAttribute('data-viz-captions')); } catch (e) { captions = null; }
}
function select(idx) {
Array.prototype.forEach.call(views, function (v) {
v.style.display = v.getAttribute('data-viz-view') === String(idx) ? '' : 'none';
});
// The caption states a claim about the numbers on screen.
if (captions && captions[idx]) cap.textContent = captions[idx];
Array.prototype.forEach.call(btns, function (b) {
b.setAttribute('aria-pressed',
b.getAttribute('data-viz-toggle-btn') === String(idx) ? 'true' : 'false');
});
hide();
}
Array.prototype.forEach.call(btns, function (b) {
b.addEventListener('click', function () {
select(b.getAttribute('data-viz-toggle-btn'));
});
});
}
})();