Skip to content

feat(julia): add Julia + Makie.jl as the third language (Phase 5) #7612

@MarkusNeusinger

Description

@MarkusNeusinger

Summary

Add Julia as anyplot's third language, with Makie.jl (CairoMakie backend for headless PNG rendering) as its first library entry. This is Phase 5 of docs/concepts/library-expansion.md — the second non-Python runtime — and it should reuse and generalize every piece of plumbing we built for R/ggplot2 in #6944 + the follow-ups #6947 / #6961 / #7066.

This is a meta / infrastructure issue, not a spec request. Do not apply the spec-request label — there is no plot type to draft. The work follows the feat(ggplot2): add R/ggplot2 library + multi-language pipeline shape, not the spec-create.yml flow.

Phase 1+2 (JavaScript / Chart.js / D3 / ECharts / Highcharts-JS) is still listed as planned in docs/concepts/library-expansion.md. Adding Julia before JS skips a phase on the roadmap — call this out in the PR description but ship anyway: the multi-language pipeline is already proven on R, and the value of a second non-Python runtime is mostly validating (not blocking) the JS work.


Why Makie.jl

Per docs/concepts/library-expansion.md §5:

| Julia | Makie.jl | ~45 % | Phase 5. Distinct scientific stack; not a wrapper. |
| Julia | Plots.jl | ~40 % | Alternative; pick Makie unless visitor data flips. |

  • CairoMakie is the right backend for CI — pure-Cairo, headless, no GPU, no GLFW window. PNG/SVG/PDF out of the box. (GLMakie / WGLMakie are interactive backends that we don't need for static gallery rendering.)
  • Distinct scientific stack — not a thin wrapper around plotly.js or Vega — so it earns its own entry under the "most-used variant" rule in §6.
  • License: MIT.
  • Excluded by the rule: Plots.jl is the alternative; we ship Makie unless Plausible data later shows the audience flips. PlotlyJS.jl is a wrapper around plotly.js (already covered via Python Plotly entry).

Reference: how R/ggplot2 was integrated

The relevant PR chain — read these before starting to understand the surface area and the bugs that surfaced after merge:

PR Title Why it matters
#6944 feat(ggplot2): add R/ggplot2 library + multi-language pipeline The big bang. Library + language registry, R runtime in CI, all five workflows made language-aware, frontend syntax-highlighting, tests. 45 files, +1110 / -179.
#6947 chore(ggplot2): follow-up Copilot review fixes after #6944 merge PlotOfTheDayTerminal runner-token, plot-generator.md docstring rule, impl-review.yml header rewrite prepend fallback.
#6961 fix(frontend+api): wire R/ggplot2 into code viewer, libraries page, languages count get_impl_code accepts ?language=, LIBRARIES array, LIB_ABBREV, languages count in stats, language-aware cache keys. The "always-going-to-need-a-follow-up" PR.
#7066 fix(debug): preserve language in plot links so R/ggplot2 deep-links work DebugPage hardcoded 'python', RecentActivity payload, SpecStatusItem column, coverage formula, LIB_TO_LANG constant.

Goal for Julia: roll all four PRs' lessons into one PR (or one PR + one tiny follow-up). Specifically: every if language == "python" / "r" branch must become a generic python | r | julia dispatch, every hardcoded 9 / 10 library count must already use LIBRARIES.length / len(SUPPORTED_LIBRARIES), and the DebugPage / LIB_TO_LANG / ?language= plumbing must be exercised end-to-end before the PR is opened.


Scope

1. Library + language registry (core/constants.py)

  • SUPPORTED_LANGUAGES: add "julia"frozenset(["python", "r", "julia"]).
  • LANGUAGES_METADATA: append the Julia entry. Suggested values:
    {
        "id": "julia",
        "name": "Julia",
        "file_extension": ".jl",
        "runtime_version": "1.11",   # confirm against the version we install in CI
        "documentation_url": "https://julialang.org",
        "description": "High-level, high-performance dynamic language for technical computing. Combines the productivity of Python/R with the speed of compiled languages; popular in scientific computing, numerical analysis, and machine learning research.",
    },
  • SUPPORTED_LIBRARIES: add "makie" (alpha-sorted, between letsplot and matplotlib).
  • LIBRARIES_METADATA: append the Makie entry. Suggested values (verify on first CI run):
    {
        "id": "makie",
        "name": "Makie.jl",
        "language_id": "julia",
        "version": "0.22",            # pin to whatever the Project.toml resolves to
        "documentation_url": "https://docs.makie.org",
        "description": "High-performance, extensible visualization ecosystem for Julia. CairoMakie renders publication-quality static PNG/SVG/PDF; GLMakie/WGLMakie handle interactive use. anyplot uses CairoMakie for the static gallery.",
    },
  • INTERACTIVE_LIBRARIES: leave makie out. CairoMakie produces PNG only — same model as matplotlib / seaborn / plotnine / ggplot2. (GLMakie / WGLMakie are out of scope: they need a display server or a JS runtime.)

2. Julia runtime in CI

  • New composite action .github/actions/setup-julia/action.yml — mirror of .github/actions/setup-r/action.yml. Steps:
    1. julia-actions/setup-julia@v2 with the pinned major.minor (1.11). Dependabot will SHA-pin on first run (same policy as r-lib actions — see chore(ggplot2): follow-up Copilot review fixes after #6944 merge #6947 §"Deliberately not in this PR").
    2. julia-actions/cache@v2 to cache the depot — Julia precompilation is slow on cold start.
    3. julia --project=. -e 'using Pkg; Pkg.instantiate()' to restore from the committed Project.toml / Manifest.toml.
    4. Smoke test: render a tiny CairoMakie PNG and assert the file exists + has non-zero bytes (same shape as the ggplot2 smoke test in setup-r/action.yml).
  • Pin packages in a top-level Project.toml + Manifest.toml (sibling of renv.lock):
    • CairoMakie, Makie, DataFrames, CSV, Colors, ColorSchemes, RDatasets (gives us iris, mtcars, diamonds, gapminder cross-language-compatibly), PalmerPenguins, Random, Statistics.
    • Same reproducibility model as renv.lock: the Manifest.toml is the lockfile; commit both. Document the snapshot expectations in a # Notes / open follow-ups section of the PR description (renv.lock had the same caveat).
  • System deps: Cairo is bundled in Cairo_jll so apt should not be needed, but mirror the apt fallback from setup-r for libfreetype6-dev / libfontconfig1-dev / libpng-dev to be safe against future runner-image changes.

3. Workflows (extension- and language-aware)

For every file below, the R/ggplot2 PR introduced a case "$LIBRARY" in ggplot2) LANGUAGE=r; EXT=.R ;; *) LANGUAGE=python; EXT=.py ;; esac step. Refactor that into a single shared shell function or a tiny Python helper sourced by all workflows, so adding the next language is a one-line table edit. At minimum, every existing case must grow a makie) LANGUAGE=julia; EXT=.jl ;; arm. Files to update:

  • .github/workflows/impl-generate.yml — add makie to the library choices; add Julia setup step (conditional on language == "julia"); add Julia version detection (julia -e 'print(VERSION)'); add Julia package-version detection for Makie (julia --project=. -e 'using Pkg; println(Pkg.project().dependencies["Makie"].version)' or equivalent).
  • .github/workflows/impl-repair.yml — same derive-lang step; conditional Julia setup; conditional Python plotting deps suppression for julia branch.
  • .github/workflows/impl-review.yml — extend the per-library canvas/dpi hint dictionary with a Makie entry (CairoMakie uses save(path, fig; px_per_unit=2) or resolution on Figure); make the header-rewrite step's prepend fallback (added in chore(ggplot2): follow-up Copilot review fixes after #6944 merge #6947) work for .jl files using # comments.
  • .github/workflows/impl-merge.yml — branch-name parser already derives LANGUAGE + EXT from LIBRARY; just add the makie) arm to the same case statement.
  • .github/workflows/bulk-generate.yml — add makie to library choices and to ALL_LIBRARIES.
  • .github/workflows/daily-regen.yml — sanity-check it still works after the matrix grows by one.

