Skip to content

Add math in labels (lookup tier + opt-in Typst tier) - #88

Draft
gabo515 wants to merge 8 commits into
Psy-Fer:devfrom
gabo515:feat/typst-math
Draft

Add math in labels (lookup tier + opt-in Typst tier)#88
gabo515 wants to merge 8 commits into
Psy-Fer:devfrom
gabo515:feat/typst-math

Conversation

@gabo515

@gabo515 gabo515 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Description

Adds math in labels — any label (axis titles, plot title, TextPlot body) 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:

  • Lookup tier (render::math::to_unicode) — always compiled, zero deps. Lowers math to inline Unicode (Greek, operators, super/subscripts, \fraca/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 $.
  • Typst tier (feature 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 typst markup backend (feature typst): emits a CETZ-based .typ document for external typst compile, sharing the $...$→Typst-math translation with the math tier.

math is deliberately excluded from full — the Typst compiler is a ~200-crate tree; ordinary builds and cargo ci-test stay lean. Enable explicitly: --features math,png. Bundles only NewCM Math (~1 MB) + reuses the existing DejaVu Sans, not the 15 MB typst-assets.

Note: Typst math is not LaTeX — a multi-letter run like mc is one identifier, so write $E = m c^2$. (Documented in the reference page.)

Type of change

  • New feature / API addition

Checklist

Library

  • src/render/math.rsto_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 cache
  • src/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.rsTypstBackend markup emitter
  • src/fonts.rs — bundled NewCM Math (gzip); fonts module gated on math too
  • Cargo.tomlmath/typst features; math pulls flate2, excluded from full

Tests

  • 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 + markdown
  • tests/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 usvg
  • tests/typst_basic.rs — markup backend incl. escaping regression
  • cargo test --features cli,full — all suites pass (lookup-tier path)

CLI

  • No new subcommand/flag — math is transparent via existing --x-label / --y-label / --title; no man-page change
  • scripts/smoke_tests.sh — 7 lookup-tier checks (scatter/line/bar) + 3 Typst-tier checks (gated on --math-bin)
  • scripts/terminal_plots.sh — math-labels entry

Documentation

  • docs/src/reference/math.md — two tiers, supported set, Typst≠LaTeX, embedded SVG examples
  • examples/math.rsdocs/src/assets/math/*.svg (8 committed assets); scripts/gen_docs.sh wired (--features full,math)
  • docs/src/SUMMARY.md — reference link

Housekeeping

  • CHANGELOG.md[Unreleased] → Added

The typst markup backend (zero-dep) and the math feature (Typst-as-library) are bundled here per discussion. Builds + clippy clean across cli,full / math,png,pdf / math alone; doc assets generated against current dev.

@gabo515

gabo515 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author
image

@Psy-Fer

Psy-Fer commented Jun 11, 2026

Copy link
Copy Markdown
Owner

So this is really 3 things in 1

  1. A unicode shorthand, for things like \sqrt and \frac or getting greek letters, that kind of thing.
  2. typst math layer, that links the typst compiler to do math in axis labels and annotations. It currently can't do plot body text (like in the textPlot type)
  3. typst backend, to emit a .typ file to add the plot to get compiled in their own doc pipeline.

my thoughts:

  1. The unicode shorthand is great. This is an excellent addition.
  2. pulling in 195 libraries for around 2% of what those libraries can do, for some axis labels, is a LOT of extra baggage. Not sure that's worth it, even with the feature gate.
  3. to too worth if 2 isn't included. in that case a user would just do #image("fig.svg") which is already what would be done. So this is linked to 2.

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)

cetz_version being pinned is good, because it's gone through a lot of breaking changes. But this also highlights why i'm hesitant, given the maintenance for this feature going forward.

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,
James

@gabo515

gabo515 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

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,
Gabe

@gabo515

gabo515 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

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:

  • Good catches on Path and Clip — both real. Primitive::Path is currently a TODO in the typst backend (so arrowheads in quiver/network vanish) and ClipStart/ClipEnd are silently dropped. I'll add both on this branch before it's ever un-drafted.
  • One thought on the coupling between (2) and (3): the typst backend is actually the zero-crate path to real typeset math — the user's own typst compile does the typesetting, and $...$ regions pass through as native Typst math with fonts matching the surrounding document, which #image("fig.svg") can't give you. So if the in-process math feature stays out (totally fair on the maintenance argument — cetz/typst churn is real), the backend might still earn its place on its own once Path/Clip are in. No urgency either way.

Cheers!

@gabo515
gabo515 marked this pull request as draft June 11, 2026 18:45
Psy-Fer added a commit that referenced this pull request Jun 12, 2026
## 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)
@Psy-Fer

Psy-Fer commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Okay, typst might have legs again for the pdf feature. I just updated kuva to use krilla, and figured out how to handle higher MSRV without bumping kuva (using docs). If we keep typst behind the pdf feature, i'm happy to bring all the features you want.

Wanna draft up/alter the PR for this using the latest dev changes?

Rebased onto the krilla-era dev: the zero-dep lookup tier already merged
via Psy-Fer#89 and evolved there, so this commit now adds only the typst work.

- feature `math`: links the Typst compiler as a library and typesets any
  label containing $...$ into an SVG fragment (SVG backend) or pixmap
  (PNG), embedded at the label position with color injection and
  per-label ID namespacing. Compile failure falls back to the lookup
  tier with a one-time warning per distinct label.
- PDF: math fragments flow through the krilla pipeline unchanged
  (SvgBackend -> usvg -> krilla-svg draw_svg) — the xlink:href->href
  rewrite is exactly what usvg needs; no pdf.rs changes required.
- feature `typst` (zero deps, now in `full`): TypstBackend emits a
  CETZ-based .typ document for external `typst compile`; $...$ regions
  pass through as native Typst math.
- bundles NewCM Math (~0.75 MB gzip) instead of the 15 MB typst-assets;
  fonts module gains newcm_math() behind the `math` gate.
Per James's proposal on Psy-Fer#88: krilla (the pdf backend since dev@e855bf5)
is Typst's own PDF stack, so `pdf` was already the crate's one
deliberately heavy, higher-MSRV feature. The typst math tier now rides
it instead of being a separate `math` feature:

- Cargo.toml: `pdf` pulls typst/typst-svg/typst-render/typst-library;
  the `math` feature is gone. `full` (and cargo ci-test) now compile
  the typst tree.
- MSRV: no floor change — typst 0.14 needs 1.89, under krilla's 1.92
  documented in [package.metadata.msrv] pdf_feature. The pdf-msrv CI
  job now covers typst implicitly; comments updated.
- cfg gates and prose flip feature="math" -> feature="pdf" across
  backends, fonts, render::math, and the math test suites (which now
  run under cargo ci-test instead of needing a separate invocation).
- smoke_tests.sh: --math-bin machinery removed — the standard cli,full
  binary now exercises the typst tier directly.
- PDF embedding needs no code: math fragments flow through
  SvgBackend -> usvg -> krilla-svg draw_svg unchanged (verified by
  tests/math_pdf.rs).
Closes the two gaps James flagged in the June review of Psy-Fer#88:

- Primitive::Path (arrowheads in quiver/network, chord ribbons, sankey
  flows, venn outlines) now emits CETZ merge-path calls. Kuva's path
  vocabulary is closed (absolute M/L/C/Q/A/Z): quadratics are promoted
  to cubics, arcs converted endpoint->center per SVG F.6.5 and split
  into <=90-degree cubic approximations. Opacity maps to
  transparentize(), dasharrays to Typst dash tuples.
- ClipStart/ClipEnd now clip: the element stream is split into chunks
  at clip boundaries; each chunk renders as a #place'd canvas, and
  clipped chunks sit inside box(clip: true) at the clip rect with the
  inner canvas shifted back so coordinates stay page-absolute. Scenes
  without clips keep the old single-canvas output shape.
- Every canvas now opens with an invisible full-page rect, anchoring
  CETZ's auto-fitted bounding box to the page so absolute positions
  hold even when no drawing touches the page corners.

All emitted documents (scatter, quiver, clip, math) verified to
compile with typst 0.14 CLI and visually inspected as PNG.
Closes the TextPlot gap from the June review of Psy-Fer#88 ("can't do plot
body text"). Design: inline fragment splicing — kuva keeps its own
markdown parsing and word-wrapping; each $...$ in a body compiles to a
typst fragment that flows through the line-breaker as one unbreakable
word. Surrounding text stays real, selectable text in SVG/PDF output.

- TextSpan gains a `math` flag; parse_inline_markup splits spans at
  $...$ boundaries under `pdf` (without it, lookup-tier lowering is
  unchanged).
- wrap_rich_spans sizes a math word by its typeset width in
  mean-char units, tracks flush adjacency so punctuation after math
  ("$x^2$.") stays tight, and never breaks inside a fragment.
- Lines with fragments taller than the text line grow their leading by
  the ascent/descent overshoot, so stacked fractions never collide
  with neighbouring lines.
- SVG backend lays math-bearing lines out manually (bundled-font
  metrics for text runs, fragment width for math) and embeds fragments
  at the shared baseline; raster blits pixmaps in its pen loop;
  terminal always lowers math spans to inline Unicode.
- render_label_svg is now memoized per process (each fragment is
  needed twice: wrapping metrics + draw; axis labels repeat across
  re-renders).
- Fragments advance like words: the 0.3em safety margin baked into the
  fragment page (FRAGMENT_MARGIN_EM) is subtracted from the inline
  advance and the embed starts a margin early.

Verified visually in PNG (raster), SVG (resvg), and PDF (krilla):
wrapping, bold+math mixing, tall-fragment leading, punctuation
adjacency all correct.
- docs/reference/math.md: restructured around the two tiers — lookup
  tables stay as the zero-dep reference; new Typst-tier section covers
  the pdf feature, compile-failure fallback, the not-LaTeX identifier
  rule, and TextPlot body splicing (with a new committed asset).
- examples/math.rs: regenerated all assets with --features full, so
  the committed SVGs now show typeset math (the docs say so
  explicitly). Fixed two typst-hostile bodies that would have silently
  fallen back to the lookup tier: E = mc^2 -> m c^2, 4ac -> 4 a c.
  Added a TextPlot body-splice example.
- CHANGELOG: body-splicing entry; TypstBackend entry now records full
  primitive coverage (Path via merge-path, clip via box(clip: true)).
page.frame.baseline() returns size.y (the frame height) when unset,
which is always the case for page frames. Using it directly placed the
fragment's bottom edge at the surrounding text baseline, so every
formula floated visibly above the line it was spliced into.

The real baseline lives on the inner paragraph-line frame nested inside
the page (has_baseline() is true there). Walk the frame tree, take the
topmost line-baseline position, add up y offsets on the way.

Assets regenerated: math now sits on the text baseline in wrapped
TextPlot bodies and in labels; before/after diff is visible on every
committed math SVG.
The previous scene put both circles fully inside the clip region, so
test_outputs/typst_clip.typ rendered as two dots with a bottom label —
technically a correct primitive-emission check, visually useless as a
clip demo. Add a large blue circle centered on the clip's left edge so
half is clipped away; keep the small red circle fully inside as a
sanity control. Test still asserts on the primitive emission.
Rustfmt-clean these files so PR review isn't cluttered by post-merge
fmt drift (James's project-wide rustfmt pass on 2026-05-04 established
the current style; new work should land fmt-clean).
@gabo515

gabo515 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Excuse the delay, I have been quite busy with work.

@Psy-Fer

Psy-Fer commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Haha yea same.

Thanks, I'll look at this soon.

James

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants