Add math in labels (lookup tier + opt-in Typst tier)#88
Conversation
Any label may embed `$...$` math (LaTeX-ish syntax). Rendering has two tiers
that share `$`-detection and the command→symbol table (`src/render/math.rs`):
Lookup tier — always compiled, zero dependencies:
- `to_unicode` lowers math to inline Unicode: Greek, operators, super/sub
(all-or-nothing, falling back to a clean `^(2q)` when a glyph is missing),
`\frac{a}{b}`→`a/b`, `\sqrt{x}`→`√(…)`. Never emits a stray `\` or `$`.
- Baseline for every backend; the only tier the terminal can use. SVG, PNG,
PDF, and terminal all route plain `$...$` labels through it.
Typst tier — feature `math`, opt-in:
- Links the Typst compiler as a library (minimal `World`: bundled fonts, no
package/network access) and typesets the *whole label* (text + math) into
one SVG fragment (SVG/PDF) or pixmap (PNG), embedded into kuva output.
Real 2-D math: stacked fractions, radicals, large operators with limits.
- Whole-label rendering means no fontdue text measurement and no per-segment
layout — `svg_math` just places one fragment; raster blits/rotate-blits one
pixmap. Math color follows the label color (injected into the Typst source).
- On compile failure (e.g. Typst's `mc` ≠ `m c`) the label degrades to the
lookup tier with a one-time warning.
- Bundles only New Computer Modern Math (~1 MB, gzipped) + reuses the bundled
DejaVu Sans for text, instead of the ~15 MB `typst-assets`. Excluded from
`full` so `cargo ci-test` and example builds stay lean; enable explicitly,
e.g. `--features math,png`.
Also adds the `typst` markup backend (`TypstBackend`): emits a CETZ-based
`.typ` document for external `typst compile`, sharing the `$...$` → Typst-math
translation with the `math` tier.
Tests: exhaustive exact-match units on the lookup tier (`render::math`);
structural tests on the typst tier (SVG embed, PNG composite incl. rotated
y-labels, PDF via usvg, color injection). Docs: Reference → Math in Labels.
|
So this is really 3 things in 1
my thoughts:
A few other issues I found in review. Arrow heads as a path primitive would get dropped through the typst backend path so no arrow heads in network, quiver, or other plots that have arrows. It also drops the Clip primitive (less of a big deal but could make for some surprises)
Overall, I think the unicode shorthand should absolutely be pulled into kuva asap. That's a great feature and it already works everywhere including text plots based on how it's implemented. I don't think the typst related stuff is quite there yet, mostly in issues with typst itself than your PR of the feature. (there was a reason I hadn't done it yet myself, besides time). What do you think about what I have said? Perhaps splitting this PR into 2 parts, the unicode shorthand and the typst math layer and backend, and then we can come back to the typst stuff later. Cheers, |
|
Hey James, Thanks for the feedback! Honestly, yea I think I bit off a bit more than I could chew. It felt incomplete with just the lookup but yea the typst stuff is quite heavy for little usability. I am happy to split this into two PRs with just the look up stuff and then everything else typst. Best, |
|
Agreed on all counts — and the split falls out naturally since the two tiers were designed to be independent. The unicode shorthand is now its own PR: #89. Zero deps, no feature flag, no Cargo.toml changes at all. I'm parking the typst work on this branch and converting it to draft. Two notes for whenever we come back to it:
Cheers! |
## Summary This is the **unicode shorthand half** of #88, split out per the review discussion there — the typst math layer and typst backend stay parked on the other branch for later. Any label (title, axis labels, `TextPlot` markdown bodies) may embed `$...$` math written in LaTeX-ish syntax. Math regions are lowered to inline Unicode by every backend, including the terminal: | Input | Output | |-------|--------| | `$\sigma^2$` | σ² | | `$x_i$` | xᵢ | | `$a \leq b \cdot c$` | a ≤ b · c | | `$\frac{a+b}{c}$` | (a+b)/c | | `$\sqrt{x^2+y^2}$` | √(x²+y²) | | `$\sum_{i=1}^{n} x_i$` | ∑ᵢ₌₁ⁿ xᵢ | **Zero dependencies, no feature flag, always on.** No `Cargo.toml` changes at all. ## Design notes - **Inline only, by design.** Fractions and radicals lower to `a/b` and `√(…)`, never stacked 2-D layout — the output is plain text that flows anywhere a label can go (rotated y-axis titles, terminal character grids, markdown bodies). - **All-or-nothing super/subscripts.** `x^{2n}` → x²ⁿ, but if any character in the group lacks a Unicode form (`q`, most capitals), the whole group falls back to a clean `x^(2q)` — never a half-substituted mix. - **Never leaks source.** The lowering never emits a stray `\` or `$`. Literal dollars are written `\$`; an unclosed `$` is left untouched as ordinary text. - **One module, four call sites.** `src/render/math.rs` holds detection (`contains_math`), segmentation, the command table, and `to_unicode`. The SVG, raster (PNG/PDF), and terminal `Text` arms lower at draw time; markdown `TextSpan`s are lowered once after markup parsing, so `**bold** $\sigma^2$` works in `TextPlot` bodies. ## Supported syntax Greek letters (incl. `\varepsilon`/`\varphi` variants), operators/relations/arrows (`\pm \times \cdot \leq \neq \approx \in \partial \nabla \infty \to \degree` …), `\frac{a}{b}`, `\sqrt{x}` / `\sqrt[3]{x}` (→ ³√x), and `^`/`_` scripts with `{...}` groups or braceless `\command` operands (`x^\alpha` → `x^(α)`). ## Testing - **21 unit tests** in `render::math` — substitution, fallbacks, escaped/unclosed dollars, nested chains (quadratic formula end-to-end), no-leak guarantees. - **`tests/math_lookup.rs`** — per-backend integration: terminal, SVG, markdown `TextPlot` body, escaped-dollar literal. - **7 smoke tests** across plot types (scatter/line/bar) and label slots (title, x/y labels, rotated y-label). - **`scripts/terminal_plots.sh`** — math labels entry for visual terminal inspection. - `cargo ci-test` (99 suites), `cargo ci-clippy`, and the full smoke suite (190 checks) all pass. ## Docs - New reference page *Reference → Math in Labels* with the substitution table, supported-syntax list, CLI usage, and generated example SVGs (`examples/math.rs`, committed under `docs/src/assets/math/`). - Crate-level docs section in `lib.rs`; CHANGELOG entry. 🤖 Generated with [Claude Code](https://claude.com/claude-code)

Description
Adds math in labels — any label (axis titles, plot title,
TextPlotbody) may embed$...$math (LaTeX-ish:$\sigma^2$,$\frac{a}{b}$,$\sqrt{x}$). Two tiers that share$-detection and the symbol table, diverging only on structure:render::math::to_unicode) — always compiled, zero deps. Lowers math to inline Unicode (Greek, operators, super/subscripts,\frac→a/b,\sqrt→√(…)). Baseline for every backend and the only tier the terminal can use. All-or-nothing super/subscript fallback (x^(2k)when no Unicode form exists); never emits a stray\or$.math) — links the Typst compiler as a library (no external binary) and typesets the whole label into an SVG fragment / pixmap embedded in SVG/PNG/PDF. Real 2-D math: stacked fractions, radicals, large operators. Math color follows the label color; compile failure falls back to the lookup tier with a one-time warning.Also lands the
typstmarkup backend (featuretypst): emits a CETZ-based.typdocument for externaltypst compile, sharing the$...$→Typst-math translation with themathtier.mathis deliberately excluded fromfull— the Typst compiler is a ~200-crate tree; ordinary builds andcargo ci-teststay lean. Enable explicitly:--features math,png. Bundles only NewCM Math (~1 MB) + reuses the existing DejaVu Sans, not the 15 MBtypst-assets.Type of change
Checklist
Library
src/render/math.rs—to_unicode(lookup tier),to_typst_math, shared$-detection + symbol table;#[cfg(feature="math")]Typst tier (render_label_svg/render_label_pixmap), whole-label compile, no cachesrc/backend/{svg,raster,terminal}.rs— math routing (typst tier → embed/blit; else lookup tier → normal text). Terminal is lookup-only.src/backend/svg_math.rs— fragment placement + per-label ID namespacing (avoids duplicate-ID SVG with multiple math labels)src/backend/typst.rs—TypstBackendmarkup emittersrc/fonts.rs— bundled NewCM Math (gzip);fontsmodule gated onmathtooCargo.toml—math/typstfeatures;mathpullsflate2, excluded fromfullTests
render::math— 23 exact-match lookup-tier unit tests (Greek, operators, frac, sqrt, all-or-nothing super/sub, nested chains, braceless command, quadratic-formula full chain)tests/math_lookup.rs— lookup tier through SVG + terminal + markdowntests/math_smoke.rs/math_png.rs/math_pdf.rs— Typst-tier SVG embed (unique IDs, color injection), PNG composite incl. rotated y-labels, PDF via usvgtests/typst_basic.rs— markup backend incl. escaping regressioncargo test --features cli,full— all suites pass (lookup-tier path)CLI
--x-label/--y-label/--title; no man-page changescripts/smoke_tests.sh— 7 lookup-tier checks (scatter/line/bar) + 3 Typst-tier checks (gated on--math-bin)scripts/terminal_plots.sh— math-labels entryDocumentation
docs/src/reference/math.md— two tiers, supported set, Typst≠LaTeX, embedded SVG examplesexamples/math.rs→docs/src/assets/math/*.svg(8 committed assets);scripts/gen_docs.shwired (--features full,math)docs/src/SUMMARY.md— reference linkHousekeeping
CHANGELOG.md—[Unreleased]→ AddedThe
typstmarkup backend (zero-dep) and themathfeature (Typst-as-library) are bundled here per discussion. Builds + clippy clean acrosscli,full/math,png,pdf/mathalone; doc assets generated against currentdev.