Files
landing_page/qdrant-landing/test/viz/shortcodes.test.mjs
T
78beef7e01 feat(viz): Introduce chart toggles (#2782)
* feat(viz): switch a chart between views

Adds `views` to a chart spec: a segmented control above the chart that
switches which measure it draws. Modelled on the charts in the author's own
blog, which use the same control for the same job.

Every view is rendered into the same committed SVG, one <g> each, and all but
the first are inline-hidden. Switching shows one and hides the rest, so nothing
re-renders and nothing is fetched on click. With JavaScript off a reader still
gets a complete chart rather than dead buttons.

A view is a patch over the base spec, so it can change the column plotted, the
axis, the title and the y-max.

Applies it to oversampling/recall, whose CSV already carried ndcg_at_10 while
the chart only drew recall_at_10 -- the second measure was in the table but
invisible in the picture. The default view is byte-identical to the chart
published today, so the page looks the same until a reader picks the other tab.

The Markdown output is unchanged and carries every view's columns, since it
prints the whole CSV.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(viz): the caption has to follow the view

Caught this in a screenshot: the oversampling caption cites recall going 0.605
to 0.988, and it stayed put when the reader switched to the nDCG view, where
every bar sits between 0.2786 and 0.3238. A caption is a claim about the
numbers on screen, so leaving it fixed makes the figure lie.

Each view after the first takes caption2=, caption3= and so on, alongside the
existing caption= at the call site, where the rest of the prose lives. The
build fails if a view has no caption rather than letting it inherit one.

The figcaption is also the figure's accessible name, so it changes for screen
readers too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* style(viz): white label on the active toggle, and fewer comments

The active segment took its colour from --qi-surface, which flips to near-black
in dark mode, so the label sat dark-on-red. Neither surface clears AA on the
accent anyway (4.30 light, 3.73 dark); white gives 4.77. Adds --qi-accent-ink
for text sitting on the accent.

Also cuts the comments this branch added from 21 lines to 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(viz): Markdown output carries every view's claim

The table is the whole CSV, so for a chart with views it holds every view's
columns at once. The title and caption were still view 0's, so the .md read
"Recall@10 by quantization" over a table that also carried nDCG.

Now emits one caption per view, labelled, and the oversampling title no longer
names a single measure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Trigger CI

* refactor(viz): call the view picker a toggle, not a switch

`.qi-switch` in islands.scss is already a binary on/off control with a
track and a knob. The chart's control picks one of N views, so it was
borrowing a name that meant something else.

viz-switch -> viz-toggle, data-viz-view-btn -> data-viz-toggle-btn,
wireSwitch -> wireToggle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(viz): resolve a chart's arguments in one place

chart.html and chart.markdown.md both derived the id, the caption, the
spec and the per-view captions, from eleven identical lines. The rules had
already drifted: the HTML output refused a view missing its caption2=,
while the Markdown output printed an empty claim for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(viz): refuse a per-chart width rather than stretch the chart

Charts deliberately share one width: the SVG scales to the column, so a
wider viewBox renders identical font sizes smaller. The manifest spread a
spec over that default, so a spec setting "width" drew at its own size
inside the 980-wide viewBox the shortcode builds, and came out stretched.
No chart sets one today; this makes the next one a build error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(viz): check each chart's CSV against the table in its post

The same numbers live twice, in the CSV a chart is drawn from and in the
markdown table above it. Nothing compared them, so correcting one and not
the other publishes a chart that disagrees with its own table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Abdon Pijpelink <abdon.pijpelink@qdrant.com>
2026-09-24 19:30:08 +05:30

123 lines
6.6 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { getFixtureHtml, getFixtureMarkdown, everyBuiltMarkdownFile } from './helpers.mjs';
test('viz.json defines a palette and is valid JSON', () => {
const viz = JSON.parse(readFileSync('data/viz.json', 'utf8'));
assert.ok(Array.isArray(viz.palette.categorical), 'palette.categorical must be an array');
assert.ok(viz.palette.categorical.length >= 4, 'need at least 4 categorical colors');
assert.match(viz.palette.categorical[0], /^#[0-9a-f]{6}$/i);
assert.ok(viz.surface.ink, 'surface.ink required');
assert.ok(viz.type.family, 'type.family required');
});
test('figure wrapper renders caption, role and aria-labelledby', () => {
const html = getFixtureHtml();
assert.match(html, /<figure class="viz-figure"/, 'figure wrapper missing');
assert.match(html, /role="img"/, 'svg must carry role="img"');
// The accessible name points at the VISIBLE figcaption. Deliberately not an
// SVG <title>: that renders as the browser's own tooltip on hover, floating
// over the chart's metrics tooltip and repeating the caption underneath it.
assert.match(html, /aria-labelledby="viz-cap-smoke"/,
'aria-labelledby must point at the figcaption id');
assert.doesNotMatch(html, /<title id="viz-/,
'no SVG <title> — it raises a native browser tooltip over the chart');
assert.match(html, /<figcaption class="viz-figure__caption" id="viz-cap-smoke">Smoke test caption\.<\/figcaption>/,
'caption must be visible and carry the id the svg points at');
});
test('no data colour is duplicated into the stylesheet', () => {
const viz = JSON.parse(readFileSync('data/viz.json', 'utf8'));
const scss = readFileSync('themes/qdrant-2024/assets/css/viz.scss', 'utf8').toLowerCase();
const colors = [...viz.palette.categorical, ...Object.values(viz.surface)]
.filter((v) => typeof v === 'string' && v.startsWith('#'));
for (const c of colors) {
assert.ok(!scss.includes(c.toLowerCase()),
`${c} is a data colour from viz.json and must not be repeated in viz.scss`);
}
});
test('chart chrome uses the shared --qi-* tokens, not its own', () => {
const scss = readFileSync('themes/qdrant-2024/assets/css/viz.scss', 'utf8');
assert.ok(scss.includes("@import 'qi-tokens'"),
'viz.scss must pull tokens from the shared partial islands.scss also uses');
assert.ok(!/--viz-(ink|muted|grid|surface|border)\b/.test(scss),
'the old chart-only --viz-* tokens should be gone');
const gen = readFileSync('scripts/viz/generate-charts.mjs', 'utf8');
assert.ok(!/var\(--viz-/.test(gen), 'the generator must emit --qi-* tokens');
});
test('dark chrome follows the system preference, not only the toggle', () => {
const scss = readFileSync('themes/qdrant-2024/assets/css/viz.scss', 'utf8');
assert.match(scss, /@media \(prefers-color-scheme: dark\)/,
'a reader on a dark OS who never touched the toggle must still get dark chrome');
assert.match(scss, /html\[data-theme='dark'\]/, 'the explicit toggle must still win');
});
test('Markdown output carries the chart data, not the drawing', () => {
const md = getFixtureMarkdown();
// The whole point: an .md reader must never receive SVG. Pages build in both
// HTML and Markdown, so without layouts/shortcodes/chart.markdown.md the HTML
// template serves both and pastes ~26KB of gridline coordinates into a
// document whose only audience is LLMs and markdown readers.
assert.doesNotMatch(md, /<svg/, 'no SVG may reach the Markdown output');
assert.doesNotMatch(md, /<figure|viz-figure|data-viz/, 'no figure chrome either');
// Site-wide, not just this page: any future shortcode that renders a visual
// has to make the same choice, and a per-page assertion would not notice.
const leaks = everyBuiltMarkdownFile().filter((f) => /<svg/.test(readFileSync(f, 'utf8')));
assert.deepEqual(leaks, [], 'these .md files leak SVG into the Markdown output');
// What replaces it is the chart's own source CSV, so the table cannot drift
// from the picture: both are generated from assets/viz/hybrid/fusion.csv.
assert.match(md, /\| dataset \| method \| ndcg \| delta \|/, 'header row missing');
assert.match(md, /\| --- \| --- \| --- \| --- \|/, 'separator row missing');
assert.match(md, /\| SciFact \| hybrid \(RRF\) \| 0\.7175 \| \+0\.0289 \|/,
'data row missing or reformatted');
// Title and caption still carry the claim the chart was making.
assert.match(md, /\*\*nDCG@10: each retriever alone versus RRF fusion\*\*/, 'title missing');
assert.match(md, /_Fixture chart caption\._/, 'caption missing');
});
test('HTML output still renders the chart as SVG', () => {
// Guards the other direction: the Markdown variant must not shadow the HTML one.
const html = getFixtureHtml();
assert.match(html, /viz-figure--chart/, 'chart figure missing from HTML');
assert.match(html, /<svg class="viz-figure__svg"/, 'chart SVG missing from HTML');
assert.match(html, /data-viz-zone/, 'hover hit zones missing from HTML');
});
test('a chart with views renders a toggle and ships every view', () => {
const html = getFixtureHtml();
// One button per view, the first one pressed.
assert.match(html, /<div class="viz-toggle"[^>]*role="group"/, 'toggle container missing');
const btns = html.match(/data-viz-toggle-btn="\d"/g) || [];
assert.equal(btns.length, 2, 'expected one button per view');
assert.match(html, /data-viz-toggle-btn="0"\s+aria-pressed="true"/, 'first view must start pressed');
assert.match(html, /data-viz-toggle-btn="1"\s+aria-pressed="false"/, 'later views must start unpressed');
// Every view is in the SVG already: toggling hides and shows, it never fetches.
const views = html.match(/data-viz-view="\d"/g) || [];
assert.equal(views.length, 2, 'expected every view inlined in the svg');
// Hidden inline rather than by class, so extra views stay hidden with no CSS,
// and via display because browsers ignore `hidden` on SVG elements.
assert.match(html, /data-viz-view="1" style="display:none"/,
'views after the first must be inline-hidden');
assert.doesNotMatch(html, /data-viz-view="0" style="display:none"/,
'the first view must render without JavaScript');
});
test('the view toggle is a drawing affordance, not data', () => {
// The Markdown output carries the CSV, which already holds every view's
// columns, so a toggle there would be buttons with nothing to toggle.
const md = getFixtureMarkdown();
assert.doesNotMatch(md, /viz-toggle|data-viz-view/, 'no toggle markup in Markdown output');
assert.match(md, /\| engine \| config \| recall_at_10 \| throughput_qps \|/,
'Markdown must carry every view column');
});