mirror of
https://github.com/qdrant/landing_page.git
synced 2026-09-25 14:08:30 +02:00
* 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>
123 lines
6.6 KiB
JavaScript
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');
|
|
});
|