Skip to content

feat(javascript): add JavaScript runtime + render harness + Chart.js, D3.js, ECharts (Phase 1) #8241

@MarkusNeusinger

Description

@MarkusNeusinger

Summary

Add JavaScript as anyplot's fourth language — the highest-impact language gap on the roadmap (~35 % of global charting demand, bigger than R + everything-else combined; docs/concepts/library-expansion.md §2). This issue is the foundation: it stands up the JS runtime + render harness and ships Phase 1 (the three framework-agnostic libraries). It is the first of three issues the JavaScript rollout is split into:

Plotly stays in Python. Per the most-used-variant rule (§6), the Python Plotly binding outweighs plotly.js by ~22×, so there is no separate plotly.js entry. Same for plotly (R) / highcharter (R) — wrappers stay collapsed onto their canonical variant.

This is a meta / infrastructure issue, not a spec request. Do not apply the spec-request label. The work follows the feat(ggplot2) (#6944) and feat(makie) (#7613) shape, not the spec-create.yml flow.

The genuinely new piece. R (Rscript) and Julia (julia) are CLI runtimes that write a PNG directly. JavaScript charting libraries render in a browser DOM — there is no headless CLI that produces a PNG. So this rollout introduces anyplot's first browser-rendered runtime: a Node.js toolchain + a headless-browser render harness. We already screenshot HTML via headless Chrome today (the Python Highcharts/Bokeh impls use selenium + webdriver-manager); this issue generalizes that into a clean, shared Node + Playwright harness so the visible snippet stays idiomatic library code.


Why these three libraries (Phase 1)

Per §3 (share within JS charting demand) and §7 (Tier 1):

Library JS share License Type Framework Why
Chart.js ~26 % MIT Canvas none The default "I need a chart on a webpage." Smallest API surface, highest ROI (Tier 1 #1). Ship/verify this first.
D3.js ~22 % ISC SVG, low-level none Expected by any serious charting site; huge SEO magnet (Tier 1 #2).
ECharts (Apache) ~15 % Apache-2.0 Canvas/SVG none Feature-complete heavyweight; complements Chart.js (Tier 1 #3).

All three are framework: none, plain .js. The two framework/migration cases that add risk live in the sibling issues: Highcharts language migration (#8242) and the MUI X React/TSX path (#8243).

Authoring convention (§6): JS and TS are one language; snippets authored in plain JavaScript. React/Vue/Svelte/Angular are JavaScript with a framework flag — not separate languages (that's why #8243's MUI X is not a new language).


Render strategy (settled — answers "is the screenshot really the best way?")

Decision: one browser-based render harness (Node + Playwright headless Chromium) is the primary render path for all JS libraries. The authored artifact is the HTML page (idiomatic library code mounting into #container); the gallery PNG (light + dark, exact canvas size) is a Playwright screenshot of that page. This is not an extra step bolted onto HTML — it's the same model anyplot already uses for its Python interactive libraries (Plotly via kaleido, Bokeh/Highcharts via headless Chrome): HTML is the deliverable, PNG is derived. Both are required: the PNG for the gallery grid (rendering hundreds of live charts in the grid would kill the page) + og:image/SEO, the HTML for the interactive detail view.

Considered and rejected: per-library headless SSR without a browser (chartjs-node-canvas, echarts SSR, d3 + jsdom + resvg, react-dom/server):

  • jsdom does not implement SVG text measurementgetBBox / getComputedTextLength return 0 — so D3 axes/labels, Highcharts, and MUI X auto-layout produce overlapping/clipped text. Faithful layout needs a real layout engine = a browser.
  • It would mean 3–4 divergent render paths (canvas-SSR vs svg-SSR vs react-SSR) instead of one, each with its own failure modes — the opposite of the "add a library = one-line table edit" goal.
  • Chart.js/ECharts canvas-SSR is clean and fast, but mixing it with the browser path for the SVG libs buys marginal CI speed at the cost of pipeline uniformity. Revisit only if CI render time becomes a bottleneck.

Why Playwright over the existing Selenium path (for the new code): bundled, version-pinned Chromium (no webdriver-manager drift); page.waitForFunction(...) to await a library-emitted render-complete signal instead of time.sleep(5); deviceScaleFactor for exact-pixel canvas sizing against the 3200×1800 / 2400×2400 hard rule.


Reference: how R and Julia were integrated

PR What it taught us
#6944 feat(ggplot2): the big-bang multi-language pipeline — registry, non-Python CI runtime, language-aware workflows, frontend highlighting, tests.
#6947 chore(ggplot2): Copilot follow-ups — runner-token, impl-review.yml header-rewrite prepend fallback.
#6961 fix(frontend+api): ?language=, LIBRARIES, LIB_ABBREV, languages count, language-aware cache keys. The "always-needs-a-follow-up" PR.
#7066 fix(debug): DebugPage hardcoded 'python', RecentActivity payload, SpecStatusItem column, LIB_TO_LANG.
#7613 feat(makie): folded all four R PRs' lessons into one PR. The one-pass discipline to match.

Goal: match #7613's one-pass discipline. Every python | r | julia dispatch becomes python | r | julia | javascript; every hardcoded count already uses len(SUPPORTED_LIBRARIES) / LIBRARIES.length; the DebugPage / LIB_TO_LANG / ?language= plumbing is exercised end-to-end before the PR opens.


Scope

1. Language + library registry (core/constants.py)

  • SUPPORTED_LANGUAGES: add "javascript"frozenset(["python", "r", "julia", "javascript"]).
  • LANGUAGES_METADATA: append:
    {"id": "javascript", "name": "JavaScript", "file_extension": ".js",
     "runtime_version": "22",   # Node 22 LTS — confirm against CI
     "documentation_url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript",
     "description": "The language of the web. Every dashboard, embed, and BI tool renders charts in the browser via JavaScript. anyplot authors snippets in plain JS (TSX for React-only libraries); TypeScript is treated as the same language."}
  • framework metadata field (NEW — §6). Add a "framework" key (default "none") to every LIBRARIES_METADATA entry. Backfill all existing 11 entries with "none"; the three new ones are "none" too. Values: none | react | vue | svelte | angular. This is the field §6 mandates so one javascript entry can later cover React libs (consumed first by feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243) without duplicating the registry.
  • Per-library extension override (NEW). Today LANGUAGE_FILE_EXTENSIONS maps one extension per language and sync_to_postgres discovers by it. JS breaks the 1:1 assumption (Phase-1 libs are .js; feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243's MUI X is .tsx). Add an optional "file_extension" key to LIBRARIES_METADATA (falling back to the language default) and make sync_to_postgres honor it. Phase-1 libs don't need the override (they're .js), but build the mechanism here so feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243 just sets .tsx. Call it out explicitly in the PR — it's the one real schema wrinkle JS adds.
  • SUPPORTED_LIBRARIES: add "chartjs", "d3", "echarts" (alpha-sorted). Suggested metadata (verify versions on first CI run):
    {"id": "chartjs", "name": "Chart.js", "language_id": "javascript", "framework": "none",
     "version": "4.4", "documentation_url": "https://www.chartjs.org",
     "description": "Simple yet flexible HTML5-canvas charting. The most popular open-source JavaScript charting library; eight core chart types, responsive, animated."},
    {"id": "d3", "name": "D3.js", "language_id": "javascript", "framework": "none",
     "version": "7.9", "documentation_url": "https://d3js.org",
     "description": "Data-Driven Documents. The low-level standard for bespoke, SVG-based data visualization on the web — bind data to the DOM and apply data-driven transformations. Maximum control, steep curve."},
    {"id": "echarts", "name": "Apache ECharts", "language_id": "javascript", "framework": "none",
     "version": "5.5", "documentation_url": "https://echarts.apache.org",
     "description": "Powerful, interactive charting and data-visualization library for the browser. Apache-licensed, Canvas/SVG rendering, an enormous catalog of chart types."},
  • INTERACTIVE_LIBRARIES: add chartjs, d3, echarts (HTML-based render path; static PNG for the grid + interactive HTML preview).

2. JavaScript runtime + browser render harness (the new infra)

  • New composite action .github/actions/setup-node/action.yml — sibling of setup-r / setup-julia:
    1. actions/setup-node@v4 (Node 22), cache: 'npm' keyed on the committed package-lock.json.
    2. npm ci — restore the exact locked toolchain.
    3. npx playwright install --with-deps chromium.
    4. Smoke test: render a tiny Chart.js chart to PNG via the harness; assert it exists + non-zero bytes.
  • Pinned toolchain in a top-level package.json + package-lock.json (sibling of Project.toml / renv.lock):
    • Phase-1 libraries: chart.js, d3, echarts. (The siblings add highcharts / @mui/x-charts + React when they land — but pinning all of them here, once, is acceptable and avoids lockfile churn; decide in the PR.)
    • Harness: playwright, plus esbuild (the React/TSX bundling path is wired but unused until feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243).
    • package-lock.json is the lockfile; commit it.
  • Shared render harness automation/js-render/render.mjs (name TBD) — the centerpiece:
    • Keep the visible snippet idiomatic (new Chart(ctx, config), d3.select(...), echarts.init(...)) — not an inline-HTML+selenium blob. The HTML scaffold, library bundle wiring, theme injection, and the Playwright screenshot are the harness's job (like a matplotlib snippet just calling plt).
    • framework=none (all Phase-1 libs): the snippet draws into a known mount node (#container / <canvas> / <svg>); harness wraps it in HTML, loads the pinned bundle, runs under Playwright, awaits render-complete, screenshots at the canvas size.
    • framework=react branch: scaffold it here (esbuild + react-dom/client) but it's first exercised by feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243 — don't block Phase 1 on it.
    • Reads ANYPLOT_THEME (light/dark) and exposes chrome tokens (page bg, text, grid) each library maps — mirror of the ggplot2/Makie ANYPLOT_THEME switch.
  • Canvas hard rule: 3200×1800 landscape / 2400×2400 square. Set Playwright viewport + deviceScaleFactor (and explicit canvas/<svg> dims) to land on exact pixels; the impl-review.yml canvas gate rejects > 16 px deviation.

3. Workflows (extension- and language-aware)

Every workflow already derives (LANGUAGE, EXT) from LIBRARY via a case "$LIBRARY" statement. Add the JS arm:

case "$LIBRARY" in
  ggplot2)                       LANGUAGE=r;          EXT=.R   ;;
  makie)                         LANGUAGE=julia;      EXT=.jl  ;;
  chartjs|d3|echarts)            LANGUAGE=javascript; EXT=.js  ;;
  *)                             LANGUAGE=python;     EXT=.py  ;;
esac

(#8242 adds highcharts to the .js arm; #8243 adds muix → .tsx.)

  • impl-generate.yml — add the three libs to library choices; setup-node step (conditional on language == "javascript"); detect Node + per-library versions from package-lock.json.
  • impl-repair.yml — same derive-lang + conditional setup-node.
  • impl-review.yml — per-library canvas/dpi hints for Chart.js/D3/ECharts; header-rewrite prepend-fallback (chore(ggplot2): follow-up Copilot review fixes after #6944 merge #6947) handles //-prefixed JS comment blocks.
  • impl-merge.yml — add the JS arms; verify the completion total derives from the library list (grows 11 → 14 here).
  • bulk-generate.yml — add the three libs to choices + ALL_LIBRARIES.
  • daily-regen.yml — sanity-check after the matrix grows by three.

Refactor target (from #7612, still open): consolidate the per-workflow case duplication into one shared helper sourced by all workflows. If too big for this PR, keep the case statements byte-identical and note the consolidation as a fast follow-up.

4. Prompts

  • New prompts/library/{chartjs,d3,echarts}.md (makie shape): file extension .js, runtime node; no-workarounds clause; the harness mount-node contract; ANYPLOT_THEME tokens; canvas hard rule; forbidden patterns (no other-lib imports, no network fetches, no CDN <script src> — use the pinned bundle); //-comment header block.
  • prompts/plot-generator.md — extend lead sentence (… "and JavaScript: Chart.js, D3, ECharts"); append the Node/JS stack to "Available environment"; add a JavaScript Forbidden block.
  • prompts/quality-evaluator.md + workflow-prompts/ai-quality-review.md — extend the ${EXT} legend (.py | .R | .jl | .js) and language allow-list ({python, r, julia, javascript}). AR-08: JS libs are genuinely interactive in HTML → they do not join the static-only list; add guidance that the interactive HTML preview is the authoritative artifact.
  • workflow-prompts/impl-generate-claude.md + impl-repair-claude.md(language, ext, runner) table grows a javascript | .js | node … row; add JS run/format sections.
  • workflow-prompts/impl-similarity-claude.md — fourth arm for javascript.

5. Frontend (app/src/)

Get it right in one pass (Julia did).

  • constants/index.ts: LIBRARIES add 'chartjs', 'd3', 'echarts' (alpha); LIB_ABBREV (cjs/d3/ec); LIB_TO_LANG all three → 'javascript'; LANG_DISPLAY javascript: 'JavaScript'; LANG_EXT javascript: 'js'. New LIB_TO_FRAMEWORK map (all 'none' for now) to back the future React filter (feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243).
  • CodeHighlighter.tsx: register javascript (and jsx/tsx so feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243 inherits it) Prism grammars; map PRISM_LANGUAGE. TS7016 blocker: declare the prism modules in app/src/types/react-syntax-highlighter.d.ts in the same PR (feat(makie): add Julia + Makie.jl as the third language (Phase 5) #7613 fixed this inline for Julia — match it).
  • MastheadRule.tsx: JS block-comment tokens ({ open: '/*', close: '*/' }) if absent.
  • LibraryCard.tsx: descriptions for the three libs.
  • PlotOfTheDay.tsx / PlotOfTheDayTerminal.tsx: node runner token + .js ext switch.
  • DebugPage.tsx: three new SpecStatusItem columns (grid auto-grows via LIBRARIES.length — verify, don't hardcode).
  • LibrariesPage.tsx / AboutPage.tsx / PlotsPage.tsx: meta-description → "…across Python, R, Julia, and JavaScript".
  • Tests: bump every *.test.tsx that enumerated libs/languages or hardcoded a count (CodeHighlighter.test.tsx JS case, useCodeFetch.test.ts ?language=javascript + per-language cache-key, DebugPage.test.tsx).

6. Backend (api/ + core/)

  • specs.py: get_impl_code keeps default "python"; verify ?language=javascript returns 200 and the interactive HTML artifact is served like the Python highcharts/bokeh previews.
  • debug.py: SpecStatusItem + library_names for the three libs.
  • libraries.py: verify the three entries (incl. the new framework field) appear in /api/libraries.
  • stats.py: languages count auto-derives from SUPPORTED_LANGUAGES; verify it reports 4.
  • insights.py / seo.py / mcp/server.py: replace any hardcoded library/language enumerations with the constants.
  • core/database/models.py: the new framework field needs a column + migration (or a JSON metadata column) — decide and note in the PR.
  • sync_to_postgres.py: honor the per-library extension override (§1) so .js (and later .tsx) under implementations/javascript/ are discovered.
  • label_manager.py: post-merge, run label_manager sync once so library:{chartjs,d3,echarts} / generate:* / impl:* labels appear.

7. Tags / Plausible / SEO

  • docs/reference/plausible.md: promote javascript to a current language slug; no new event types.
  • docs/reference/seo.md: sitemap auto-picks up /{spec}/javascript/{chartjs,d3,echarts}; optional js.anyplot.ai subdomain is a separate small PR.
  • docs/reference/tagging-system.md: verify no narrative text hardcodes "python, r, or julia".

8. Tests

  • tests/unit/core/test_constants.py: add JavaScript + the three libs; assert len(SUPPORTED_LANGUAGES) == 4, len(SUPPORTED_LIBRARIES) == 14; assert every library has a valid framework; assert the per-library extension override resolves.
  • tests/unit/api/test_debug.py, test_routers.py, test_stats.py, test_schemas.py, mcp/test_tools.py: verify len(...)-derived assertions pass without per-item edits (fix in place, no noqa).
  • Frontend: useCodeFetch.test.ts JavaScript case + four-language cache-key; CodeHighlighter.test.tsx JS case; DebugPage.test.tsx new columns.
  • Harness test (new): the harness produces a non-empty PNG at exact canvas size for one framework=none lib (Chart.js).

9. Docs

  • docs/concepts/library-expansion.md: §1 table grows to 14 entries; §8 mark Phase 1 shipped; §9 record the render-strategy decision (browser harness; SSR considered & rejected — jsdom getBBox), the framework-field rollout, and the per-library-extension mechanism.
  • docs/reference/repository.md: implementations/ gains a javascript/ sibling.
  • docs/concepts/vision.md, docs/reference/style-guide.md: pipeline-story copy → "fourteen libraries across four languages" (the siblings bump it further).
  • prompts/README.md, agentic/docs/project-guide.md, README.md: counts + per-library entries.

10. R/Julia-rollout gaps to not repeat

Gap Verify for JavaScript
/api/specs/.../{lib}/code 404 (no language piped) (#6961) curl /api/specs/scatter-basic/chartjs/code?language=javascript → 200, light and dark.
Frontend LIBRARIES silently skipped the lib (#6961) /libraries shows all three new cards; landing strip shows languages: 4.
DebugPage hardcoded 'python' (#7066) JS cell deep-links to /{spec}/javascript/{lib}.
SpecStatusItem column missing (#7066) Matrix grows to LIBRARIES.length columns, no hardcoded edit.
Hardcoded counts (#7066) Grep finds zero hardcoded library counts beyond intentionally-pinned fixtures.
PlotOfTheDayTerminal hardcoded runner (#6947) Terminal chip on a JS POTD reads node ….
Header-rewrite prepend fallback (#6947) JS branch uses re.match first, prepends the canonical // header if absent.
@types/react-syntax-highlighter missing grammar decl (#6961) Declare prism/javascript (+ jsx/tsx for #8243) in the same PR — zero TS7016 carry-forward.

Acceptance criteria

  • core/constants.py: SUPPORTED_LANGUAGES includes "javascript"; SUPPORTED_LIBRARIES contains chartjs, d3, echarts; every library has a framework; per-library extension override mechanism exists.
  • .github/actions/setup-node/action.yml installs Node 22, restores from package-lock.json, installs Playwright Chromium, smoke-tests a Chart.js PNG.
  • Shared render harness produces exact-size PNGs (3200×1800 / 2400×2400) for the framework=none libs, reading ANYPLOT_THEME; the framework=react branch is scaffolded (exercised in feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243).
  • All workflows derive (LANGUAGE, EXT) from LIBRARY via the shared table with chartjs|d3|echarts → (javascript, .js).
  • prompts/library/{chartjs,d3,echarts}.md exist (makie shape).
  • Frontend renders language="javascript" with JS highlighting — no @types/react-syntax-highlighter TS7016 carry-forward.
  • gh workflow run impl-generate.yml -f specification_id=scatter-basic -f library=chartjs produces a passing JS impl (plot-light.png + plot-dark.png + interactive HTML) in GCS. Same for d3, echarts.
  • /scatter-basic/javascript/{chartjs,d3,echarts} loads code + preview in light & dark; ?language=javascript 200s.
  • /debug matrix has the three new columns; clicking a JS cell deep-links to /{spec}/javascript/{lib}.
  • /libraries lists all three; /stats reports languages: 4; /sitemap.xml includes the JS routes.
  • uv run pytest tests/unit tests/integration green; frontend yarn test --run, yarn tsc --noEmit, yarn lint clean.
  • docs/concepts/library-expansion.md Phase 1 marked shipped; counts updated; render-strategy decision recorded in §9.
  • Post-merge: label_manager sync run once.

Out of scope (covered elsewhere)

  • Highcharts Python → JavaScript migrationfeat(highcharts): migrate Highcharts from Python to JavaScript (Phase 2, deprecation window) #8242 (Phase 2). Reuses this issue's runtime/harness.
  • MUI X Charts (React/TSX path)feat(muix): add MUI X Charts (community, React framework) as a JavaScript entry #8243 (Phase 4 pulled forward). Reuses this issue's runtime/harness + framework field; the React-bundling path is the riskiest surface, isolated there.
  • plotly.js as a separate entry — Python is the most-used Plotly variant by ~22× (§6); one entry only, in Python. Plotly stays Python.
  • Recharts, Observable Plot, ApexCharts — later phases; the framework field added here unblocks Recharts when it's time.
  • js.anyplot.ai subdomain — small optional follow-up modeled on the python.anyplot.ai nginx block.
  • First batch of implementations across existing specs — the regular bulk-generate.yml flow once this lands.

Foundation issue of the 3-way JavaScript rollout split (#8241#8242, #8243). Reviewed against docs/concepts/library-expansion.md §§1–9, the R chain (#6944/#6947/#6961/#7066), and the Julia rollout (#7612 / #7613).

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