* 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>
* Replace the text processing diagram with an interactive island
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Add diagram to the 'BM25 Text Processing' section too
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every chart's spec lived in one shared data/viz-charts.json array, keyed by an
id that was already the path to its files. Each chart is now self-contained:
assets/viz/<id>.{json,csv,svg}, one folder holding everything about it.
Because the id is the path, a spec no longer repeats it or the data path, and
the shortcode resolves a chart directly instead of scanning the array.
Drops three keys that were never decisions. width was 980 on all seven and now
lives in data/viz.json alongside the legend spacing, where the generator and
the shortcode read the same number. extraHeight was the legend's own offset
leaking out of the generator into both the spec and the viewBox, so it is
derived from the chart kind. Tooltip units come from the column suffix.
232 lines become 187 across 7 files, while adding the title that 5 charts were
missing. chart.markdown.md reads that title, so those five had no heading above
their table in the Markdown output.
Parses CSVs with d3-dsv instead of split(','), which would shift every column
right on a label containing a comma. Already installed as a Plot dependency;
now declared.
Every SVG is byte-identical and all 6 chart pages render identical HTML. The
only rendered change is the 5 restored Markdown headings.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The AnchorJS link icon already updated the address bar; it now also
copies the absolute URL and shows a "Copied" toast at the bottom of
the screen.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(build): publish each page's markdown at /path.md as well as /path/index.md
Hugo writes the Markdown output format beside the HTML it belongs to, so a page
at /articles/foo/ is published at /articles/foo/index.md. That is correct and it
stays: it is what the llms.txt convention prescribes for directory-style URLs,
it is the form /llms.txt already advertises for 670 pages, and it is the target
of the <link rel="alternate" type="text/markdown"> we emit in every page head.
What it is not is guessable. Every other documentation site I compared exposes a
page's markdown by swapping the extension, so a client holding only a URL can
construct /articles/foo.md directly instead of first fetching the HTML to read
the alternate link:
/articles/immutable-data-structures.md 404 -> 200 text/markdown
/articles/immutable-data-structures/index.md 200 200 (unchanged)
So publish both. A post-build step copies every public/**/index.md to
public/**.md; the originals are untouched, so no published URL changes. Netlify
already serves .md as text/markdown, and the files are small: 7.6 MB against a
1.4 GB build, 0.5%.
Deliberately a build step rather than a Netlify redirect. A splat has to be
terminal, so /articles/*.md is not a pattern Netlify accepts, and a rule that
cannot be verified locally is worse than a copy that can.
Adds npm run md:test, which builds the site, runs the script over the output and
checks all 789 aliases exist and match their source byte for byte, that the
canonical index.md and index.html are untouched, that nothing is written outside
the publish directory, and that a second run overwrites nothing. Verified the
tests fail when the script is stubbed out.
The test deletes its build directory on exit. A build is ~1.4 GB, and a suite
that leaves temp directories behind fills a disk faster than anyone notices.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(md): cut the alias tests down, and keep master's npm scripts
Review: the tests were redundant. Two of the four asserted what cp does, and
all four needed a full ~1.4 GB Hugo build to say it. Replaced with two tests
over a synthetic directory, covering the only two things the script decides:
skip the site root, never overwrite an existing file. 87 lines to 39, and the
run drops from a full site build to 0.26s.
package.json: this branch predates the charts work, so its scripts block
replaced master's. Merging as-is would have silently deleted viz:test and
viz:charts. Now keeps both and adds md:test.
Also trims the script's comment header from 19 lines to 8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(md): drop the npm script, run the test file directly
package.json is the site's manifest; a runner alias for one shell script does
not belong in it. The test runs as:
node --test test/markdown/aliases.test.mjs
package.json is now untouched by this branch, which also removes the risk of
the merge dropping master's viz scripts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
7 charts across 5 posts, in 3 kinds: two-panel columns, grouped columns and faceted lines. Replaces 1,024 KB of raster screenshots with 71 KB of inline SVG, hover and keyboard focus included.
Charts are generated from CSVs and committed, so a changed number shows up as a diff in review.
Pages also build as Markdown, where the shortcode emits the source data as a table instead of the raw SVG.
Joint article with Matin Mahmood of hyper3labs on hyperbolic embeddings for hierarchical data, and what it takes to serve them from Qdrant.
Covers the Poincare ball, a controlled comparison on the Google Product Taxonomy, the d+2 lift failing inside HNSW, and the prefetch plus Formula Query rescore that fixes it. Two figures are live against the collection: the taxonomy viewer and the CLIP comparison.