A board of figures that is also a catalogue. πίναξ (Ancient Greek) — "tablet / catalogue / register"; the Pinakes were the catalogue of the Library of Alexandria.
Pinax turns the figures and tables your analysis scripts produce into a structured, self-contained catalogue of a computational study — described once and rendered three ways:
- HTML gallery — sections as cards, a responsive figure grid, markdown + KaTeX math, cross-references, citations, and an interactive comment layer;
- LaTeX → PDF — the same manuscript as a typeset document;
agent.json— a machine-readable view (figures as data tables, sections, captions) that an LLM or downstream tool can read.
It generalizes the hand-written build_report page an analysis pipeline grows over time: you
describe the manuscript once with a small DSL, point each figure at the value (or data key) it
plots, and render writes the artifact.
pkg> add PinaxRequires Julia v1.12+.
using Pinax, Plots
plot_energy() = plot(0.1:0.05:2, β -> 1 / β; xlabel = "β", ylabel = "E/N", legend = false)
@page :results "Results" begin
@section :energy "Energy" begin
@desc md"Energy density $E/N$ versus inverse temperature $\beta$."
@figure plot_energy() # any Plots/Makie figure — captured lazily
@caption "χ-convergence"
end
end
render(out = "site") # -> site/index.html + assets/figures/…/energy_fig1.svgThen preview it, or render the same document another way:
serve("site") # preview over HTTP
render(theme = :latex, out = "pdf") # -> pdf/document.tex -> PDF
render(theme = :agent, out = "agent") # -> agent/agent.json (machine-readable)@figure captures its expression lazily, so figures are computed (and cached) only when you
render. Sections become cards; figures lay out in a responsive grid; $…$ math is rendered by
KaTeX.
theme |
output | for |
|---|---|---|
:gallery (default) |
self-contained HTML | humans browsing results |
:latex |
LaTeX → PDF | manuscripts / sharing |
:agent |
agent.json |
LLMs / downstream tooling |
The same source — sections, @desc/@caption, @table, citations, a @benchmark's PASS/FAIL
verdict — flows to every face. Themes are pluggable: render(; theme = MyTheme()),
register_theme!(:mine, MyTheme()), or theme = "path/to/mytheme.jl".
report(vault, recipe; title, out) discovers a finished sweep's results, hands each (key, dict)
pair to a project-specific recipe that builds the doc, and renders both the gallery and
agent.json — so the same results become a human notebook and an LLM-readable artifact in one call.
This is an experimental feature. On Julia 1.13+ the
Pkg.testdelegation (Pinax.test()with no argument) is unavailable: it installs a root testset that has to survive into the suite the preamble runs afterwards, and 1.13 moved Test's testset stack to aScopedValue, which cannot outlive the block that sets it. It warns once and produces no report; the suite runs and its verdict is unchanged.Pinax.test("test/runtests.jl")is unaffected: it wraps the suite in a@testset, which needs no such thing.
A test suite reports one bit: green or red. A @test isapprox(E, oracle; rtol=1e-3) computed E,
the reference and the tolerance, then threw all three away. Pinax.test renders the suite instead —
one page per test file, each check carrying the margin it passed by.
The suite is not touched. There is no Pinax macro to add to it; the suite stays plain
@testset / @test, and the one Pinax touch is the call:
using Pinax, Test
Pinax.test() # delegate to an unmodified `Pkg.test`, and render a report
Pinax.test("test/runtests.jl") # render a specific suite in the current processPinax.test() injects a -L preamble that installs a capturing root before the suite runs and
renders at exit; Pkg.test still does all the sandbox and dependency work. A bare Pkg.test()
installs no root and produces no report, so switching the report on cannot regress a passing suite.
A red suite always fails the process — a report must never turn a failing suite green.
A test file becomes a page, a nested @testset a section, and each @test a Check with its real
got / want / tol. @pinaxignore drops a testset from the document while still running and
counting it.
Sharded CI uses the same primitive: with PINAX_TEST_DUMP a run dumps its testset tree as TOML
instead of rendering, and render_test_report(dumps; out) merges the dumps into one gallery whose
pages are the test files — the shard boundary never appears in the output. Publishing that report
alongside the Documenter docs is #68.
render(theme = :agent) emits an agent.json an LLM can read. clients/pinax-mcp
is a Node MCP server over that artifact: it serves every unit (figure / table / section) by id and
presents a figure as its underlying data table — npx pinax-mcp --agent <render-out>. See its
README.