mirror of
https://github.com/qdrant/landing_page.git
synced 2026-09-26 14:38:30 +02:00
170 lines
6.7 KiB
JavaScript
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'));
|
|
});
|
|
});
|
|
}
|
|
})();
|