4. Prompts

  • New prompts/library/makie.md — analogous to prompts/library/ggplot2.md. Cover:

    • File extension .jl, runtime julia (not python).
    • No workarounds clause (don't shell out to Python / R / matplotlib).
    • Static PNG only via CairoMakie. NOT_FEASIBLE for primary-interactivity specs.
    • Imports: using CairoMakie, Makie, DataFrames, Colors, ColorSchemes, Random, Statistics. Optional dataset packages: RDatasets, PalmerPenguins.
    • Reproducibility: Random.seed!(42).
    • Theme-adaptive chrome tokens via ENV["ANYPLOT_THEME"] (mirror of the ggplot2 ANYPLOT_THEME switch).
    • Save canvas: hard rule on output size (mirror of ggplot2 §"Canvas — hard rule, no deviation"). For Makie, save("plot-light.png", fig; px_per_unit=…) plus an explicit Figure(; resolution=(3200, 1800)) (landscape) or (2400, 2400) (square). Pick whichever combo of resolution × px_per_unit lands cleanly on those exact pixel sizes — pre-render gate in impl-review.yml rejects anything off by more than 16 px.
    • Forbidden patterns (Julia equivalents of the ggplot2 list): no Plots.jl, no shelling out, no display(fig) instead of save(...), no @show, etc.
    • Header style: # comment block at the top of every .jl file mirroring the Python/R conventions (# Library: Makie.jl 0.22 | Julia 1.11 etc.).
  • prompts/plot-generator.md — three small edits:

    1. The lead sentence ("Most anyplot libraries are Python … ggplot2 is R") gets extended to mention Makie/Julia.
    2. The "Available environment" block — append the Julia stack alongside Python and R.
    3. The "Forbidden" sections — add a Julia / Makie block mirroring the R / ggplot2 block.
  • prompts/quality-evaluator.md + prompts/workflow-prompts/ai-quality-review.md:

    • Extend the ${EXT} legend (.py | .R | .jl) and the language allow-list ({python, r, julia}).
    • Extend the AR-08 (fake interactivity) static-libraries list — Makie via CairoMakie is static, so it joins matplotlib / seaborn / plotnine / ggplot2.
  • prompts/workflow-prompts/impl-generate-claude.md + impl-repair-claude.md:

    • The (language, ext, runner) table grows a julia | .jl | julia --project=. row.
    • The Rscript {LIBRARY}.R example pair gets a sibling ANYPLOT_THEME=light julia --project=. {LIBRARY}.jl / …=dark … block.
  • prompts/workflow-prompts/impl-similarity-claude.md — the {language} is python / r mapping in the parenthetical needs a third arm for julia.

5. Frontend (app/src/)

This is the section that took two follow-up PRs to finish for R (#6961, #7066). Get it right in one pass this time.

  • constants/index.ts:

    • LIBRARIES: insert 'makie' in alpha order (between letsplot and matplotlib).
    • LIB_ABBREV: add makie: 'mk' (or mki — pick whichever the design wants).
    • LIB_TO_LANG: add makie: 'julia'.
    • LANG_DISPLAY: add julia: 'Julia'.
    • LANG_EXT: add julia: 'jl'.
  • components/CodeHighlighter.tsx:

    • Import julia from react-syntax-highlighter/dist/esm/languages/prism/julia.
    • SyntaxHighlighter.registerLanguage('julia', julia).
    • PRISM_LANGUAGE map: julia: 'julia'.
    • TS-types blocker: the @types/react-syntax-highlighter package didn't declare prism/r and we shipped that gap (see fix(frontend+api): wire R/ggplot2 into code viewer, libraries page, languages count #6961 "Out of scope"). Fix it for Julia in the same PR — either via declare module '…/prism/julia' or by patching the project-local ambient types so we don't carry forward two unresolved TS7016 imports.
  • components/CodeHighlighter.test.tsx — add a case rendering using CairoMakie under language="julia".

  • components/LibraryCard.tsx — add a one-line description for makie (mirror of the ggplot2 entry).

  • components/MastheadRule.tsxjulia: { open: '#=', close: '=#' } is already there. Verify it actually gets used after this change lands.

  • components/PlotOfTheDay.tsx + PlotOfTheDayTerminal.tsx — the runner token (python / Rscript) added in chore(ggplot2): follow-up Copilot review fixes after #6944 merge #6947 grows a julia --project=. arm. Same for the ext switch.

  • pages/DebugPage.tsxSpecStatusItem interface adds a makie column. The matrix grid repeat(…, 40px) is supposed to use LIBRARIES.length (per fix(debug): preserve language in plot links so R/ggplot2 deep-links work #7066) — verify, don't hardcode 11.

  • pages/LibrariesPage.tsx — meta-description string ("Ten plotting libraries across Python and R …") bumps to "Eleven plotting libraries across Python, R, and Julia …".

  • Tests — every *.test.tsx that hardcoded 9 / 10 or enumerated libraries / languages needs the same bump.

6. Backend (api/ + core/)

Per the lessons of #6961 / #7066:

7. Tags / Plausible / SEO

  • docs/reference/plausible.md — line 305 already lists julia as a future language token (python, future: julia, r, matlab). Promote it from "future" to "current" once this PR lands. No new event types needed: every existing language / library event slot already accepts arbitrary strings.
  • docs/reference/seo.md §"Adding a New Language" already documents the exact procedure (Library.language = "julia", sitemap auto-picks up /scatter-basic/julia/makie, optional julia.anyplot.ai subdomain). Walk through each step and confirm there's nothing left to wire — and if a julia.anyplot.ai subdomain is on the table, it's a separate small PR (mirror of the python.anyplot.ai nginx block).
  • docs/reference/tagging-system.md — no schema change needed; tags are language-agnostic. Verify nothing hardcodes "python or r" in narrative text.

8. Tests

  • tests/unit/core/test_constants.pyTestSupportedLanguages (added in feat(ggplot2): add R/ggplot2 library + multi-language pipeline #6944) gets a Julia assertion. language_id integrity test covers Makie automatically. Bump any explicit length expectations from 1011 libraries and 23 languages.
  • tests/unit/api/test_debug.py, test_routers.py, test_stats.py, test_schemas.py — verify all len(SUPPORTED_LIBRARIES) / len(SUPPORTED_LANGUAGES) references still pass without per-library / per-language edits (this is the pattern feat(ggplot2): add R/ggplot2 library + multi-language pipeline #6944 introduced; new tests added since then may have regressed it — fix in place, don't add noqa suppressions).
  • tests/unit/api/mcp/test_tools.py — same.
  • Frontend: useCodeFetch.test.ts gains a Julia case (mirror of the ggplot2 case added in fix(frontend+api): wire R/ggplot2 into code viewer, libraries page, languages count #6961 — separate cache keys per language for same library_id, ?language=julia appended for non-python). CodeHighlighter.test.tsx, LibraryCard.test.tsx, ImageCard.test.tsx, DebugPage.test.tsx — Julia row / Julia language string.

9. Documentation

  • docs/concepts/library-expansion.md — update §1 ("Current state" table grows a Julia row; library count goes from 10 to 11), §8 (Phase 5 status: shipped), §9 (record the decision to ship Makie over Plots.jl). Note the phase-skip vs. the original roadmap (we shipped Julia before JS): one paragraph in §9 explaining why is enough.
  • docs/reference/repository.md — the plots/{spec}/implementations/ tree gets a julia/ sibling alongside python/ and r/.
  • docs/reference/style-guide.md — the §3.8 "pipeline story" / "catalogue story" copy ("a thousand examples across ten libraries in two languages") gets bumped to "eleven libraries in three languages". The §1 plot-palette intro ("10 libraries (9 Python + R/ggplot2)") gets bumped similarly.
  • docs/concepts/vision.md — quick grep for hardcoded counts, update as needed.
  • agentic/docs/project-guide.md — append a Makie entry to the per-library list (the section that already covers plotnine / lets-plot / ggplot2).
  • prompts/README.md — library count.
  • README.md — quick grep for hardcoded library/language counts.

10. Existing R-rollout gaps to not repeat

For each gap the R rollout hit, prove the Julia path doesn't:

Gap (R rollout) Verify for Julia
/api/specs/scatter-basic/ggplot2/code 404'd because language wasn't piped through (#6961) Manually curl /api/specs/scatter-basic/makie/code?language=julia after the first Makie impl lands and verify 200.
Frontend LIBRARIES array silently skipped ggplot2 (#6961) /libraries shows the Makie card; landing strip shows languages: 3.
DebugPage hardcoded 'python' when building plot deep links (#7066) Click a Julia cell in the debug matrix → lands on /{spec}/julia/makie, not /{spec}/python/makie.
RecentActivity payload missing language_id (#7066) Recent-activity for a Julia impl deep-links to the Julia detail page.
SpecStatusItem matrix column missing for ggplot2 (#7066) Matrix grows from 10 to 11 columns (or whatever LIBRARIES.length says) without hardcoded edits.
Hardcoded 9s / * 9 / repeat(9, 40px) (#7066) Grep the codebase once after the PR is up — should find zero hardcoded library counts beyond the test-fixture cases that intentionally pin them.
PlotOfTheDayTerminal hardcoded python runner (#6947) Terminal chip on a Makie POTD reads julia --project=. plots/.../makie.jl (or whatever runner token is chosen).
Python impl-review.yml header rewrite needed prepend fallback (#6947) The Julia branch of the same step uses re.match first, prepends the canonical # header if absent.
@types/react-syntax-highlighter missing prism/r declaration (#6961 "Out of scope") Don't carry this forward — declare or patch the Julia import in the same PR.

Suggested phasing inside this PR

The R PR landed as a single 45-file commit. That's fine for Julia too, but it's also reasonable to split:

  1. PR A (the big one): constants + workflows + setup-julia action + Project.toml + prompts + tests.
  2. PR B (the frontend wire-up): the changes in §5, gated on PR A having merged so the API actually returns Julia impls.
  3. PR C (cleanup): docs sweep, label sync, GCS bucket structure verification, julia.anyplot.ai subdomain if we want one.

If the team has bandwidth for a single combined PR, prefer that — fewer roundtrips with Copilot review, and the "always-going-to-need-a-follow-up" PR class (#6961) shouldn't be inevitable a second time around.


Acceptance criteria

  • core/constants.py: SUPPORTED_LANGUAGES = frozenset(["python", "r", "julia"]), SUPPORTED_LIBRARIES contains "makie", both LANGUAGES_METADATA and LIBRARIES_METADATA are extended.
  • .github/actions/setup-julia/action.yml installs Julia, restores from Project.toml + Manifest.toml, smoke-tests a CairoMakie render.
  • All five workflows (impl-generate, impl-repair, impl-review, impl-merge, bulk-generate) derive LANGUAGE + EXT from LIBRARY via a shared table, and the table contains a makie → (julia, .jl) row.
  • prompts/library/makie.md exists and follows the ggplot2-prompt shape (no-workarounds, NOT_FEASIBLE policy, canvas hard rule, ANYPLOT_THEME handling, forbidden patterns, header style).
  • Frontend renders language="julia" with Julia syntax highlighting and the MastheadRule block-comment tokens (#==#) for the masthead are wired up.
  • gh workflow run impl-generate.yml -f specification_id=scatter-basic -f library=makie produces a passing Julia implementation with plot-light.png + plot-dark.png in GCS.
  • /scatter-basic/julia/makie loads the code panel (no 404 on /api/specs/scatter-basic/makie/code?language=julia) and the rendered preview, in both light and dark mode.
  • /debug matrix has a makie column; clicking a Julia cell deep-links to /{spec}/julia/makie, not the python hub.
  • /libraries lists Makie; /stats reports languages: 3; /sitemap.xml includes /{spec}/julia/makie entries.
  • uv run pytest tests/unit tests/integration is green. Frontend yarn test --run, yarn tsc --noEmit, yarn lint are clean — no @types/react-syntax-highlighter TS7016 carry-forward.
  • docs/concepts/library-expansion.md Phase 5 row marked shipped; library + language counts updated everywhere they're surfaced.
  • Post-merge: uv run python -m automation.scripts.label_manager sync run once so library:makie, generate:makie, impl:makie:{pending,done,failed} labels exist.

Out of scope (separate issues)

  • Phase 1+2 (JavaScript family) — Chart.js / D3 / ECharts / Highcharts-JS. Tracked in docs/concepts/library-expansion.md; not blocked by Julia.
  • Plots.jl as a second Julia library — defer until Plausible data shows Makie traffic and a clear case for a second Julia entry, per the ≥ 3× rule in §6.
  • GLMakie / WGLMakie interactive backends — out of scope; CairoMakie covers the static gallery, and INTERACTIVE_LIBRARIES intentionally excludes Makie.
  • julia.anyplot.ai marketing subdomain — small, optional follow-up modeled on the python.anyplot.ai nginx block in app/nginx.conf.
  • First batch of Makie implementations across existing specs — handled by the regular bulk-generate.yml flow once this PR lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestinfrastructureWorkflow, backend, or frontend issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions